CTO Vetmanager, PHP Developer, Ironman 70.3

One assert per test. Одно утверждение на тест.

На последнем митапе в кулуарах активно обсуждался вопрос про один assert на тест. Я задал прямой вопрос Александру Макарову и его ответ был однозначен - в каждом тесте должен быть только один assert

Это породило дискуссию, обсуждали какие-то случаи и эта дискуссия продолжилась в чате сообщества.

Выводы

  1. В unit и smoke тестах - должно быть только одно утверждение на тест.
  2. В тестах, которые долго и дорого готовить(интеграционные, функциональные, фича-тесты) можно допустить несколько assert, если они не избыточны и не дублируют друг друга в хоть в каком-то виде.
  3. Если в тесте несколько утверждений, скорее всего, перед вами несколько разных тестов. Разделите их, а чтобы вас не смущало нарушение DRY, используйте приватные методы тест-класса или даже создавайте классы специально для написания тестов.

В чате обсуджали ситуацию:

Допустим: у нас есть класс, который применяет скидочные правила к товарам в корзине. Получает корзину, получает правила. Теперь у него есть список товаров у которых заполнены свойства, которые касаются скидки.

Например написали вот такой метод, который все это тестирует:

$sut = new CartWithDiscount($cart, $discountRules);

$item = $sut->itemById($id);

$this->assertEquals($item->amount, 550);
$this->assertEquals($item->discountPercent, 10);
$this->assertEquals($item->discountDescription, "Скидки для привитых");

Что делать с этим тестовым методом?

  1. Разбить на 3 одинаковых теста, но каждый тест будет иметь 1 ассерт
  2. Оставить как есть и забить на правило 1 ассерт на тест
  3. Оставить какой-то 1 ассерт, а остальные считать избыточными и выбросить
  4. Разбить класс CartWithDiscount на 15 других классов, чтобы даже при большом желании больше 1го ассерта не могло получиться в тесте.
  5. Я свой вариант в комментариях напишу

Предлагались разные варианты, но все они сводились к тому, что “один assert” это хорошее правило и его нужно соблюдать.

Был вот такой вариант ответа:

Логически здесь один ассерт, что-то вроде assertCorrectItem. Я думаю оставить как есть и правило по факту не нарушено

Но и это исключение, только подтверждает правило “one assert per test”. Кто-то предложил вот такой вариант для соблюдения правила.

$this->assertEquals(
    $item->toArray(), 
    [
        amount => 550,
        discountPercent => 10,
        discountDescription => "Скидки для привитых"
    ]
);

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

public function testSumForDiscount()
{
    $originalItem = $this->newItem(611);

    $item = $this->applyDiscount($originalItem, 10, 'Скидки для привитых');

    $this->assertEquals($item->amount, 550);
}

public function testPercent()
{
    $originalItem = $this->newItem(611);

    $item = $this->applyDiscount($originalItem, 10, 'Скидки для привитых');

    $this->assertEquals($item->discountPercent, 10);
}

public function testDescription()
{
    $originalItem = $this->newItem(611);

    $item = $this->applyDiscount($originalItem, 10, 'Скидки для привитых');

    $this->assertEquals($item->discountDescription, 'Скидки для привитых');
}

Перед написанием каждого теста мы задаем себе вопрос “Что конкретно может сломаться?”. Не может сломаться работа метода или работа класса. В нашем случае ломается рассчет суммы или правильное указание процента и описания.

Но есть еще одно “НО” для этого примера. Это всего лишь пример и он не самый удачный, чтобы показать проблему острой необходимости нескольких утверждений в тесте. Есть правило - “не тестируй тривиальный код”. К примеру: геттеры и сеттеры не стоит тестировать.

В этом конкретном случае нужно просто удалить лишние утверждения.

$sut = new CartWithDiscount($cart, $discountRules);

$item = $sut->itemById($id);

$this->assertEquals($item->amount, 550);

Оставить только эту проверку, остальные - избыточны. Скорее всего, вся соль именно в выборе нужного правила, остальной код тривиален и его не стоит тестировать. Если остальной код сложен или если вы словили баг в тривиальном коде - добавляйте новые тесты соблюдая правило “одно утверждение на тест”.

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

Егор Бугаенко идет дальше этого правила и вводит более строгое. “Только одно выражение на тест”. Ваш тест должен начинаться с assert, как вам такое?

@Test
public void testIntStream() {
  assertEquals(
    new ArrayFromRandom(
      new Random(System.currentTimeMillis() as seed)
    ).toArray(SIZE),
    new Random(seed).ints().limit(SIZE).toArray()
  );
}

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