На последнем митапе в кулуарах активно обсуждался вопрос про один assert на тест. Я задал прямой вопрос Александру Макарову и его ответ был однозначен - в каждом тесте должен быть только один assert
Это породило дискуссию, обсуждали какие-то случаи и эта дискуссия продолжилась в чате сообщества.
Выводы
- В unit и smoke тестах - должно быть только одно утверждение на тест.
- В тестах, которые долго и дорого готовить(интеграционные, функциональные, фича-тесты) можно допустить несколько assert, если они не избыточны и не дублируют друг друга в хоть в каком-то виде.
- Если в тесте несколько утверждений, скорее всего, перед вами несколько разных тестов. Разделите их, а чтобы вас не смущало нарушение DRY, используйте приватные методы тест-класса или даже создавайте классы специально для написания тестов.
В чате обсуджали ситуацию:
Допустим: у нас есть класс, который применяет скидочные правила к товарам в корзине. Получает корзину, получает правила. Теперь у него есть список товаров у которых заполнены свойства, которые касаются скидки.
Например написали вот такой метод, который все это тестирует:
$sut = new CartWithDiscount($cart, $discountRules);
$item = $sut->itemById($id);
$this->assertEquals($item->amount, 550);
$this->assertEquals($item->discountPercent, 10);
$this->assertEquals($item->discountDescription, "Скидки для привитых");
Что делать с этим тестовым методом?
- Разбить на 3 одинаковых теста, но каждый тест будет иметь 1 ассерт
- Оставить как есть и забить на правило 1 ассерт на тест
- Оставить какой-то 1 ассерт, а остальные считать избыточными и выбросить
- Разбить класс CartWithDiscount на 15 других классов, чтобы даже при большом желании больше 1го ассерта не могло получиться в тесте.
- Я свой вариант в комментариях напишу
Предлагались разные варианты, но все они сводились к тому, что “один 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()
);
}
Мне нравится как это выглядит, увы не всегда получается так организовать свой код.