Как замокать HttpClient
При написании юнит-тестов на сервисы, где есть вызов внешнего API через HttpClient, в нашем упрощенном примере это ArticleService, нам необходимо подменить реальные вызовы API на моки.
При этом просто замокать интерфейс IHttpClientFactory, используя, например, NSubstitute, будет недостаточно. Нам необходимо создать класс MockHttpMessageHandler, который будет наследоваться от HttpMessageHandler.
MockHttpMessageHandler в конструкторе принимает HttpResponseMessage (сообщение, которое мы ожидаем получить от нашего мока) и просто возвращает его в переопределенном методе SendAsync.
Далее, при создании HttpClient, передаем в конструктор экземпляр класса MockHttpMessageHandler и делаем этот HttpClient возвращаемым значением метода CreateClient экземпляра mockHttpClientFactory.
В результате мы получили достаточно простой способ мокать HttpClient без внешних зависимостей с возможностью гибко настраивать ожидаемые http ответы.
Пост на Medium
При написании юнит-тестов на сервисы, где есть вызов внешнего API через HttpClient, в нашем упрощенном примере это ArticleService, нам необходимо подменить реальные вызовы API на моки.
При этом просто замокать интерфейс IHttpClientFactory, используя, например, NSubstitute, будет недостаточно. Нам необходимо создать класс MockHttpMessageHandler, который будет наследоваться от HttpMessageHandler.
MockHttpMessageHandler в конструкторе принимает HttpResponseMessage (сообщение, которое мы ожидаем получить от нашего мока) и просто возвращает его в переопределенном методе SendAsync.
Далее, при создании HttpClient, передаем в конструктор экземпляр класса MockHttpMessageHandler и делаем этот HttpClient возвращаемым значением метода CreateClient экземпляра mockHttpClientFactory.
В результате мы получили достаточно простой способ мокать HttpClient без внешних зависимостей с возможностью гибко настраивать ожидаемые http ответы.
Пост на Medium
👍11
Как использовать ADR, чтобы он был полезен и не превратился в бюрократию
Ведение записей архитектурных решений (ADR) — это отличная идея. Однако полное следование всем рекомендациям по ведению ADR может превратиться в бюрократию.
Как часто бывает с документацией, она становится обузой, а не полезным инструментом. Давайте разберёмся, как можно вести ADR, не затрачивая чрезмерных усилий на их заполнение и поддержание актуальности.
Рекомендации по ведению ADR:
— Используйте один файл для всех записей, если у вас небольшой проект или новый проект с количеством записей до 10.
— Минимум текста, максимум сути. Например, вы выбрали PostgreSQL — напишите кратко, почему. Нет необходимости перечислять все причины отказа от других баз данных, таких как SQL Server или Oracle.
— Легкочитаемость важнее формата. Часто пару предложений в свободном стиле будут полезнее, чем заполнение строгих полей в официальном формате.
— Акцент на актуальность. Актуальность записей важнее их объёма или строгого соблюдения форматов.
Что действительно должна содержать запись ADR:
— Дата решения. Записи должны быть в хронологическом порядке.
— Заголовок. Краткое описание решения.
— Имя сотрудника, кто принимал решение. Через пару лет сотрудник, возможно, всё ещё будет работать в компании, либо это облегчит поиск коллег, знакомых с проектом.
— Описание самого решения.
— Указание ограничений. Если решение было неочевидным и принималось из-за ограничений, важно указать их.
— Привязка к конкретному классу (при необходимости). Например, если вы решили использовать кэширование данных через Redis, можно указать название класса RedisCacheProvider.cs. Это может показаться избыточным, но такая запись включит ADR в контекст GitHub Copilot, что даст ему больше понимания архитектурных решений. Об этом я упоминал в одном из предыдущих постов.
— Уникальный ID записи. Используя ID и заголовок записи, вы сможете ссылаться на неё из XML-комментариев, например для RedisCacheProvider.cs, что также повысит эффективность работы с Copilot.
Итог:
Если у вас небольшие проекты или на документацию не выделяется достаточно времени, упрощайте ведение ADR, но не отказывайтесь от него совсем. ADR-записи действительно могут быть полезными: для новых разработчиков, GitHub Copilot, а также для сотрудников, которые будут работать с проектом в будущем.
Посты на Linkedin, Medium
Ведение записей архитектурных решений (ADR) — это отличная идея. Однако полное следование всем рекомендациям по ведению ADR может превратиться в бюрократию.
Как часто бывает с документацией, она становится обузой, а не полезным инструментом. Давайте разберёмся, как можно вести ADR, не затрачивая чрезмерных усилий на их заполнение и поддержание актуальности.
Рекомендации по ведению ADR:
— Используйте один файл для всех записей, если у вас небольшой проект или новый проект с количеством записей до 10.
— Минимум текста, максимум сути. Например, вы выбрали PostgreSQL — напишите кратко, почему. Нет необходимости перечислять все причины отказа от других баз данных, таких как SQL Server или Oracle.
— Легкочитаемость важнее формата. Часто пару предложений в свободном стиле будут полезнее, чем заполнение строгих полей в официальном формате.
— Акцент на актуальность. Актуальность записей важнее их объёма или строгого соблюдения форматов.
Что действительно должна содержать запись ADR:
— Дата решения. Записи должны быть в хронологическом порядке.
— Заголовок. Краткое описание решения.
— Имя сотрудника, кто принимал решение. Через пару лет сотрудник, возможно, всё ещё будет работать в компании, либо это облегчит поиск коллег, знакомых с проектом.
— Описание самого решения.
— Указание ограничений. Если решение было неочевидным и принималось из-за ограничений, важно указать их.
— Привязка к конкретному классу (при необходимости). Например, если вы решили использовать кэширование данных через Redis, можно указать название класса RedisCacheProvider.cs. Это может показаться избыточным, но такая запись включит ADR в контекст GitHub Copilot, что даст ему больше понимания архитектурных решений. Об этом я упоминал в одном из предыдущих постов.
— Уникальный ID записи. Используя ID и заголовок записи, вы сможете ссылаться на неё из XML-комментариев, например для RedisCacheProvider.cs, что также повысит эффективность работы с Copilot.
Итог:
Если у вас небольшие проекты или на документацию не выделяется достаточно времени, упрощайте ведение ADR, но не отказывайтесь от него совсем. ADR-записи действительно могут быть полезными: для новых разработчиков, GitHub Copilot, а также для сотрудников, которые будут работать с проектом в будущем.
Посты на Linkedin, Medium
👍10
Мутационное тестирование
Мутационное тестирование — это способ оценки качества наших юнит-тестов.
Для мутационного тестирования существует инструмент Stryker NET, который автоматически выполнит мутации в коде, запустит тесты и составит подробный отчет с результатами.
Давайте разберемся, как это работает на примере.
У нас есть класс
В методе есть несколько ветвлений: если
Для начала установим Stryker NET. Для этого нужно выполнить команду
Далее запустим Stryker с помощью команды, из директории, где расположены юнит-тесты
После выполнения тестирования в консоли отобразится отчет — сколько мутантов было убито, а сколько выжило. Более подробный отчет в формате html появится в директории StrykerOutput тестового проекта.
Теперь рассмотрим, что такое мутанты и что значит «выжившие» и «убитые».
Мутант — это преднамеренная модификация кода, которую делает Stryker, чтобы проверить, изменятся ли результаты юнит-тестов.
В нашем примере мутант — это замена выражения
Stryker поддерживает несколько видов мутаций:
— эквивалентные (например, заменяет < на <=)
— арифметические (+ на -)
— строковые («text» на «»)
— логические (&& на ||)
и т. д., с полным списком вы можете ознакомиться в документации Stryker NET.
Если после изменения нашего кода юнит-тесты прошли успешно, значит, они недостаточно качественные, и мутант выжил.
После запуска мутационного тестирования выжило 5 мутантов. Добавим тестовые данные на граничные значения и повторно запустим мутационное тестирование.
После исправления эквивалентных мутантов у нас остались только строковые мутации, которые мы можем легко «убить» проверкой на текст exception message.
Таким образом, мутационное тестирование помогает находить слабые места в тестах и делает их надежнее. Оно заставляет проверять не только 'happy path', но и сложные граничные случаи, снижая вероятность багов в продакшене.
Доклад «Мутационное тестирование в .NET»
Testing Your Tests: Mutation Testing in C# with Stryker
Мой пост на LinkedIn, Medium
Мутационное тестирование — это способ оценки качества наших юнит-тестов.
Для мутационного тестирования существует инструмент Stryker NET, который автоматически выполнит мутации в коде, запустит тесты и составит подробный отчет с результатами.
Давайте разберемся, как это работает на примере.
У нас есть класс
PriceCalculator с методом Calculate, который считает цену с учетом скидки. Этот метод покрыт юнит-тестами.В методе есть несколько ветвлений: если
price меньше или равно 0, то мы выбросим исключение, а также если discountPercent меньше 0 или больше 100, то будет исключение.Для начала установим Stryker NET. Для этого нужно выполнить команду
dotnet tool install -g dotnet-stryker
Далее запустим Stryker с помощью команды, из директории, где расположены юнит-тесты
dotnet stryker
После выполнения тестирования в консоли отобразится отчет — сколько мутантов было убито, а сколько выжило. Более подробный отчет в формате html появится в директории StrykerOutput тестового проекта.
Теперь рассмотрим, что такое мутанты и что значит «выжившие» и «убитые».
Мутант — это преднамеренная модификация кода, которую делает Stryker, чтобы проверить, изменятся ли результаты юнит-тестов.
В нашем примере мутант — это замена выражения
price <= 0, например, на price < 0, после чего запускаются юнит-тесты.Stryker поддерживает несколько видов мутаций:
— эквивалентные (например, заменяет < на <=)
— арифметические (+ на -)
— строковые («text» на «»)
— логические (&& на ||)
и т. д., с полным списком вы можете ознакомиться в документации Stryker NET.
Если после изменения нашего кода юнит-тесты прошли успешно, значит, они недостаточно качественные, и мутант выжил.
После запуска мутационного тестирования выжило 5 мутантов. Добавим тестовые данные на граничные значения и повторно запустим мутационное тестирование.
[Fact]
public void InvalidPrice_ShouldThrowException()
{
Assert.Throws<ArgumentException>(()
=> PriceCalculator.CalculatePrice(0, 10));
// change price from -10 to 0
}
[Fact] // Added test for 0 and 100 discount
public void NoExceptionForZeroAnd100Discount()
{
var exceptionWhen0 = Record.Exception(() => PriceCalculator.CalculatePrice(100, 0));
var exceptionWhen100 = Record.Exception(() => PriceCalculator.CalculatePrice(100, 100));
Assert.Null(exceptionWhen0);
Assert.Null(exceptionWhen100);
}
После исправления эквивалентных мутантов у нас остались только строковые мутации, которые мы можем легко «убить» проверкой на текст exception message.
Таким образом, мутационное тестирование помогает находить слабые места в тестах и делает их надежнее. Оно заставляет проверять не только 'happy path', но и сложные граничные случаи, снижая вероятность багов в продакшене.
Доклад «Мутационное тестирование в .NET»
Testing Your Tests: Mutation Testing in C# with Stryker
Мой пост на LinkedIn, Medium
👍10
Полиморфная сериализация с System.Text.Json
Иногда из внешних сервисов приходят данные, содержащие один объект, но с разной структурой.
В нашем примере мы получаем JSON заказа Order, который содержит вложенный объект Client.
При этом Client может быть двух типов: Person или Company, каждый со своими уникальными полями.
Для определения типа клиента используется поле ClientType.
— Создать базовый класс
— Использовать атрибут
— Добавить
При этом в самих классах
Таким образом, при десериализации .NET автоматически выберет нужный класс, исключая необходимость ручного определения типа.
Атрибуты
Посты LinkedIn, Medium
Иногда из внешних сервисов приходят данные, содержащие один объект, но с разной структурой.
В нашем примере мы получаем JSON заказа Order, который содержит вложенный объект Client.
{
"Id": 1,
"Client": {
"ClientType": "Person",
"FullName": "John Doe"
}
}
{
"Id": 2,
"Client": {
"ClientType": "Company",
"CompanyName": "Big Tech Ltd"
}
}При этом Client может быть двух типов: Person или Company, каждый со своими уникальными полями.
Для определения типа клиента используется поле ClientType.
— Создать базовый класс
Client— Использовать атрибут
[JsonPolymorphic], указывая ClientType в качестве дескриминатора— Добавить
[JsonDerivedType] над базовым классом Client, указав производные классы PersonClient и CompanyClient и задав их соответствующие значения в ClientType (Person или Company).При этом в самих классах
PersonClient и CompanyClient нет необходимости вручную указывать поле ClientType — оно будет автоматически добавлено при сериализации и использоваться при десериализации для выбора нужного класса.Таким образом, при десериализации .NET автоматически выберет нужный класс, исключая необходимость ручного определения типа.
Атрибуты
[JsonPolymorphic] и [JsonDerivedType] доступны только с .NET 7. В более ранних версиях потребуется кастомный конвертер JsonConverter<T>.Посты LinkedIn, Medium
👍17
Как протестировать Middleware
В качестве примера возьмем простой кастомный Middleware под названием
Тестирование Middleware на первый взгляд может показаться сложным, так как требуется создавать HttpContext. Чтобы не зависеть от реальных HTTP-запросов и лишней инфраструктуры, будем использовать
Создадим тест с возникновением исключения
Для этого нам нужно замокать
Затем определим RequestDelegate, который выбросит исключение, и создадим экземпляр
После этого вызываем созданный Middleware, передавая ему
В секции проверки теста убеждаемся, что в
Тест без исключения
Также необходим тест, чтобы убедиться, что при отсутствии исключения код 500 не возвращается и логгер не вызывается. Для этого в RequestDelegate не выбрасываем исключение и проверяем, что StatusCode не равен 500, а метод логгера не был вызван.
Таким образом, мы можем проверить корректную работу нашего
Пост на LinkedIn, Medium
В качестве примера возьмем простой кастомный Middleware под названием
ExceptionHandlingMiddleware, который перехватывает исключения во время выполнения запроса, логирует их и возвращает HTTP-статус-код 500 (Internal Server Error).Тестирование Middleware на первый взгляд может показаться сложным, так как требуется создавать HttpContext. Чтобы не зависеть от реальных HTTP-запросов и лишней инфраструктуры, будем использовать
DefaultHttpContext.Создадим тест с возникновением исключения
Для этого нам нужно замокать
ILogger, я буду использовать NSubstitute. Затем определим RequestDelegate, который выбросит исключение, и создадим экземпляр
ExceptionHandlingMiddleware, передавая в конструктор наш делегат и мок логгера. После этого вызываем созданный Middleware, передавая ему
DefaultHttpContext, и ожидаем, что он обработает исключение. В секции проверки теста убеждаемся, что в
httpContext.Response.StatusCode установлено значение StatusCodes.Status500InternalServerError, а логгер был вызван один раз с соответствующими параметрами.Тест без исключения
Также необходим тест, чтобы убедиться, что при отсутствии исключения код 500 не возвращается и логгер не вызывается. Для этого в RequestDelegate не выбрасываем исключение и проверяем, что StatusCode не равен 500, а метод логгера не был вызван.
Таким образом, мы можем проверить корректную работу нашего
ExceptionHandlingMiddleware, не завися от реальных HTTP-запросов и внешней инфраструктуры.Пост на LinkedIn, Medium
👍7🔥2
Редактирование шаблона класса C# в Visual Studio 2022
В Visual Studio 2022 можно изменить шаблон нового класса. Я делаю это, так как предпочитаю, чтобы создаваемые классы были sealed и использовали file-scoped namespace. Для этого необходимо:
Отредактировать шаблон для классов:
Откройте файл
* если у вас версия Professional, замените часть пути Community на Professional.
После этого внесите необходимые изменения в шаблон (например, как показано на изображении или по вашим требованиям).
Отредактировать шаблон для классов в проектах ASP NET Core:
Откройте файл
и отредактируйте его аналогичным образом.
Настройка file-scoped namespace:
Если вы хотите использовать file-scoped namespace, добавьте в файл
* file-scoped namespaces доступны с .Net 6
Таким образом, вы сможете настроить шаблоны классов в Visual Studio 2022 в соответствии с вашими предпочтениями.
Пост на LinkedIn, Medium
В Visual Studio 2022 можно изменить шаблон нового класса. Я делаю это, так как предпочитаю, чтобы создаваемые классы были sealed и использовали file-scoped namespace. Для этого необходимо:
Отредактировать шаблон для классов:
Откройте файл
Class.cs, расположенный по следующему пути:C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\ItemTemplates\CSharp\Code\1033\Class* если у вас версия Professional, замените часть пути Community на Professional.
После этого внесите необходимые изменения в шаблон (например, как показано на изображении или по вашим требованиям).
Отредактировать шаблон для классов в проектах ASP NET Core:
Откройте файл
Class.cs, находящийся по пути:C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\ItemTemplates\AspNetCore\Code\1033\Classи отредактируйте его аналогичным образом.
Настройка file-scoped namespace:
Если вы хотите использовать file-scoped namespace, добавьте в файл
.editorconfig следующую запись:[*.cs]
csharp_style_namespace_declarations = file_scoped:suggestion
* file-scoped namespaces доступны с .Net 6
Таким образом, вы сможете настроить шаблоны классов в Visual Studio 2022 в соответствии с вашими предпочтениями.
Пост на LinkedIn, Medium
👍13
Как эффективно внедрить практику чистого кода в команде?
Несмотря на усилия по созданию codestyle конвенций часто возникают трудности с их практическим применением в команде.
Чтобы договоренности работали на практике, необходимо создать единое техническое пространство:
— Единые настройки профиля Code Cleanup: В Visual Studio или Rider важно использовать единые настройки профиля Code Cleanup, чтобы все участники команды могли автоматически применять одни и те же стандарты по форматированию и рефакторингу кода.
— Auto Cleanup в IDE: Убедитесь, что в вашей IDE настроен автоматический запуск Cleanup при сохранении файлов. Это поможет соблюдать единый стиль кодирования в реальном времени.
— Использование унифицированных шаблонов классов и интерфейсов: Используйте стандартизированные шаблоны для классов и интерфейсов. В предыдущем посте я рассказывал, как создать шаблон класса для Visual Studio.
— EditorConfig: Подготовьте и настройте файл
— Отсутствие Warnings: Договоритесь, что в проекте не допускаются warnings, что позволит поддерживать высокое качество кода с самого начала.
— Статические анализаторы: После реализации базовых настроек интегрируйте инструменты статического анализа кода, такие как
— Настройка подавлений (Suppression): Определите, какие правила анализаторов можно подавлять для упрощения работы, и добавьте соответствующие исключения в
Важно понимать, что эти техники не заменяют соглашения о коде, принятые в вашей команде. В самом начале этого соглашения четко определите его цели - улучшить качество кода, снизить риск ошибок и упростить процесс код-ревью.
Внедряя эти практики, вы не только повысите качество своего продукта, но и разовьете культуру ответственности за код, что положительно скажется на профессиональном росте всей команды.
Пост в LinkedIn, Medium
Несмотря на усилия по созданию codestyle конвенций часто возникают трудности с их практическим применением в команде.
Чтобы договоренности работали на практике, необходимо создать единое техническое пространство:
— Единые настройки профиля Code Cleanup: В Visual Studio или Rider важно использовать единые настройки профиля Code Cleanup, чтобы все участники команды могли автоматически применять одни и те же стандарты по форматированию и рефакторингу кода.
— Auto Cleanup в IDE: Убедитесь, что в вашей IDE настроен автоматический запуск Cleanup при сохранении файлов. Это поможет соблюдать единый стиль кодирования в реальном времени.
— Использование унифицированных шаблонов классов и интерфейсов: Используйте стандартизированные шаблоны для классов и интерфейсов. В предыдущем посте я рассказывал, как создать шаблон класса для Visual Studio.
— EditorConfig: Подготовьте и настройте файл
.editorconfig, который будет задавать базовые правила форматирования кода для всей команды.— Отсутствие Warnings: Договоритесь, что в проекте не допускаются warnings, что позволит поддерживать высокое качество кода с самого начала.
— Статические анализаторы: После реализации базовых настроек интегрируйте инструменты статического анализа кода, такие как
Roslynator, Meziantou, SonarQube. Они помогут обнаруживать проблемы до этапа ревью.— Настройка подавлений (Suppression): Определите, какие правила анализаторов можно подавлять для упрощения работы, и добавьте соответствующие исключения в
.editorconfig.Важно понимать, что эти техники не заменяют соглашения о коде, принятые в вашей команде. В самом начале этого соглашения четко определите его цели - улучшить качество кода, снизить риск ошибок и упростить процесс код-ревью.
Внедряя эти практики, вы не только повысите качество своего продукта, но и разовьете культуру ответственности за код, что положительно скажется на профессиональном росте всей команды.
Пост в LinkedIn, Medium
👍11