Обновить
9
Андрей Федотов@KoscheyScrag

Разработчик ПО

1
Подписчики
Отправить сообщение

я не очень понимаю над чем вы работаете, но пишете вы отлично!

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

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

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

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

возможно, скажу ужасную вещь, но у нас всё просто: мы ничего не мокаем из перечисленного =)

разве что только Redis у нас локальный

да, используем FluentAssertion - довольно удобная штука

Конечно, тестирование поведения - это поначалу не также просто, как взять просто класс и протестировать его. Классов может быть как много, так и вовсе только один метод из одного класса. Всё зависит от самого поведения, которое мы тестируем. И здесь может быть много факторов: предметная область, проект и договоренности на нем, разработчики тоже, как вы правильно упомянули.

Вот что пишет В. Хориков: "В идеале тест должен рассказывать о проблеме, решаемой кодом проекта, и этот рассказ должен быть связным и понятным даже для непрограммиста."

И вот такой пример связного рассказа приводит:
Когда я зову свою собаку, она идет ко мне.


И другой пример того же самого:

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

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

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

Вообще, весь код можно разделить на 4 категории: тривиальный, переусложненный, контроллеры и бизнес логика. Точно стоит тестировать бизнес логику и контроллеры. Тривиальный менее важен для тестирования. Переусложненный в идеале должен быть отрефакторен и перейти в категорию бизнес логики или контроллеров.
Соответственно, для бизнес логики больше подойдут юниты, а для контроллеров интеграционные/сквозные (конечно, тут зависит он контекста каждого отдельного случая еще)

Понятно, спасибо за ответ.

Ну, кстати, кажется, что в последние годы они очень много вкладываются в развитие экосистемы .NET и самого C#.

Если Вы имеете в виду что в нескольких методах будут одинаковые большие блоки 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);
    }
}

Спасибо большое!
А если не секрет, то почему C# не очень нравится?)

да, можно использовать Refit, это очень удобно для создания API клиентов.
на проекте планируем использовать этот способ в будущем. пока что типизированные клиенты делали

да, Refit отличная библиотека. как раз удобно для создания API клиентов для сервисов использовать:
https://learn.microsoft.com/ru-ru/aspnet/core/fundamentals/http-requests?view=aspnetcore-8.0#generated-clients

RestSharp как минималистичная альтернатива использованию HttpClient'а (но под капотом используется HttpClient все равно)

конструкция using это же синтаксический сахар, так что в любой реализации (до сегодняшнего дня) код типа using(var client = new HttpClient()) не "нормален" для HttpClient'а =)

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

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

что касается использования статического экземпляра то субъективно предпочтительнее использовать IHttpClientFactory, но тем не менее это рабочий вариант, который в том числе рекомендует Майкрософт. Только надо помнить про DNS TTL и добавить в конструкторе SocketsHttpHandler с настройкой PooledConnectionLifetime.

возможно, пропустили ввиду их отсутствия)) https://habr.com/ru/news/t/468585/

Как же вдохновляют подобные истории! Я будто сам всё вышепрочитанное пережил! Спасибо=)

Информация

В рейтинге
Не участвует
Зарегистрирован
Активность