.NET Разработчик
6.48K subscribers
420 photos
2 videos
14 files
2.02K links
Дневник сертифицированного .NET разработчика.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День 1729. #Testing
Тестирование на Основе Свойств. Реальный пример
Теория
Простой пример

Сегодня рассмотрим более реалистичный пример и функции, которые предоставляет FsCkeck. Допустим, нам нужно проверить номер кредитной карты.

Если у нас есть метод CardNumber.IsValid(), проверяющий валидность номера кредитной карты, мы можем добавить тесты, вроде следующих:
[TestCase("12345678910")]
[TestCase("12345621748")]
[TestCase("12345621777")]
[TestCase("Test")]
[TestCase("00000000000")]
[TestCase("99999999999")]
[TestCase("!@#!@%^@^@$^&@$^sdfasdf")]
[TestCase("$^@#^@##$44")]
[TestCase("15435#$%4354dfsg")]
[TestCase("90022742192")]
public void ValidationShouldFail(string num)
{
var result = CardNumber.IsValid(num);
result.Should().BeFalse();
}

Как видите, это вполне типичные примеры тестов. Некоторые магические значения использовались для проверки теста, но уверены ли мы, что охватили все крайние случаи?

Используем FsCheck:
[Property]
public bool ValidationShouldFail(string num)
{
return !CardNumber.IsValid(num);
}

Также можно использовать альтернативную форму записи теста:
[Property]
public void ValidationShouldFail()
{
Prop.ForAll<string>(
x => !CardNumber.IsValid(x)
)
.VerboseCheck();
}

Тест проходит, но если посмотреть расшифровку ([Property(Verbose = true)]), мы можем заметить, что проверяются случайные строки, и вряд ли хотя бы одна из них будет валидным номером карты. Мы можем заменить строку на long, но это тоже вряд ли сильно ограничит варианты.

Произвольные значения (Arbitraties)
FsCheck использует комбинацию генераторов (generator) и сокращателей (shrinker) для создания тестовых данных. Генераторы производят случайный набор значений из интервала с равномерным распределением. Сокращатели ограничивают (фильтруют) этот набор по заданному условию. В FsCheck определены произвольные значения по умолчанию для некоторых часто используемых типов:
- NegativeInt
- NonNegativeInt
- NonEmptyString
- IntWithMinMax
- NonEmptyArray
и т.п.

Используем альтернативную форму записи тестового метода с использованием предопределённых произвольных значений:
[Property(Verbose = true)]
public Property ValidationShouldFail()
{
var arb = Arb
.Default
.Int64()
.Filter(x => x > 100);

return Prop.ForAll<Int64>(
arb,
x => !CardNumber.IsValid(x.ToString()));
}

Здесь мы использовали произвольные значения для long и функцию-сокращатель (x > 100). Заметьте, что FsCheck сначала генерирует случайные значения, а затем проверяет их на соответствие условию сокращателя. В примере выше при попытке задать большое значение в функции Filter, тесты могут выполняться очень долго.

Продолжение следует…

Источник:
https://bartwullems.blogspot.com/2023/01/property-based-testing-in-cpart-3.html
👍4
День 1730. #Testing
Тестирование на Основе Свойств. Собственный генератор
Теория
Простой пример
Реальный пример

Сегодня рассмотрим, как писать собственные генераторы.

В предыдущем примере мы использовали Filter(), чтобы возвращать только нужные числа.

Для создания генератора нужен публичный статический класс с публичным статическим методом, возвращающим Arbitrary<T>.
public static class CCNumberGenerator
{
public static Arbitrary<Int64> Generate()
{
return Arb.Default
.Int64()
.Filter(x => x > 100);
}
}

Теперь его можно использовать в качестве типа генератора в атрибуте Property:
[Property(Arbitrary = new[] { 
typeof(CCNumberGenerator) },
Verbose = true)]
public bool ValidateShouldFail(long num)
{
return !CardNumber.IsValid(num.ToString());
}

В примере выше мы использовали существующий генератор. Но можно и создать его с нуля. Одной из наиболее часто используемых функций является Gen.Choose(), которая осуществляет случайный выбор значения из интервала с равномерным распределением.

Например, так можно создать генератор месяца в определённом году:
public static class MonthOfYearGenerator
{
public static Gen<MonthOfYear> Generator =
from month in Gen.Choose(1, 12)
from year in Gen.Choose(1982, 2023)
select new MonthOfYear()
{
Month = month,
Year = year
};

public static Arbitrary<MonthOfYear> Generate() =>
Arb.From(Generator);
}

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

В этом же классе можно создать свой сокращатель:
public static IEnumerable<MonthOfYear> 
Shrinker(MonthOfYear moy)
{
yield return new MonthOfYear() {
Month = moy.Month,
Year = moy.Year - 1
};
yield return new MonthOfYear() {
Month = moy.Month,
Year = moy.Year + 1
};
}

А затем использовать перегрузку метода Generate, принимающую и генератор, и сокращатель:
public static Arbitrary<MonthOfYear> Generate() =>
Arb.From(Generator, Shrinker);

Окончание следует…

Источник:
https://bartwullems.blogspot.com/2023/01/property-based-testing-in-cpart-4.html
👍5
День 1731. #Testing
Тестирование на Основе Свойств. Воспроизведение
Теория
Простой пример
Реальный пример
Собственный генератор

Для каждого тестируемого свойства FsCheck генерирует случайные входные значения. Эти значения могут быть разными для каждого запуска теста, что означает, что каждый запуск может закончиться разными результатами. Поэтому, если у вас было неправильное входное значение, которое не прошло тест в предыдущий раз, вполне возможно, что при следующем запуске теста вы больше не увидите этот неправильный вход. Для простых входных значений это не должно быть проблемой. Но если вы используете более сложные входные данные, повторить неудачный тест может быть трудно.

Если посмотреть на выходные данные неудавшегося теста, можно заметить строку вроде
Falsifiable, after 3 tests (4241 shrinks) (StdGen(318823861,297138967)).

StdGen – это начальные значения, которые FsCheck использовал для генерации входных данных. Если вы хотите использовать в своём тесте точно такие же входные данные, используйте это значение в свойстве Replay атрибута Property:
[Property(Replay="318823861,297138967")]
public bool ValidationShouldFail(long num)
{

}

Приёмочные тесты на основе свойств
Ещё одна область, где могут пригодиться тесты на основе свойств, — это случаи, когда вам нужен своего рода приёмочный тест для какого-то кода. Например, у вас есть некоторый метод, в правильности которого вы уверены, но необходимо провести его рефакторинг, либо он недостаточно быстр. Вы можете написать тест на основе свойств, в котором свойство, которое должно соблюдаться, заключается в том, что для любых входных данных результат изначального метода должен совпадать с результатом нового. Затем увеличьте количество случайных входных данных настолько, чтобы можно было быть уверенным в правильности. И если тесты проходят, значит новый метод работает правильно.

Итого
Тесты на основе свойств полезны, но не являются панацеей. Их может быть сложнее читать другим разработчикам, которые не привыкли проводить тестирование на основе свойств. Когда вы решаете реализовать свои собственные произвольные алгоритмы и ограничиваете возможные произвольные значения, вы рискуете упустить важные случаи из-за излишней строгости, как и при написании обычных тестов. Тесты на основе свойств также выполняются медленнее: по умолчанию они генерируют 100 случайных входных данных и проверяют их. Поскольку вы имеете дело со случайностью, это также означает, что два прогона теста не идентичны. У вас может быть удачный, а затем неудачный прогон. Если второй запуск выявит реальную проблему, то всё в порядке. Следует написать специальный модульный тест для этого случая, а затем исправить проблему. Однако если это связано с тем, что один из ваших генераторов случайно генерирует значения, которые сильно отличаются от ожидаемых входных данных, выявить и устранить проблему может быть сложнее. Это связано с тем, что сначала вам нужно идентифицировать генератор, дающий неожиданные значения, и обернуть его механизмом фильтрации. И, хотя это возможно сделать, это всё же больше работы, чем простое изменение постоянного значения, определённого в модульном тесте.

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

Источники:
-
https://bartwullems.blogspot.com/2023/01/property-based-testing-in-cpart-5.html
-
https://rasmus-feldthaus.medium.com/supercharge-your-testing-with-property-based-tests-bc3a7b75ca9f
👍4
День 1741. #ЗаметкиНаПолях #Testing
Пишем Тесты с Autofixture
AutoFixture - мощный инструмент для юнит-тестирования повседневных задач. Он помогает писать меньше кода в блоке Arrange. Рассмотрим его использование на примере совместно с библиотекой xUnit.

Сначала надо создать AutoFixture в тестовом классе:

private readonly IFixture _fixture;
public SampleTests()
{
_fixture = new Fixture();
}

AutoFixture использует обобщённый метод Create, генерирующий случайные данные для заданного типа, например:

var name = _fixture.Create<string>();
var age = _fixture.Create<int>();

Однако, с помощью дополнительного пакета AutoFixture.Xunit2, вы можете автоматически создавать целые классы, вообще без блока Arrange с помощью атрибута AutoData:

[Theory, AutoData]
public void TestEmployee(
string name, int age,
string pos, decimal rate)
{
// Act
var emp = new Employee(name, age, pos, rate);
// Assert
emp.Name.Should().Be(name);
emp.Age.Should().Be(age);
emp.Position.Should().Be(pos);
emp.Rate.Should().Be(rate);
}

Либо можно использовать метод Create:

[Fact]
public void TestEmployee()
{
// Arrange
var emp = _fixture.Create<Employee>();
var comp = _fixture.Create<Company>();

}

Здесь все свойства классов будут заданы случайными значениями.

Вы можете управлять созданием, задавая данные для определённых свойств. Также можно создавать коллекции элементов:

var emps = _fixture
.CreateMany<Employee>(5).ToList();
var comp = _fixture.Build<Company>()
.OmitAutoProperties()
.With(x => x.Employees, emps)
.Without(x => x.City)
.Create();

Здесь вызов OmitAutoProperties не будет задавать случайные значения свойствам, у которых есть значения по умолчанию. А Without не будет задавать значение выбранного свойства (в примере выше City останется null).

Вы можете создавать реализации интерфейсов с помощью метода Register:

_fixture.Register<IMyInterface>(() =>
new FakeMyInterface());

Здесь каждый раз, когда Autofixture нужно будет создать класс, зависящий от IMyInterface, будет создаваться экземпляр FakeMyInterface.

AutoFixture также можно использовать с пакетами для создания моков, вроде Moq или NSubstitute. Для этого надо настроить создание Autofixture (пример для Moq):

var _fixture = new Fixture()
.Customize(new AutoMoqCustomization
{ ConfigureMembers = true });

Тогда Autofixture можно будет использовать для создания реализаций интерфейсов:

var result = _fixture.Create<IMyInterface>();

При этом установка ConfigureMembers в true приводит к тому, что для классов-реализаций интерфейсов Autofixture сгенерирует случайные значения для свойств.

Здесь можно посмотреть множество других примеров использования AutoFixture.

Источник: https://dev.to/serhii_korol_ab7776c50dba/you-write-unit-tests-wrong-5d9f
👍24
День 1756. #Testing
Тестируем Валидацию Модели

Валидация модели имеет основополагающее значение для любого проекта: она обеспечивает безопасность и надёжность, выступая в качестве защиты от недопустимого состояния модели.

Надо бы добавить модульные тесты валидации модели. Фактически, при определении входной модели всегда следует учитывать как допустимые, так и, тем более, недопустимые модели, следя за тем, чтобы все недопустимые модели отбрасывались.

Определим простую модель:
public class User
{
[Required]
[MinLength(3)]
public string FirstName { get; set; }

[Required]
[MinLength(3)]
public string LastName { get; set; }

[Range(18, 100)]
public int Age { get; set; }
}

У нас есть два варианта: мы можем написать интеграционные тесты для отправки запросов в систему, на которой работает сервер, и проверить полученный ответ. Или мы можем использовать внутренний класс Validator, который используется ASP.NET для проверки моделей ввода, и создать быстрые юнит-тесты. Вот вспомогательный метод, который мы можем использовать в тестах:
public static IList<ValidationResult> 
ValidateModel(object model)
{
var results = new List<ValidationResult>();
var context =
new ValidationContext(model, null, null);

Validator.TryValidateObject(
model, context, results, true);

if (model is IValidatableObject vm)
results.AddRange(vm.Validate(context));

return results;
}

Мы создаём контекст проверки без какой-либо внешней зависимости, ориентированный только на модель ввода. Затем мы проверяем все свойства, вызывая TryValidateObject, и сохраняем результаты проверки в списке results. Наконец, если модель реализует интерфейс IValidatableObject, который предоставляет метод Validate, мы вызываем его и добавляем возвращённые ошибки в тот же список results. Таким образом мы можем обрабатывать как атрибуты полей, такие как [Required], так и пользовательскую проверку в методе Validate() класса модели.

Используем этот метод в тестах:
[Test]
public void Pass_WhenModelValid()
{
var model = new User {
FirstName = "Jon",
LastName = "Smith",
Age = 42
};

var result = ValidateModel(model);

Assert.That(result, Is.Empty);
}

[Test]
public void Fail_WhenAgeLessThan18()
{
var model = new User {
FirstName = "Jane",
LastName = "Smith",
Age = 17
};

var result = ValidateModel(model);

Assert.That(result, Is.Not.Empty);
}

Аналогично мы можем проверять и правильность возвращаемых ошибок, которые можно кастомизировать как в атрибутах валидации (свойство ErrorMessage), так и в методе Validate().

Источник: https://www.code4it.dev/csharptips/unit-test-model-validation/
👍12
День 1813. #Testing
Практикуем TDD

Люди не пришли к единому мнению относительно определения процесса разработки через тестирование (TDD). Сегодня рассмотрим процесс Cannon TDD, предложенный Кентом Бэком. Если вы делаете что-то по-другому и это вам подходит, отлично!

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

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

Шаги
Люди — паршивые компьютеры. Следующий процесс похож на алгоритм, но это не так. Он написан так в попытке эффективно общаться с людьми, привыкшими работать с программами. «Попытке», потому что люди склонны говорить: «TDD - отстой! Я сделал <совершенно другое>, и это провалилось».

1. Список тестов
Составьте список всех ожидаемых вариантов нового поведения:
- базовый случай,
- что, если время ожидания истечет,
- что, если ключа нет в БД,
и т.п.
Это поведенческий анализ. Вы думаете обо всех случаях, в которых изменение поведения должно сработать. Если вы думаете, как изменение поведения не должно нарушить существующее поведение, добавьте и это.

Ошибка: вносить детали реализации. Нет. Позже будет достаточно времени, чтобы решить, как будут выглядеть внутренности. Вы сможете лучше составить список тестов, если поведение - всё, на чём вы сосредоточитесь.

2. Напишите тест
Один. По-настоящему автоматизированный тест с настройкой, вызовом и утверждениями (совет: попробуйте начинать с утверждений). Именно сейчас вы начнёте принимать проектные решения, в первую очередь решения по интерфейсу. Некоторые решения по реализации могут просочиться, но со временем вы научитесь избегать этого.

Ошибки:
1) Писать тесты без утверждений только для того, чтобы было покрытие.
2) Писать сразу все тесты для списка, а затем заставлять их проходить по одному. Что будет, если проход 1го теста заставит вас пересмотреть решение, которое повлияет на остальные тесты? Переписывание всего, депрессия и/или скука. Выбор следующего теста — важный навык, который приходит только с опытом. Порядок тестов может существенно повлиять как на опыт программирования, так и на конечный результат.

3. Заставьте тест пройти
Измените систему так, чтобы тест прошёл успешно.
Ошибки:
1) Удалять утверждения, чтобы тест казался пройденным.
2) Копировать фактически вычисленные значения в ожидаемые значения теста. Это исключает двойную проверку, которая создает большую часть ценности TDD.
3) Смешивать рефакторинг с прохождением теста. Заставьте тест работать, а затем исправьте его. Ваш мозг (в конце концов) скажет вам спасибо.

Если в процессе вы обнаружите необходимость нового теста, добавьте его в список тестов. Если тест заставляет изменить решения и переписать предыдущие тесты, решите, продолжать или начать заново (совет: начните сначала, но выберите другой порядок реализации тестов). Когда тест пройден, вычеркните его из списка.

4. Рефакторинг
При необходимости.
Ошибки:
1) Переписывать больше, чем нужно.
2) Вводить абстракции слишком рано. Дублирование кода — это подсказка, а не приказ к действию.

5. Если список тестов не пуст, перейти к п. 2.
Продолжайте тестировать и писать код, пока ваш страх перед поведением кода не превратится в скуку.

Источник: https://tidyfirst.substack.com/p/canon-tdd
👍12👎5
Шпаргалка к предыдущему посту.
👍13
День 1847. #Testing
Компромиссы при Написании Тестов

Многие компании стремятся иметь 100% покрытия кода. Даже делают частью процесса CI/CD проверки, чтобы гарантировать, что покрытие тестами всегда увеличивается. Это имеет несколько последствий.

Запланированный эффект - разработчики пишут больше тестов. Незапланированный - иногда они пишут плохие тесты, либо просто костыли, чтобы «обмануть» проверку. Например, если вы провели рефакторинг хорошо протестированного кода, уменьшив его объём, покрытие кода уменьшится, но кодовая база улучшится.

Стремиться к 100% покрытию кода — плохая идея, но где провести черту?

Зачем мы пишем тесты?
В конечном счёте тесты обслуживают код, который мы пишем, и предназначены для решения проблемы. Если добавление теста не помогло решить проблему, это лишь трата времени и денег. Тесты снижают риск, позволяют проверить вашу работу и убедиться, что она скорее всего верна. Каждый тест даёт вам немного больше уверенности в тестируемом коде. Ценность тестового кода не прямая. Она в предотвращении потерь, как с точки зрения реального ущерба от ошибок (потеря дохода, нарушение конфиденциальности, неверные результаты), так и с точки зрения времени на обнаружение и исправление этих ошибок. Нам не нравится платить эту цену, поэтому вместо этого мы платим за тесты. Это страховка.

Какие риски вы хотите покрыть?
Как страховые полисы имеют разное покрытие, лимиты и доп. услуги за дополнительную цену, так с количеством тестов. Мы не можем позволить себе покрыть все риски. Нужно выбрать, сколько платить в виде «страховой премии» и сколько - в случае аварии. При 100% покрытии кода вы хотите избежать риска любой ошибки. А если тестов нет, значит даже серьёзные ошибки с максимальной стоимостью вас не беспокоят.

Как выбрать, какой риск мы хотим покрыть при тестировании? Часто это неявное решение: кто-то считает, что «больше покрытия кода, это хорошо», а затем люди начинают писать больше тестов, потому что «это наша культура, чувак»! Лучший способ – обдумать решение.

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

Нужно сравнить два числа:
1. Стоимость написания тестов
Сколько времени (в % от задачи) тратится на тестирование всеми членами команды. Не нужно измерять это для каждой задачи, сделайте выборку, чтобы получить примерное значение.
2. Стоимость ошибок
Это сложнее. Некоторые ошибки имеют явную цену, например отток клиентов. Но цена многих скрыта, например, подрыв доверия или иной вред. Измерьте время, которое команда тратит на выявление и исправление ошибок - это одна из основных трат. Бизнес затраты придётся оценить вместе с руководством. Идея в том, чтобы оценить общие затраты как можно более точно.

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

Ещё один сигнал о том, что вы идёте на неправильный компромисс, — это если вы не можете оценить стоимость ошибок. Т.е. вы либо тратите слишком много времени на выявление и предотвращение ошибок, и следует уделить больше времени созданию или улучшению функций. Либо вы не получаете всех отчётов о возникающих ошибках, что плохо по другим причинам.

Как вы решаете это в своей команде? У вас есть цель обеспечить покрытие кода?

Источник: https://ntietz.com/blog/too-much-of-a-good-thing-the-cost-of-excess-testing/
👍2
День 2043. #Testing
Автоматизированные Тесты. Начало
Т
есты важны для обеспечения качества приложения. Существует множество видов тестов: модульные, интеграционные, функциональные, дымовые и т.п. Но некоторые тесты не вписываются ни в одну категорию, и создатели придают им другое значение, создавая их.

Тесты — это компромиссы:
- Уверенность: Гарантирует ли тест, что приложение работает так, как ожидалось?
- Продолжительность: Сколько времени требуется для выполнения теста?
- Надёжность: Выдаёт ли тест случайные сбои?
- Усилия по написанию: Легко ли реализовать тестовый сценарий?
- Стоимость обслуживания: Сколько времени нужно на обновление теста при изменении кода?

Мой способ написания тестов значительно изменился со временем. Теперь мои тесты стали намного эффективнее, а их количество существенно сократилось. Изначально я писал тесты почти для каждого публичного метода для всех классов сборки. Я имитировал все внешние зависимости, гарантируя, что каждый тест проверяет только поведение внутри тестируемого метода. Иногда я тестировал внутренние классы, чтобы обеспечить всестороннее тестирование. Это приводило к многочисленным тестам, в которых каждый блок функциональности подвергался тщательному тестированию.

Теперь я принял новую стратегию, которая оказалась очень эффективной для разработанного мной приложения. Мои текущие практики включают:
1. Тестирование в основном публичного API, поскольку внутренние компоненты неявно тестируются и включаются в метрики покрытия кода. Публичный API – не обязательно классы или методы, а может быть точками входа приложения или конечными точками HTTP.
2. Минимизация использования моков, резервирование их для особых случаев, таких как взаимодействие клиентов с внешними сервисами.
3. Использование строгого статического анализа, чтобы избежать написания тестов для того, что может быть обнаружено во время компиляции.
4. Больше утверждений в коде (например, Debug.Assert).

Это даёт несколько преимуществ:
1. Меньше тестов нужно поддерживать.
2. Пользовательские сценарии чётко определены и протестированы.
3. Не нужно переписывать тесты при рефакторинге кода или изменениях в реализации, пока API не изменился.
4. Возможно безопасно рефакторить код, обеспечивая неизменное поведение.
5. Помогает выявлять устаревшие сегменты кода.

Тестирование публичного API
Публичный API — это то, как ваше приложение отображается для пользователей. Публичный API может отличаться в зависимости от типа проекта:
- Библиотека — классы и методы, доступные пользователям библиотеки.
- Веб-API — конечные точки HTTP.
- Приложение CLI — аргументы командной строки.

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

Например, если вы пишете приложение ASP.NET Core, не тестируйте класс Controller напрямую. Используйте WebApplicationFactory для настройки и запуска сервера, затем отправляйте запрос и проверяйте ответ. Так вы тестируете приложение в целом, включая маршрутизацию, привязку модели, фильтры, промежуточное ПО, внедрение зависимостей и т. д. Также возможен рефакторинг на Minimal API или изменение промежуточного ПО без обновления тестов.

Это не значит, что не следует тестировать детали реализации. Но это должно быть ограничено несколькими случаями, когда нужно проверить определённое поведение или когда есть сложный метод, который трудно тестировать.

Тестирование общедоступного API не предполагает тестирование методом черного ящика. Можно заменить некоторые зависимости. Например, проверить, как ведёт себя приложение, когда сторонний API возвращает ошибку.

Продолжение следует…

Источник:
https://www.meziantou.net/automated-tests.htm
Автор оригинала: Gérald Barré
👍14👎1
День 2044. #Testing
Автоматизированные Тесты. Продолжение

Начало

Подразумевает ли термин «модульное тестирование» тестирование каждой единицы реализации? Не совсем. Кент Бек, автор книги «Экстремальное программирование: разработка через тестирование», признал, что название «модульный тест», возможно, не лучший выбор. В этом контексте «модуль» относится к определённому поведению, которое может включать взаимодействие нескольких модулей реализации. Некоторые даже определяют «модуль» как целый пакет или библиотеку, и эта точка зрения также верна. Некоторые используют термины «изолированные» и «коммуникабельные» тесты, чтобы различать тесты, использующие и не использующие моки зависимостей.

Пишите код помогающий писать тесты
Написание теста должно быть простым, а тестовый код — понятным. Важно создать тестовую инфраструктуру, которая упрощает процесс написания тестов. Часто используют класс TestContext, содержащий общий код для написания тестов. Например, он может содержать код запуска приложения, отправки запроса, подтверждения ответа, регистрации мока и т. д. Так вы сможете сосредоточиться на написании теста, а не на шаблонном коде:
// Arrange
using var ctx = new TestContext();
ctx.ReplaceService<IService, MockImplementation>();
ctx.SetConfigurationFile("""{ "path: "/tmp" }""");
ctx.SeedDatabase(db =>
db.Users.Add(new User { Username = "user" }));
var user = ctx.SetCurrentUser("user");

// Act
var response = await ctx.Get("/user");

// Assert
Assert.AreEqual(response, """
Id: 1
UserName: user
""");


Нужно ли имитировать зависимости?
Моки (Moq) — способ протестировать приложение без использования реальных зависимостей. Поэтому, используя моки, вы тестируете не реальное приложение, а его разновидность. Важно максимально приблизить эту разновидность к реальности. Может быть сложно поддерживать тесты с моками. Сервисы, которые вы имитируете, могут изменить поведение, и вы можете не заметить этого, поскольку не используете их напрямую. Также приходится писать много кода для настройки тестов.

Разработчики, как правило, используют слишком много моков. Нужно ли имитировать файловую систему? Это не так-то просто, и у многих неправильные предположения о ней. Например, в Windows она чувствительна к регистру, некоторые имена файлов или символы в них недопустимы. В случае кроссплатформенного приложения может потребоваться протестировать поведение, специфичное для ОС. Почему бы просто не писать во временный каталог? То же самое касается БД. Можно использовать Docker для запуска БД, чтобы протестировать SQL и ограничения базы?

В большинстве случаев нужно имитировать внешние сервисы, которые не находятся под вашим контролем. Рассмотрите возможность использования фактических сервисов, когда это возможно. Конечно, желательно, чтобы тесты были изолированы. Здесь есть много стратегий. Например, если нужно читать или записывать файлы, можно создать временный каталог для каждого теста. Если вы запускаете БД в контейнере, можно использовать разные имена баз для изоляции тестов. Можно положиться на эмуляторы, предоставляемые поставщиками облачных услуг, для локального запуска некоторых зависимостей. Так вы будете уверены, что код работает, как ожидалось, и не нужно писать сложные моки. Убедитесь, что приложение можно настроить, например, легко подставлять разные строки соединений или папки для хранения данных приложения.

Полезные инструменты для избежания моков
- TestContainers или .NET Aspire. Если запуск Docker-контейнера медленный, можно переиспользовать его между несколькими тестовыми запусками. .NET Aspire также может предоставлять ресурсы в облаке (Azure, AWS и т. д.).
- Эмуляторы облачных сервисов. Например, Azure предоставляет эмулятор для хранилища.

Продолжение следует…

Источник:
https://www.meziantou.net/automated-tests.htm
👍15👎1
День 2045. #Testing
Автоматизированные Тесты. Продолжение

Начало
Продолжение

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

Я стараюсь избегать использования фреймворков-имитаторов, а создавать тестовые двойники вручную. Тестовые двойники могут более точно имитировать реальные сценарии, чем фреймворки-имитаторы. Они могут сохранять состояние между несколькими вызовами методов, что может быть сложно с фреймворками-имитаторами. Может потребоваться убедиться, что мок вызывается в правильном порядке (например, Save перед Get – хотя это можно сделать), или что вызывается правильное количество раз.

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

Это может показаться более трудоёмким, но эти усилия не существенны. Обычно вы не имеете дело с чрезмерным количеством фиктивных классов, и можете повторно использовать обобщённую реализацию в различных тестах. Так вы избегаете дублирования блоков кода для настройки в каждом тесте.
// Используем фреймворк-имитатор FakeItEasy
// Что, если сервис вызовет GetAsync до SaveAsync?
var service = A.Fake<IService>();
A.CallTo(() => service.SaveAsync()).Returns(true);
A.CallTo(() => service.GetAsync()).Returns(["value"]);

var sut = new MyClass(service);
sut.DoSomething();

Сравните с:
// Используем собственный двойник 
var service = new StubService();
service.AddItem("value"); // Добавляем данные

var sut = new MyClass(service);
sut.DoSomething();


Нужно ли добавлять интерфейсы, чтобы иметь возможность имитировать?
Интерфейсы с единственной реализацией бесполезны. Они просто добавляют сложности коду. По возможности не усложняйте код только для того, чтобы иметь возможность использовать моки. Если в коде будет единственная реализация, то вам может не понадобиться интерфейс. Кроме того, можно создать мок самого класса, а не интерфейса.

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

Окончание следует…

Источник:
https://www.meziantou.net/automated-tests.htm
Автор оригинала: Gérald Barré
👍7👎1
День 2046. #Testing
Автоматизированные Тесты. Окончание

Начало
Продолжение 1
Продолжение 2

Пишите больше утверждений в коде
Хотя утверждения (Assert) в тестах являются наиболее распространённым способом проверки поведения кода, можно писать утверждения и в коде. Например, использовать Debug.Assert для проверки состояния приложения. Это может быть полезно для проверки некоторых предположений, которые у вас есть относительно кода. Одним из преимуществ является то, что ошибка будет возникать на ранней стадии и также при отладке приложения, а не только при запуске тестов.

Утверждения также могут улучшить читаемость кода. Вы можете увидеть ожидаемое поведение непосредственно в коде, что может быть полезно при написании сложного кода или когда вы хотите проверить определённое поведение:
public void DoSomething()
{
Debug.Assert(_service != null,
"Этого быть не должно!");
_service.DoSomething();
}


Тестовые фреймворки
Используйте тот, который вам больше нравится: xUnit, NUnit и MSTests очень похожи по функциям. Синтаксис может отличаться, но концепции одинаковы. Для утверждений можно использовать встроенные или библиотеку вроде FluentAssertions.

Покрытие
Не стоит стремиться к 100% покрытию тестами. Вот некоторые проблемы с этой метрикой:
- 100% покрытие означает, что ошибок нет? Нет, это просто означает, что весь код покрыт тестами. И чаще всего только ваш код, но не зависимости.
- Сколько времени вы потратите на написание тестов, чтобы покрыть последние 10% кода? Стоит ли оно того?
- Нужно ли покрывать все пути в коде? Например, если есть метод, который выдаёт ArgumentNullException, нужно ли его тестировать? Добавляет ли это больше уверенности в коде или это просто пустая трата времени?

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

Статический анализ
Инструменты статического анализа могут помочь найти проблемы в коде перед запуском тестов, например, нулевые ссылки, неиспользуемые переменные и т. д. Инструменты могут помочь обнаружить проблемы во время компиляции, что может сократить количество тестов, которые нужно написать.

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

Есть несколько способов справиться с нестабильными тестами:
- Исправить тест - лучшее решение.
- Поместить тест в карантин и создать задачу для его исправления позже.
- Удалить тест, если он бесполезен.

Некоторые инструменты CI могут обнаружить нестабильность теста. Например, Azure DevOps может это делать.

См. также:
-
Мутационное тестирование с помощью Stryker
-
Пишите Тесты Быстрее с Approval Tests

Источник: https://www.meziantou.net/automated-tests.htm
👍5
День 2051. #Testing
Введение в WebApplicationFactory. Начало

Сегодня рассмотрим, что такое WebApplicationFactory и как она помогает в тестировании.

Мотивация
Все мы знаем про юнит-тесты, интеграционные тесты и End-to-end тесты. Чёткого разграничения нет. Спросите 10 разработчиков, и они дадут вам 10 разных определений. Но несомненно, что есть код, если модули кода, и их как-то надо тестировать.

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

Преимущества
1. Если контракт поменяется (как в WebAPI), тест не пройдёт.
2. Вы тестируете всю цепочку и пишете тесты с точки зрения конечного пользователя (не полагаясь на технические детали).
3. Вы получаете хороший обзор ваших функций, таким образом создавая «живую документацию» своего кода.

WebApplicationFactory
Это упрощённый сервер в памяти, который запускает ваш WebAPI ASP.NET Core. Она применит все настройки приложения (включая application.json), DI и будет работать с реальной БД (если не настроено иначе).
Вот простейший минимальный API для примера:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/", (HelloRequest request) =>
$"Hello {request.Name}!");

app.Run();

И простая запись для запроса:
public record HelloRequest(string Name); 


Настроим тестовый проект (xUnit, nUnit, MSTest – какой хотите).
Добавим NuGet-пакет Microsoft.AspNetCore.Mvc.Testing и ссылку с тестового проекта на основной.

Теперь можно написать код настройки тестов. В xUnit используется конструктор:
public class MyApiTests :
IClassFixture<WebApplicationFactory<Program>>
{
public void MyApiTests(
WebApplicationFactory<Program> factory)
{
}
}

Здесь IClassFixture<WebApplicationFactory<Program>> используется, чтобы указать xUnit передать WebApplicationFactory<Program> в конструктор.

Замечания:
1. Называйте классы и методы тестов как модули и их функции, которые вы хотите протестировать. Это важно, если вы хотите иметь живую документацию внутри своего кода.
2. Этот код не скомпилируется. Причина в том, что класс Program является внутренним (internal). В минимальных API больше нет метода Main, компилятор создаёт его за вас. Но теперь там используется модификатор internal. Одним из возможных исправлений было бы добавление InternalsVisibleTo, но это влечёт за собой много исправлений. Тогда тестовые методы также должны быть внутренними. Но внутренние методы не могут быть выполнены тестовой средой. Есть более простой «хак»: с помощью partial класса сделать класс Program публичным. Добавьте в конец файла Program.cs:
public partial class Program
{
}

Не идеально для минимальных API, но просто.

Окончание следует…

Источник:
https://steven-giesel.com/blogPost/cd62475b-2c7d-4ce2-bd97-9670f91ebac8/introduction-to-webapplicationfactory
👍18
День 2052. #Testing
Введение в WebApplicationFactory. Окончание
Начало

Теперь добавим тест. Для простых сценариев можно использовать HttpClient.
public class MyApiTests :
IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient client;

public MyApiTests(
WebApplicationFactory<Program> factory)
{
client = factory.CreateClient();
}

[Fact]
public async Task
PassingNameShouldReturnWelcomeMessage()
{
var resp =
await client.PostAsJsonAsync("/", new
{
Name = "Jon Smith"
});

Assert.True(resp.IsSuccessStatusCode);
var content =
await resp.Content.ReadAsStringAsync();
Assert.Equal("Hello Jon Smith!", content);
}
}

Почему мы передали анонимный объект, а не HelloRequest (он ведь публичный)? Несколько причин:
1. Он публичный, но не чтобы использовать в тестах. Помните, мы являемся пользователем API. Пользователь не знает внутреннего представления наших доменных объектов.
2. Тесты не должны пострадать, если мы поменяем доменную модель.
3. Мы также тестируем сериализацию и десериализацию.
Поэтому анонимный объект идеален.

Конфигурация
Есть много настроек WebApplicationFactory, которые можно сделать в конструкторе. Рассмотрим основные:
public MyApiTests(
WebApplicationFactory<Program> factory)
{
this.factory = factory.WithWebHostBuilder(
builder =>
{
// Изменить настройки из application.json
builder.UseSetting(
"ConnectionString",
"file=:memory:");

// Если в appsettings.json объект:
// MyObject {
// MyProp: 123
// }
// Используем нотацию ":"
builder.UseSetting("MyObject:MyProp", 234);

// Изменить среду и загружать
// настройки из appsettings.tests.json
builder.UseEnvironment("tests");

// Перенастроить сервисы
builder.ConfigureServices(
services => …
);
});
}


Источник: https://steven-giesel.com/blogPost/cd62475b-2c7d-4ce2-bd97-9670f91ebac8/introduction-to-webapplicationfactory
👍16
День 2205. #Testing
Нагрузочное Тестирование с Помощью K6 в Windows. Начало

Понимание того, как ваша система реагирует на входящий сетевой трафик, имеет решающее значение для определения ее стабильности, способности соответствовать ожидаемому SLO и исправности базовой инфраструктуры и архитектуры. Рассмотрим, как использовать K6 для запуска нагрузочных тестов и локального отображения конечного результата.

Что это?
Нагрузочное тестирование имитирует реальные условия использования, чтобы убедиться, что ПО может обрабатывать большой трафик без ущерба для производительности или пользовательского опыта. Его важность заключается в способности выявлять узкие места в системе, которые могут привести к медленному времени отклика, ошибкам или сбоям в условиях нагрузки.

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

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

Демо проект
Создадим простой проект .NET API. Одна конечная точка, /randombook, которая возвращает информацию о случайной книге, хранящейся в БД в памяти:
int requests = 0;
int concurrency = 0;
object _lock = new();
app.MapGet("/randombook", async (CancellationToken ct) =>
{
Book? book = default;
var delayMs = Random.Shared.Next(10, 10000);
try
{
lock (_lock)
{
requests++;
concurrency++;
app.Logger.LogInformation(
@"Request {Count}.
Concurrent Executions {Executions}.
Delay: {DelayMs}ms",
requests, concurrency, delayMs);
}

using var ctx = new ApiContext();
await Task.Delay(delayMs, ct);
if (ct.IsCancellationRequested)
{
app.Logger.LogWarning("Cancelled");
throw new OperationCanceledException();
}
var books = await ctx.Books.ToArrayAsync();
book = Random.Shared
.GetItems(books, 1).First();
}
catch (Exception ex)
{
app.Logger.LogError(ex, "Error ");
return Results.Problem(ex.Message);
}
finally
{
lock (_lock)
{
concurrency--;
}
}

return TypedResults.Ok(book);
});

Здесь добавлены:
- случайная задержка delayMs, эмулирующая запрос из БД;
- потокобезопасный счётчик параллельных операций concurrency;
- логирование сообщений внутри lock для избежания проблем параллелизма.
Это, конечно, не идеальное решение, но оно подойдёт для демонстрации.

Далее посмотрим, как провести нагрузочное тестирование этого API.

Продолжение следует…

Источник:
https://www.code4it.dev/blog/k6-load-testing/
1👍18
День 2206. #ЗаметкиНаПолях #Testing
Нагрузочное Тестирование с Помощью K6 в Windows. Продолжение

Начало

Запуск K6 в Windows
С помощью K6 вы можете запустить нагрузочные тесты, определив конечную точку для вызова, количество запросов в минуту и некоторые другие настройки.

Этот бесплатный инструмент можно установить с помощью Winget:
winget install k6 --source winget

Проверить правильность установки можно через командную строку (не Powershell):
k6 --version

Теперь можно инициализировать инструмент:
k6 new

Команда сгенерирует файл script.js, в котором надо будет настроить конфигурацию тестов. Например:
import http from "k6/http"
import { sleep } from "k6"

export const options = {
vus: 10,
duration: "30s",
}

export default function () {
http.get("https://localhost:7123/randombook")
sleep(1)
}

Здесь:
- vus: 10 - виртуальные пользователи, симулирующие параллельные входящие запросы;
- duration: "30s" – общее время теста;
- http.get("https://…") - основная функция, вызывающая конечную точку и считающая ответы, метрики, тайминг и т.п.;
- sleep(1) – время паузы между итерациями.

То есть, в течение 30 секунд k6 будет посылать до 10 параллельных запросов, потом ждать 1 секунду, и повторять. После он истечения времени теста, он даст ещё 30 секунд приложению, чтобы завершить текущие запросы.

Для запуска убедитесь, что API запущен, и выполните следующую команду:
k6 run script.js


В консоли API мы увидим логи запросов:
[15:19:51 INF] Request 1. Concurrent Executions 1. Delay: 7124ms
[15:20:02 INF] Request 2. Concurrent Executions 1. Delay: 4981ms

[15:20:27 INF] Request 57. Concurrent Executions 10. Delay: 7655ms

А в консоли k6 отчёт вроде представленного на рисунке выше.

Окончание следует…

Источник:
https://www.code4it.dev/blog/k6-load-testing/
👍12
День 2207. #ЗаметкиНаПолях #Testing
Нагрузочное Тестирование с Помощью K6 в Windows. Окончание

Начало
Продолжение

Отчёт
Вернёмся к отчёту, показанному на картинке в предыдущем посте. Либо, установив 2 переменные окружения, вы можете получить более визуально приятный отчёт в виде HTML документа, показанного на рисунке выше.
set K6_WEB_DASHBOARD=true
set K6_WEB_DASHBOARD_EXPORT=html-report.html
k6 run script.js

В отчёте множество значений, названия которых в основном говорят сами за себя:
- data_received и data_sent - размер отправленных и полученных данных;
- продолжительность и ответы HTTP-запросов (http_req_duration, http_req_sending, http_reqs);
- информация о фазах HTTP-соединения, например http_req_tls_handshaking;
- конфигурации K6 (iterations, vus и vus_max).
Вы можете увидеть среднее значение, минимальное и максимальное значение, а также некоторые процентили для большинства значений.

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

HTTP-методы
Мы использовали только метод GET, но можно использовать все доступные HTTP-методы с помощью соответствующей функции Javascript:
- get() – метод GET,
- post() - метод POST,
- put() - метод PUT,
- del() - метод DELETE.

Стадии
Вы можете определить несколько стадий тестирования, например:
export const options = {
stages: [
{ duration: "30s", target: 20 },
{ duration: "1m30s", target: 10 },
{ duration: "20s", target: 0 },
],
}

Здесь определены 3 стадии:
1. 30 сек – нагрузка в 20 виртуальных пользователей,
2. 1м 30 сек – 10,
3. 20 сек – время на завершение оставшихся запросов.

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

Сценарий — это элемент JSON, в котором вы определяете аргументы, такие как продолжительность, количество пользователей (как мы рассмотрели выше), а также переменные среды, время старта и т.д. Определив сценарий, вы можете запустить тесты на той же конечной точке, но с использованием разных поведений. Например, создать один сценарий для постепенного роста пользователей, a другой - для резкого взлёта их количества.

Источник: https://www.code4it.dev/blog/k6-load-testing/
👍9
День 2282. #Testing
Тестирование Характеристик
Большинство типов тестирования проверяют правильность. Мы тестируем код, чтобы увидеть, что он делает то, что мы хотим, чтобы он делал. Это предполагает, что мы знаем, что он должен делать. А что, если мы этого не знаем?

К сожалению, это бывает часто. Нужно вносить изменения в код, но мы недостаточно знаем о том, что он делает. Вот небольшой пример:
public class Parser
{
public static string FormatText(string text)
{
var result = new StringBuilder();
for (int n = 0; n < text.Length; ++n)
{
int c = text[n];
if (c == '<')
{
while (n < text.Length && text[n] != '/' && text[n] != '>')
n++;
if (n < text.Length && text[n] == '/')
n += 4;
else
n++;
}
if (n < text.Length)
result.Append(text[n]);
}
return result.ToString();
}
}

Что делает этот код? Кажется, удаляет HTML-теги из текста, но логика странная и, скорее всего, неправильная. Этот крошечный пример показывает, как сложно читать плохо написанный код. Мы можем использовать тестирование. Но вместо того, чтобы пытаться выяснить, является ли код правильным, мы можем попытаться охарактеризовать его поведение, чтобы понять, что он на самом деле делает. Начнем с простого теста. Создадим тест и назовём его «x». «X», потому что мы не знаем, что будет делать метод FormatText. И мы даже не зададим ожидаемого значения, т.к. на данный момент мы не знаем, каким будет поведение:
[TestMethod]
public void x()
{
Assert.AreEqual(null, Parser.FormatText("text"));
}

Тест упадёт, но мы хотя бы узнали, что метод выдаст в виде результата. Теперь мы можем поставить результат вместо ожидаемого значения и заставить тест проходить. А также зададим тесту имя, которое отражает наше понимание того, что делает код:
[TestMethod]
public void DoesNotChangePlainText()
{
Assert.AreEqual("text", Parser.FormatText("text"));
}

Мы не разобрались в коде, а пишем тесты. Какую ценность они могут иметь? Большую. Когда мы пишем тесты характеристик, мы накапливаем знания о том, что на самом деле делает код. Это особенно полезно, когда мы хотим его отрефакторить. Мы можем запустить наши тесты и сразу узнать, изменили ли мы поведение. Вот другой тест. Он проходит:
[TestMethod]
public void RemovesTextBetweenAngleBrackets()
{
Assert.AreEqual("", Parser.FormatText("<>"));
}

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

У вас было, что вы исправили ошибку, а пользователи стали жаловаться? Они не думали, что это ошибка, они думали, что это особенность системы. Когда система переходит в эксплуатацию, она в некотором смысле становится своей собственной спецификацией. Нам нужно знать, когда мы меняем существующее поведение, независимо от того, считаем мы его правильным или нет.

Мы можем рассматривать тесты характеристик как описания того, что у нас есть, а не как утверждения о правильности. Можно периодически пересматривать тесты, чтобы ужесточить их условия, когда мы решаем, каким должно быть поведение. Самое сложное — разорвать зависимости вокруг фрагмента кода, чтобы иметь возможность проверить его в тестовой среде. Как только вы это сделаете, останется только поинтересоваться, что будет делать код в определённых условиях и запустить тест, чтобы найти фактическое значение. Часто вы будете пересматривать названия тестов, поскольку будете больше понимать код, который проверяете. Начните с теста с именем «x».

Источник: https://michaelfeathers.silvrback.com/characterization-testing
👍13
День 2361. #Testing #BestPractices
Лучшие Практики Интеграционного Тестирования с Testcontainers. Начало

Интеграционные тесты с Testcontainers — мощный инструмент, но их поддержка может быстро превратиться в кошмар. Сегодня рассмотрим шаблоны, которые делают тесты Testcontainers надёжными, быстрыми и простыми в поддержке.

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

Testcontainers решает эту проблему, разворачивая настоящие Docker-контейнеры для ваших зависимостей. Тесты выполняются с использованием реальных PostgreSQL, Redis или любого другого сервиса, используемого в рабочей среде. После завершения тестов контейнеры уничтожаются, каждый раз позволяя вам начинать с чистого листа.

Всё происходит через API Docker. Testcontainers управляет всем жизненным циклом: извлечение образов, запуск контейнеров, ожидание готовности и очистка. Тестовому коду нужно лишь знать, как подключиться.

1. Подготовка
Убедитесь, что у вас есть необходимые пакеты:
Install-Package Microsoft.AspNetCore.Mvc.Testing
Install-Package Testcontainers.PostgreSql
Install-Package Testcontainers.Redis

Пакеты TestContainers существуют для множества сервисов.

2. Создание
Вот так можно создать контейнеры для PostgreSql и Redis:
var _pg = new PostgreSqlBuilder()
.WithImage("postgres:17")
.WithDatabase("mydb")
.WithUsername("postgres")
.WithPassword("postgres")
.Build();

var _redis = new RedisBuilder()
.WithImage("redis:latest")
.Build();


3. Использование
Чтобы запускать и останавливать контейнеры в тестах, нужно реализовать IAsyncLifetime в вашей WebApplicationFactory:
public sealed class IntegrationWebAppFactory :
WebApplicationFactory<Program>, IAsyncLifetime
{
public async Task InitializeAsync()
{
await _pg.StartAsync();
await _redis.StartAsync();
// Старт других зависимостей
}

public async Task DisposeAsync()
{
await _pg.StopAsync();
await _redis.StopAsync();
// Остановка других зависимостей
}
}

Это гарантирует готовность контейнеров до запуска тестов и их очистку после них. Т.е. отсутствие остаточного состояния Docker или состояний гонки.
Совет: закрепите версии образов (например, postgres:17), чтобы избежать сюрпризов от изменений версий зависимостей.

4. Передача конфигурации в приложение
Testcontainers назначает динамические порты. Не пишите жёсткие строки подключения в коде. Вместо этого внедряйте значения через WebApplicationFactory.ConfigureWebHost:
protected override void 
ConfigureWebHost(IWebHostBuilder bldr)
{
bldr.UseSetting("ConnectionStrings:Database",
_pg.GetConnectionString());
bldr.UseSetting("ConnectionStrings:Redis",
_redis.GetConnectionString());
}

Здесь используется метод UseSetting для динамической передачи строк подключения. Это также позволяет избежать состояний гонки или конфликтов с другими тестами, которые могут выполняться параллельно. Это гарантирует, что тесты всегда будут подключаться к правильным портам, независимо от того, что назначает Docker.

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

Окончание следует…

Источник:
https://www.milanjovanovic.tech/blog/testcontainers-best-practices-dotnet-integration-testing
👍11
День 2362. #Testing #BestPractices
Лучшие Практики Интеграционного Тестирования с Testcontainers. Окончание

Начало

5. Совместное использование настроек с фикстурами xUnit
Фикстура — это общий контекст для тестов, позволяющий настроить дорогостоящие ресурсы, такие как БД или брокеры сообщений, один раз и использовать их повторно в нескольких тестах. Выбор между фикстурами классов и коллекций влияет как на производительность тестов, так и на изоляцию.

Фикстура класса — один контейнер на каждый тестовый класс.
Используйте, когда тесты изменяют глобальное состояние или когда отладка тестовых взаимодействий становится затруднительной. Применяйте, когда требуется полная изоляция между тестовыми классами (это медленнее, но безопаснее).
public class AddItemToCartTests : 
IClassFixture<IntegrationWebAppFactory>
{
private IntegrationWebAppFactory _factory;

public AddItemToCartTests(
IntegrationWebAppFactory factory)
{
_factory = factory;
}

[Fact]
public async Task ShouldFail_WhenNotEnoughQuantity()
{ … }
}


Фикстура коллекции — один контейнер, общий для нескольких тестовых классов.
Используйте, когда тесты не изменяют общее состояние или когда вы можете надёжно выполнить очистку между тестами. Т.е. когда тестовые классы не мешают друг другу (это быстрее, но требует дисциплины).
[CollectionDefinition(nameof(IntegrationCollection))]
public sealed class IntegrationCollection :
ICollectionFixture<IntegrationWebAppFactory>
{
}

// Применение
[Collection(nameof(IntegrationCollection))]
public class AddItemToCartTests :
IntegrationTestFixture
{
public AddItemToCartTests(
IntegrationWebAppFactory factory)
: base(factory) { }

[Fact]
public async Task Should_Fail_WhenNotEnoughQuantity()
{ … }
}

При использовании фикстур коллекций необходимо позаботиться об очистке любого состояния, которое может сохраняться между тестами. Это может включать сброс БД, очистку кэшей или удаление тестовых данных. Если этого не сделать, тесты могут влиять друг на друга, что приведёт к нестабильным результатам.

6. Вспомогательные методы для аутентификации и очистки
Фикстура может предоставлять вспомогательные методы для упрощения написания тестов:
public async Task<HttpClient> 
CreateAuthenticatedClientAsync() { … }

protected async Task CleanupDBAsync() { … }

Эти методы могут выполнять настройку аутентификации и очистку БД, чтобы не повторять шаблонный код в каждом тесте.

7. Написание поддерживаемых интеграционных тестов
При правильной настройке инфраструктуры ваши тесты должны быть сосредоточены на бизнес-логике. Сложность контейнеров должна быть скрыта за грамотно спроектированными базовыми классами и вспомогательными методами. В тестах вы не должны заботиться о правильной имитаций Postgres или Redis, а должны тестировать реальное поведение.

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

Начните с простого: выберите один интеграционный тест, который в настоящее время использует моки или БД в памяти, и преобразуйте его для использования Testcontainers. Вы сразу заметите разницу в уверенности, когда тест пройдёт успешно. Затем постепенно расширяйте его, чтобы охватить критически важные бизнес-процессы.

Источник: https://www.milanjovanovic.tech/blog/testcontainers-best-practices-dotnet-integration-testing
👍3