@totsamiynixon

Как написать правильные Unit/Integration тесты?

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

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

Итак у меня есть некая система, которая рассчитывает зарплату сотрудников.

Пример кода:
public class SalaryCalculationService {
    private readonly Calculator_calculator;

    public SalaryCalculationService(Calculator calculator) {
        _calculator = calculator;
    }

     public int CalculateSalary(Employee employee) {
       //logic that uses calculator goes here
     }
}

Одной из ее зависимостей является класс Calculator.
Пример теста SalaryCalculationService:
public class SalaryCalculationServiceTest {

  [TestMethod]
  public void ShouldCalculateSalaryCorrectly() {
    //Arrange
    var calculatorMock = new Mock<Calculator>().Sum().Returns((a, b) => Math.Sum(a, b)); //Мы абстрагируемся от конкретной реализации ICalculator
    var calculationService = new SalaryCalculationService(calculatorMock.Object);
    var employee = new Employee();
    employee.SetWorkingDays(10);

    //Act
    var result = calculationService.CalculateSalary(employee);

    //Assert
    Assert.Equals(40, result);
  }
}


Пример кода Calculator:
public class Calculator:  ICalculator {
    public int Sum(int a, int b) {
        return a + b;
    }
}


Пример кода CalculatorTest:
public class CalculatorTest {
 
    public void ShouldCalculateSumCorrectly() {
        //Arrange
        var calculator = new Calculator();
     
       //Act
       var result = calculator.Sum(2, 2);

      //Assert
      Assert.Equals(4, result);
    }
}


И вот появляется требование, что калькулятор теперь может считать только только нечетных чисел.
Мой коллега работает по TDD, создает тест на это поведение, реализует это поведение, запускает все тесты - зеленые, значит, можно чекинить.

Очевидно, что система в комплексе работать не будет, потому что разные части системы, которые использовали Calculator не ожидают, что теперь он работает таким странным образом. Так и оказалось. Наши тестеры быстро нашли этот баг.

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

Но вот загвоздка, нам все еще летят баги, причем на те модули, которы покрыты тестами. Мы разбираемся в ситуации и понимаем, что один из наших сервисов, который использует базу данных, отпарвляет SQL запрос с синкаксической ошибкой. Ну вот опять двадцать пять. Все пишем по TDD, все тесты зеленые, а баги все равно появляются.

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

Я прочитал сотню статей по unit/integration тестам и так и не нашел ответ на вопрос: как правильно организовать Unit/Integration тестирование, чтобы создынные тесты отлавливали баги связанные как минимум с рефакторингом, чтобы позволяли рефакторить код без постояных изменений в тестах (меняем завимость = переписываем все моки для нее по всему приложению) и тд?

Расскажите, пожалуйста, о вашем опыте, очень важно узнать разные мнения на эту проблему!
  • Вопрос задан
  • 38 просмотров
Пригласить эксперта
Ответы на вопрос 2
@soloveid
что чем меньше мы мокаем, тем больше багов ловят наши тесты.

Да, так и есть.
Моки используются для того, чтобы протестировать на какое-то нестандартное поведение, или
какое-то трудновоспроизводимое, например, отвалилось соединение с базой или упала сеть.
Но зачем мокать калькулятор, который как-бы везде используется и по факту в мок подставляется
его кусок кода (принцип DRY не знаком?), мне непонятно.
Ответ написан
@vabka
Токсичный шарпист
> как правильнее
Выбирай оптимальное соотношение, между простотой написания, скоростью выполнения, и покрытием.

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

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

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

А теперь про остальное:
Все пишем по TDD, все тесты зеленые, а баги все равно появляются.

Скорее всего, проблема архитектурная. Из-за большого количества моков у вас возникло много непротестированного кода.
То что вы мокаете также должно быть полностью протестировано.

один из наших сервисов, который использует базу данных, отпарвляет SQL запрос с синкаксической ошибкой

Выносишь кусок кода, который генерирует этот SQL-запрос в новый сервис, и тестируешь, что он всегда даёт корректный синтаксис.

Советую почитать про чистую архитектуру
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы