Как стать автором
Обновить
86.06

Один assert на тест. А может быть, нет?

Время на прочтение4 мин
Количество просмотров1.8K
Автор оригинала: Mikhail Polivakha

Команда Spring АйО перевела статью эксперта Михаила Поливахи о том, почему правило о единственном assert'е на тест иногда можно и нужно нарушать.


Я искренне верю, что большинство людей не совершают зла намеренно (хотя некоторые — да, совершают) Многие проблемы современного мира возникают из‑за недопонимания и различных точек зрения на те или иные вопросы, усугублённых человеческими эмоциями, культурными различиями и другими факторами.

В частности, я считаю, что афоризм:

«Дорога в ад вымощена благими намерениями»

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

Например, так называемое «правило единственного assert'а», изложенное в книге Clean Code, я нередко нахожу запутанным и даже непрактичным во многих случаях, связанных с разработкой программного обеспечения в целом.

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

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

Почему?

Потому что программное обеспечение — это крайне разнообразная и сложная сфера. Давайте представим, что мы пишем фабричный класс, примерно вот такой:

public class ConfigurationFactory {

  public static Configuration createInstance() {
    String javaVersion = System.getProperty("java.version");
    String user = System.getenv("USER");
    int cpusAvailable = Runtime.getRuntime().availableProcessors();

    return new Configuration(javaVersion, user, cpusAvailable);
  }

  static class Configuration {

    String javaVersion;
    String user; 
    int cpusAvailable;

    // constructor, getters etc.
  }
}

Приведённый выше пример — это Java-код, реализующий фабрику, которая создаёт объект, предварительно настраивая его. Этот шаблон, по крайней мере с точки зрения семантики, довольно распространён в разработке в целом.

Теперь допустим, что мы хотим протестировать метод createInstance() этой фабрики. И вот возникает вопрос — какое именно поведение мы хотим протестировать?

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

Но тогда возникает следующий вопрос — как именно мы будем проверять это состояние?

Следуя правилу

Если следовать «правилу единственного assert’а», то мне пришлось бы ограничиться единственной проверкой внутри теста. И тут становится очевидным: если я не хочу жертвовать качеством тестирования, мне нужно вручную создать экземпляр Configuration в рамках теста. А затем сравнить, совпадает ли внутреннее состояние этой вручную сконструированной Configuration с той, которую возвращает фабрика.

Проблема №1

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

  • Скучным (а это куда более серьёзная проблема, чем может показаться на первый взгляд)

  • Подверженным ошибкам

  • Трудным в сопровождении (представьте, как «весело» будет добавлять 31-е поле).

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

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

  • установлен фабрикой,

  • и соответствует спецификации.

Проблема №2

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

Видите ли, я не знаю, на каком языке программирования вы пишете, но в Java по умолчанию при сравнении двух ссылок на объекты проверяется лишь то, указывают ли они на один и тот же участок памяти в heap-е Java процесса в рамках ОС.

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

Что можно сделать? Мы можем переопределить методы equals() и hashCode() в Java, чтобы сравнение происходило по содержимому полей, а не по ссылкам:

  static class Configuration {

    String javaVersion;
    String user;
    int cpusAvailable;

    // constructor, getters etc.

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }
      Configuration that = (Configuration) o;
      return cpusAvailable == that.cpusAvailable && Objects.equals(javaVersion, that.javaVersion) && Objects.equals(user,
          that.user);
    }

    @Override
    public int hashCode() {
      return Objects.hash(javaVersion, user, cpusAvailable);
    }
  }

Так что, хорошо ли это?
Нет. Это ужасно.

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

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

Решение

Просто пишите несколько assert’ов, если считаете это уместным. Не бойтесь.
Вас за это не посадят, честное слово.

В этом нет ничего сложного:

  @Test
  void testCreateInstance() {
    Configuration instance = ConfigurationFactory.createInstance();

    assertSoftly(softAssertions -> {
      softAssertions.assertThat(instance.getUser()).isEqualTo("my_user");
      softAssertions.assertThat(instance.getJavaVersion()).isEqualTo("17.0.4.1");
    });
  }

В результате вам не только не нужно создавать объект вручную, но и можно проверить только те поля, которые действительно вас интересуют, и на тех условиях, которые имеют смысл (вспомните пример с UUID). Вы не вынуждаете ваш класс Configuration переопределять equals()/hashCode() или подстраиваться под какие-либо особенности, нужные только для тестов.

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

Хорошего дня!


Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

Теги:
Хабы:
+12
Комментарии3

Публикации

Информация

Сайт
t.me
Дата регистрации
Численность
11–30 человек