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
Вот и завершился мой отпуск... Это была моя вторая поездка в Майами (впервые я побывал здесь в 2022 году) и пока для меня это самое комфортное место для отдыха. В этот раз я впервые посетил Ки-Уэст — самую южную точку США. Остался под большим впечатлением и обязательно сюда вернусь.
🔥4👍1
Как и обещал в предыдущем посте, продолжаем обсуждать отказоустойчивость и надежность приложения.

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

Сегодня обсудим ситуацию с отказом внешнего сервиса (или наш микросервис к которому мы обращаемся например по REST).
Потенциально, любой внешний сервис - это точка отказа, и мы должны корректно обработать его сбой или недоступность.

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

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

Если не предпринять никаких мер, то при сбое внешнего сервиса или при отправке им некорректных данных могут возникнуть неприятные сценарии для пользователя, например «белый экран» из-за сломанного JS и невозможность оформить заказ.

Чтобы избежать таких ситуаций, делаем следующее:

1. Оборачиваем вызов сервиса в try-catch.

2. Устанавливаем минимальный таймаут. Из-за деградации второстепенного сервиса пользователь не должен долго ждать загрузки ключевого функционала.

3. Делаем возможность отключения вызова сервиса «на горячую» через appsettings.json с помощью IOptionsMonitor.
Например, если мы понимаем, что сервис стал долго отвечать или отправлять некорректные данные, мы можем вручную отключить его вызов, не дожидаясь срабатывания таймаута. Здесь можно применить шаблон «размыкания цепи» (circuit breaker), например, с помощью библиотеки Polly, но возможность ручного отключения должна быть сохранена.

Таким образом, при выбросе исключения, срабатывании таймаута или ручном отключении вызова мы возвращаем пустой список рекомендаций вместо ошибки.
На фронте мы ожидаем такой сценарий и просто скрываем блок «Рекомендации».
👍5🔥4
Упрощаем написание юнит-тестов с Visual Studio и GitHub Copilot

Я активно использую GitHub Copilot уже больше года, и в целом он очень неплохо справляется с написанием юнит-тестов. Однако в настоящее время Copilot ещё не умеет создавать .NET-проекты и директории, а также не умеет напрямую размещать в проекте сгенерированные файлы.

Давайте попробуем использовать команды Visual Studio для генерации проектов xUnit с нужной нам структурой директорий и файлов.

Команда Create Unit Tests по умолчанию работает с MSTest, но так как я использую xUnit, мне необходимо скачать расширение для Visual Studio — xUnit.net.TestGenerator2022.

Чтобы сгенерировать заглушку для юнит-теста, нам нужно:

1. Выделить метод (в моём случае GetProductById класса ProductService).

2. Кликнуть правой кнопкой мыши и выбрать команду Create Unit Tests.

3. В появившемся диалоговом окне в выпадающем списке Test Framework выбрать xUnit (он появится только в том случае, если установлено соответствующее расширение).

Если у нас ещё нет проекта с тестами, оставляем New Test Project или же выбираем существующий.
При необходимости задаём шаблон наименования пространства имён (namespace), класса и метода, после чего нажимаем OK.

4. После нескольких секунд Visual Studio подтянет необходимые пакеты и мы получим сгенерированный xUnit-проект с нужными пакетами, структурой, ссылкой на тестируемый проект, а также (в моём случае) с классом ProductServiceTests и методом-заглушкой.

Теперь нам нужно сгенерировать сами тесты:

Снова выделяем тестируемый метод.

Кликаем правой кнопкой мыши — Ask Copilot.

Вводим простой prompt, например: generate unit tests using xunit, nsubstitute and insert the result into ProductServiceTests
(Здесь надо через # выбрать наш класс с тестами. Для быстрого поиска желательно, чтобы ProductServiceTests был открыт в отдельной вкладке.)

Выполняем prompt, нажимаем Accept, и Copilot генерирует код тестов. После этого остаётся установить необходимые пакеты — в моём случае это NSubstitute.

Когда пакеты будут установлены, можно запускать тесты. У меня они выполнились с первого раза: Copilot отлично знает, как работать с NSubstitute, а все зависимости у меня были оформлены через интерфейсы.

Таким образом, использование Visual Studio в сочетании с GitHub Copilot значительно упрощает процесс генерации и написания юнит-тестов.
В дальнейшем мы продолжим обсуждать, как проектировать надёжные приложения, которые будет легко покрывать тестами.

Пост на английском в Linkedin
👍10🔥2💯1
Как мы можем помочь GitHub Copilot генерировать качественные юнит тесты

В предыдущем посте я писал о том, как мы можем генерировать структуру тестовых проектов с помощью IDE, а также сами тесты — с помощью Copilot.

Качество генерации юнит-тестов напрямую зависит от того, насколько наш код и проект готовы к тестированию и насколько хорошо их понимает GitHub Copilot.
К счастью, рекомендации по написанию кода для Copilot не противоречат принципам «чистого кода».

Контекст

Основное преимущество Copilot перед ChatGPT — это доступ к большему контексту. Copilot ориентируется на открытые вкладки с файлами в IDE.
Таким образом, первая рекомендация — открывать вкладки, соответствующие запросу (промпту), а также ссылаться на файлы в самом промпте через символ #.
На самом деле, новые версии GitHub Copilot уже могут «подтягивать» весь проект в свой контекст. Однако это зависит от размера проекта. Поэтому вторая рекомендация — использовать хорошо структурированные, небольшие проекты. Copilot будет работать эффективнее с отдельным микросервисом, чем с огромным монолитом.

Мы разобрались с объёмом контекста, теперь перейдём к вопросу качества.

Сложность зависимостей

• Используйте зависимости через интерфейсы. Так Copilot сможет без проблем их «замокать» при помощи выбранной библиотеки, что упростит написание юнит-тестов.
• Старайтесь избегать избыточного использования паттернов и наследования — это может запутать не только Copilot, но и других разработчиков, которые впервые сталкиваются с проектом. (Об этом ещё напишу отдельный пост)

Ясность кода

• При необходимости составьте ADR (Architecture Decision Records) и включайте их в контекст, чтобы Copilot «понимал» ключевые архитектурные решения.
• Используйте описательные, недвусмысленные имена для переменных, методов и классов.
• Дополняйте код комментариями там, где это нужно.
• В основном Copilot верно определяет тип переменных, однако если возникают ошибки, лучше вместо var явно указать тип.
• Удалите код который сейчас не используется, если считаете что он может понадобиться в будущем, вы сможете восстановить его используя GIT

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

Этот пост на Medium на английском языке
👍4🔥2
Как замокать HttpClient

При написании юнит-тестов на сервисы, где есть вызов внешнего 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
👍10
Мутационное тестирование

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