Упрощаем написание юнит-тестов с 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
Я активно использую 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 на английском языке
В предыдущем посте я писал о том, как мы можем генерировать структуру тестовых проектов с помощью 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
При написании юнит-тестов на сервисы, где есть вызов внешнего 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