DotNet developer blog C# | .Net
802 subscribers
85 photos
36 links
Blog about C# .NET development. Almost all posts are published in english on my linkedin page https://www.linkedin.com/in/yegor-sychev/
Download Telegram
Мутационное тестирование

Мутационное тестирование — это способ оценки качества наших юнит-тестов.
Для мутационного тестирования существует инструмент 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.
{
"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 под названием 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. Для этого необходимо:

Отредактировать шаблон для классов:

Откройте файл 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: Подготовьте и настройте файл .editorconfig, который будет задавать базовые правила форматирования кода для всей команды.

— Отсутствие Warnings: Договоритесь, что в проекте не допускаются warnings, что позволит поддерживать высокое качество кода с самого начала.

— Статические анализаторы: После реализации базовых настроек интегрируйте инструменты статического анализа кода, такие как Roslynator, Meziantou, SonarQube. Они помогут обнаруживать проблемы до этапа ревью.

— Настройка подавлений (Suppression): Определите, какие правила анализаторов можно подавлять для упрощения работы, и добавьте соответствующие исключения в .editorconfig.

Важно понимать, что эти техники не заменяют соглашения о коде, принятые в вашей команде. В самом начале этого соглашения четко определите его цели - улучшить качество кода, снизить риск ошибок и упростить процесс код-ревью.

Внедряя эти практики, вы не только повысите качество своего продукта, но и разовьете культуру ответственности за код, что положительно скажется на профессиональном росте всей команды.

Пост в LinkedIn, Medium
👍11
Удобный способ сгенерировать файл editorconfig по вашим требованиям

В предыдущем посте я рассказывал, как создать единое техническое пространство для соблюдения codestyle-конвенций внутри команды.

Это пространство включает настройки IDE (например, настройки правил code cleanup, автоматическую очистку кода при сохранении файлов), набор статических анализаторов и файл editorconfig, по которому работает функция очистки IDE и в котором задаются исключения для правил статических анализаторов.

Сегодня я хочу поделиться удобным способом генерации файла editorconfig для C# в Visual Studio 2022.

Для начала откройте диалоговое окно Options через меню Tools, затем выберите Text-Editor–C#–Code Style–General.

В открывшемся окне вы увидите список правил стиля кода с указанием их предпочтений (Preference) и уровня важности (Severity), а также визуальное отображение примеров настроек для каждого правила. Именно благодаря такому наглядному представлению я предпочитаю этот метод генерации editorconfig.

После выбора необходимых предпочтений для правил, установите для каждого из них соответствующий уровень важности (Severity).

Например, в слайде, где показаны настройки для Code block preferences, для параметра Preference braces выбрано значение Yes с уровнем важности Warning. Это означает, что если в проекте будет использована конструкция if без фигурных скобок, IDE выдаст предупреждение.

После настройки правил достаточно нажать Generate .editorconfig file from settings и сохранить файл в корне проекта.

Пост на LinkedIn
👍8
Спустя два месяца мой пул-реквест наконец принят и замержен в документацию .NET. Надеюсь, что уже в ближайшее время на learn.microsoft.com появится мой небольшой гайд по генерации юнит-тестов с использованием Copilot и Visual Studio 2022. А сейчас в разработке находится ещё один гайд
🔥35
Как навести порядок в зависимостях пакетов в проектах?

Часто в legacy-проектах возникает хаос с зависимостями: неиспользуемые библиотеки, пакеты, подключенные не к тем проектам, или использование транзитивных зависимостей вместо прямых.

Visual Studio позволяет быстро удалить неиспользуемые пакеты.
Для этого:

— Кликните правой кнопкой мыши по проекту.
— Выберите пункт Remove Unused References...

Однако после этой операции могут возникнуть ошибки. Почему?

Первая причина

Пакет подключен к одному проекту, но используется в другом (зависимом).
Например:

— Проекты API и Core, где API зависит от Core.
— Пакет prometheus-net.AspNetCore ошибочно подключен к Core, хотя используется только в API.
— После команды Remove Unused References пакет prometheus-net.AspNetCore удаляется из Core, что вызовет ошибки в проекте API.
— Решение: доустановите нужный пакет непосредственно в API (например, через комбинацию Alt+Enter → Install package).

Вторая причина

Используются косвенные зависимости.
Например:

— В проекте зарегистрирован пакет prometheus-net.AspNetCore, хотя прямое использование Prometheus отсутствует.
— Однако класс HttpRequest использует зависимости этого пакета.
— После удаления неиспользуемых ссылок появятся ошибки, так как HttpRequest потеряет необходимые зависимости.
— Решение — подключение подходящего целевого пакета, например, Microsoft.AspNetCore.Http.Abstractions.

Таким образом, правильный подход к управлению зависимостями не только очищает проект от лишних библиотек, но и делает кодовую базу более прозрачной и управляемой.

Пост на LinkedIn, Medium
👍8🔥1
Насыщенная была неделя. Мои гайды наконец появились на официальном сайте Learn Microsoft. Опубликовался гайд по мутационному тестированию (https://learn.microsoft.com/en-us/dotnet/core/testing/mutation-testing) и руководство по генерации тестов с помощью GitHub Copilot в Visual Studio 2022 (https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-copilot).

Встретился с Данилом @danny_v3 автор канала "Логгирую разработку". На его канале можно найти записи реальных собеседований. Это его первый визит в Алматы, и я не мог упустить шанс показать горы.

Ребята с подкаста RadioDotNet порадовали меня классным мерчем (см. фото) – такие подарки отправляются тем, кто поддерживает подкаст на Boosty. Очень рекомендую послушать их передачи, особенно плейлист RadioDotNet, обсуждение книг "System Design Interview" и "Architecture for Containerized .NET Applications".
🔥12👍5
Использование различных политик CORS для конечных точек в ASP.NET

Иногда возникает ситуация, когда отдельным конечным точкам (endpoint) требуется применять отличную от основной политику CORS.

Представим, что большинство конечных точек вашего API взаимодействует только с фронтендом, размещенным на домене sample com, и вы настроили общую политику CORS с origin sample com (например, назвав ее AllowSampleCom).

Однако одному или нескольким методам нужно разрешить запросы с другого домена, скажем, admin-sample com.

В таком случае удобно создать отдельную политику, например AllowAdminSampleCom, специально для этого домена.

Затем вы можете точечно применять созданную политику к методам:

— Для контроллеров используйте атрибут [EnableCors("AllowAdminSampleCom")]
— Для Minimal API используйте метод app.MapGet("/endpoint", handler).RequireCors("AllowAdminSampleCom")

Если же для конкретного метода требуется вовсе отключить CORS, используйте атрибут [DisableCors]

Это позволяет эффективно и гибко управлять CORS в вашем приложении.

Пост на LinkedIn, Medium
🔥12👍5
Используйте панель Toolbox в Visual Studio 2022

Панель Toolbox хорошо известна разработчикам, которые работали с WinForms - в ней располагались готовые компоненты.

В разработке современных приложений мы также можем использовать эту панель для сохранения собственных сниппетов кода, которые часто применяются в различных проектах.

Чтобы открыть панель, перейдите в View → Toolbox или используйте сочетание клавиш Ctrl+Alt+X.

Чтобы добавить сниппет, достаточно выделить нужные строки кода и перенести их в панель с помощью drag-n-drop. Затем, для удобства, можно переименовать сниппет.

После этого вы можете перетаскивать сниппеты (также с помощью drag-n-drop) в нужные участки кода в разных проектах и решениях.
👍12✍1
Архитектура которую я использую в своих проектах

В этом посте я хочу поделиться архитектурой приложений, которую я использую в реальных проектах и которая хорошо себя зарекомендовала.

При проектировании архитектуры приложения я поставил для себя следующие требования:

— Приложение должно легко покрываться тестами
— Приложение должно легко обновляться, переходить на новые версии .Net
— Код должен быть простым, так его становится легче поддерживать, новые разработчики быстрее втягиваются в процесс
— Легко можем заменить поставщика данных, например с БД на внешний сервис, такое часто происходит в крупных компаниях с множеством команд разработки
— Не использовать библиотеки которые влияют на архитектуру Revo, Marten, MediatR и т. д.

На практике я встречал много проектов которые не соответствовали этим требованиям. Поддержка, написание новых фич было достаточно затруднительно, никаких тестов соответственно тоже не было.

Данная архитектура является чистой, используется всего три слоя: Api, Core, Infrastructure, однако не стоит путать с трехзвенной архитектурой, в нашем случае используется принцип инверсии зависимостей. Так например, бизнес-слой (Core) не использует напрямую слой Infrastructure и не ссылается на него как проект.

Пример структуры приложения:
Api.csproj
├── Controllers
│ └── ProductController.cs

Core.csproj
├── RepositoriesContracts
│ └── IProductRepository.cs
├── UseCases
│ └── GetProduct
│ ├── GetProductUseCase.cs
│ └── IGetProductUseCase.cs

Infrastructure.csproj
├── Repositories
│ └── ProductRepository.cs


Использование только трех слоев позволяет избежать путаницы и возможной протечки абстракции в перспективе. Вся бизнес-логика расположена в слое Core.

Как мы помним, основное требование было возможность замены поставщика данных без внесения изменений в бизнес-слой. Это действительно важное требование в наших приложениях, так как источники данных часто меняются. Например, ранее отправляли данные по REST сейчас другая команда просит отправлять эти данные в Kafka или раннее данные получали из БД затем часть этого функционала забрала себе другая команда и выставила REST.

Чтобы выполнить это требование бизнес-логика в слое Core должна работать с данными только через интерфейсы, в моем случае они имеют суффикс Repository тем самым абстрагируясь от типа источника данных.

Далее для выполнения этого требования модель данных, которую мы получили от источники должна мапится в другую модель - возвращаемый результат IProductRepository. Маппинг должен происходить на уровне Infrastructure, помним что Core не должен иметь зависимость от других проектов, но Infrastructure имеет зависимость от Core т.к. реализует интерфейс IProductRepository.

Такая абстракция также позволяет нам легко писать юнит-тесты, а с началом использования Copilot в нашей команде с этим отлично справляется Github Copilot.

Я использую эту архитектуру на протяжении 3-х лет и она отлично себя зарекомендовала, мы начали применять ее для других проектов в команде.
За это время действительно несколько раз менялись источники данных и это происходило без серьезных изменений в коде проекта. Благодаря простоте архитектуры Copilot отлично генерирует юнит-тесты, которые, в большинстве случаев запускаются с первого раза.

Пост на LinkedIn, Medium
👍18🔥8