Комментарии 20
Большое спасибо за статью. Не смотря на то, что я не большой поклонник С#, но примеры достаточно универсальны.
Спасибо большое!
А если не секрет, то почему C# не очень нравится?)
MS масдай и C# порок его.
С тех давних времен, когда майки тормозили Джаву под Винду, чтобы продвинуть Си шарп. Потом ситуация стала получше, но от шарпа по прежнему сильный запах менеджмента майков. Т.е. не заметишь, как погрузишься в экосистему и будешь использовать "патентованные решения". Причем не только майки в этой экосистеме так страдают, но и, например, Юнити.
Спасибо за статью! Вопрос у меня к Вам. Что вы делаете с толстыми и повторяющимися блоками Assert и Arrange внутри тестовых методов?
Показанный в данной статье пример - идеальный. Мне интересно было бы узнать как Вы обычные, "бытовые" тесты пишете.
Если Вы имеете в виду что в нескольких методах будут одинаковые большие блоки Assert/Arrange, то я считаю, что отталкиваться нужно от поведения, которое Вы тестируете.
Для интеграционного теста вполне обыденно тестировать несколько единиц поведения за раз.
Если же в одном тесте это будет не уместно, то не вижу ничего страшного, если в разных методах блоки будут повторяться (по необходимости можно такую логику вынести отдельно - в хелперы или в некий test init).
Если же речь идет про повторяющиеся блоки в рамках одного теста, то такого быть не должно.
Вот пример одного из наших "бытовых" тестов (всё переименовано на всякий случай):
[Fact]
public async Task Subscription_SomeScenario_ReceivesDataFromOneAndThenOnlyFromAnother()
{
// Arrange
const int valuesCount = 10;
const int oneValuesCount = valuesCount / 2;
const int anotherValuesCount = valuesCount - valuesCount;
var (id, name) = await _apiTestHelper.CreateSomethingAsync();
var firstValueTs = DateTimeOffset.UtcNow.AddHours(-1);
var firstValue = Random.Shared.NextDouble();
var expectedValues = new List<(DateTimeOffset Ts, double Value)> { (firstValueTs, firstValue) };
await _apiClient.WriteValueAsync(id, firstValueTs, firstValue);
var linkedId = await _apiTestHelper.CreateLinkedSomethingAsync(id);
var subscriberId = Guid.NewGuid();
await _anotherApiClient.PrepareAsync(subscriberId, [linkedId]);
var receivedValues = new List<Info>();
var authToken = await _securityService.GetAccessToken();
var testServerWebSocketClient = _webAppFixture.Server.CreateWebSocketClient();
testServerWebSocketClient.ConfigureRequest = request =>
{
request.Headers.Authorization = $"{authToken!.TokenType} {authToken.AccessToken}";
};
// Act
var firstValueReceivedTcs = new TaskCompletionSource();
var expectedAmountOfValuesFromOneIsReceivedTcs = new TaskCompletionSource();
var expectedAmountOfValuesFromAnotherIsReceivedTcs = new TaskCompletionSource();
using var websocketClient = await _anotherApiClient.SubscribeAsync(
subscriberId: subscriberId,
onReceived: info =>
{
receivedValues.Add(info);
firstValueReceivedTcs.TrySetResult();
if (receivedValues.Count == valuesCount)
expectedAmountOfValuesFromOneIsReceivedTcs.TrySetResult();
if (receivedValues.Count == valuesCount)
expectedAmountOfValuesFromAnotherIsReceivedTcs.TrySetResult();
},
connectionFactory: (uri, token) => testServerWebSocketClient.ConnectAsync(uri, token));
await Task.WhenAny(firstValueReceivedTcs.Task, DelayHelper.WaitForSomethingAsync());
for (var i = 1; i < valuesCount; i++)
{
var valueTs = DateTimeOffset.UtcNow.AddMilliseconds(new Random().NextInt64(-100, 100));
var value = Random.Shared.NextDouble();
expectedValues.Add((valueTs, value));
await _apiClient.WriteValueAsync(id, valueTs, value);
}
await Task.WhenAny(expectedAmountOfValuesFromOneIsReceivedTcs.Task, DelayHelper.WaitForSomethingAgainAsync());
await _apiTestHelper.UpdateSomethingAsync(id, Random.Shared.NextDouble());
var something = await _apiTestHelper.CreateSomethingAsync();
await DelayHelper.WaitForSomethingChangeEventAsync();
for (var i = 0; i < constValuesCount; i++)
{
var value = Random.Shared.NextDouble();
expectedValues.Add((DateTimeOffset.UtcNow, value));
await _apiTestHelper.UpdateToSomethingAsync(id, value);
await _apiClient.WriteValueAsync(
something, DateTimeOffset.UtcNow, Random.Shared.NextDouble());
await DelayHelper.WaitForSomethingChangeEventAsync();
}
await Task.WhenAny(expectedAmountOfValuesFromAnotherIsReceivedTcs.Task, DelayHelper.WaitForSomethingAgainAsync());
// Assert
receivedValues.Count.Should().Be(expectedValues.Count);
expectedValues = expectedValues.OrderBy(tuple => tuple.Ts).ToList();
for (var i = 0; i < receivedValues.Count; i++)
{
receivedValues[i].Value.Should().Be(expectedValues[i].Value);
receivedValues[i].ValueStatusId.Should()
.Be(i == 0 ? ValueStatus.SomeStatus : ValueStatus.AnotherStatus);
}
}Правильно ли я понимаю, что случае с попаданием в замкнутый цикл "стресса и тестов" - нужно переходить от сквозных в сторону юнит тестов? И тогда, проработав код отдельными кусочками появится возможность наконец увидеть положительный результат и в сквозных показателях?
Ну, если положительного результата совсем не видно, то возможно код действительно стоит проработать отдельными кусочками. Может уделить время рефакторингу и понять какой код тестировать, а какой нет.
Вообще, весь код можно разделить на 4 категории: тривиальный, переусложненный, контроллеры и бизнес логика. Точно стоит тестировать бизнес логику и контроллеры. Тривиальный менее важен для тестирования. Переусложненный в идеале должен быть отрефакторен и перейти в категорию бизнес логики или контроллеров.
Соответственно, для бизнес логики больше подойдут юниты, а для контроллеров интеграционные/сквозные (конечно, тут зависит он контекста каждого отдельного случая еще)
Хоть мне не нравится лондонская школа в её категоричном чистом виде, но вот у этого правила классической школы тестирования есть один огромный минус:
Тесты должны проверять единицы поведения, а не единицы кода
Минус этот заключается в отсутствии определенности, что же именно считать единицей поведения. У каждого разработчика зачастую может быть какое-то свое видение, что в том или ином случае считать "атомарной единицей поведения". Возникают случаи, когда прилетает PR, где в таком "юнит-тесте" принимает участие с десяток классов, с последующими времязатратными дискуссиями и доказыванием своей точки зрения.
Одновременно это же и огромный плюс лондонской школы - определенность. Все в команде четко знают, что юнит это класс, и не возникает никаких разночтений и споров по этому поводу. Хочешь проверить взаимодействие классов между собой - напиши 1-2 интеграционных теста, тестирующих именно взаимодействие, а детальное тестирование поведения уже в юнит тестах.
А что такое эти самые "единицы поведения" и как их считать - это самое интересное!
Конечно, тестирование поведения - это поначалу не также просто, как взять просто класс и протестировать его. Классов может быть как много, так и вовсе только один метод из одного класса. Всё зависит от самого поведения, которое мы тестируем. И здесь может быть много факторов: предметная область, проект и договоренности на нем, разработчики тоже, как вы правильно упомянули.
Вот что пишет В. Хориков: "В идеале тест должен рассказывать о проблеме, решаемой кодом проекта, и этот рассказ должен быть связным и понятным даже для непрограммиста."
И вот такой пример связного рассказа приводит:Когда я зову свою собаку, она идет ко мне.
И другой пример того же самого:
Когда я зову свою собаку, она сначала выставляет вперед левую переднюю лапу, потом правую переднюю лапу, поворачивает голову, начинает вилять хвостом...
Этим утрированным примером он пытается донести мысль, что не нужно завязываться на детали внутренней реализации (как при тестировании отдельных классов), а думать стоит о наблюдаемом поведении.
Тестирование внутренней реализации это не одно и то же, что тестирование отдельных классов. Тестировать внутреннюю реализацию это значит, проверять, какие методы вызывает внутри себя класс, в какой последовательности, какие поля меняет и т.п. Но даже когда мы тестируем отдельный класс, нам не обязательно так делать, мы можем проверять сам результат его работы.
А проецируя пример с собакой на подходы лондонской и классической школ - лондонская школа гласит, что если мы тестируем собаку, мы тестируем только ее, в идеальных окружающих условиях. Мы не тестируем в этом же тесте хозяина, который стоит рядом, мы не тестируем в этом же тесте дорогу, на которой стоит собака - мы тестируем только собаку. То есть, мы "мокаем" все, что вокруг. И если тест сломается, мы будем знать, что проблема только в самой собаке. С классической школой мы бы вполне могли тестировать собаку со всем, что ее огружает, если это окружение не использует еще одна собака, и тест мог бы сломаться по множеству разных причин - испортилось настроение у хозяина, дорога стала скользкой, пошел дождь и т.п.
Андрей, я бегло прочел статью и не понял. У вас интеграционный сервис, вас попросили взять данные, вы посмотрели в кеш метаданных, проверили права в объектной модели и пошли в сервис временных рядов за данными (все это - не секрет).
А что из этого у вас реальное, что вы мокаете, и как решаете, что мокать, а что запускать реально?
возможно, скажу ужасную вещь, но у нас всё просто: мы ничего не мокаем из перечисленного =)
разве что только Redis у нас локальный
Ага. А почему именно так? Вы для себя это рефлексировали?
ну, мы изначально не планировали мокать ничего...
1. использовать у нас реальные зависимости не сложно, в том плане что это не сложнее, чем создавать заглушку (а может даже легче)
2. это условно бесплатно (не считая ресурсов стендов), у нас нет каких-то зависимостей, которые бы кушали денежку за их использование
конечно тут есть и минусы. например, нужно следить за версиями сервисов, развернутых на стенде. но с другой стороны мы всё время проверяем реальное поведение
вообще, у нас есть кейсы, где было бы логичнее использовать юниты с заглушками, нежели end-to-end без заглушек. но мы решили не "размазывать" тесты так и оставить юниты для проверки логики (которой немного), а end-to-end для проверки поведений.
это конечно немного дольше, чем запускать юниты, но не так, чтоб сильно)
Код, который работает: Unit и интеграционное тестирование для повышения надежности ПО