Към съдържанието
wannacode

Тестове на ниво компонент

Накратко: Сценарият е за примерите, които бизнесът е договорил и иска да чете. Тестът на ниво компонент вика кода направо: десет суми за милисекунди, без стъпки и без Gherkin. Двата вида проверяват едно правило за различни читатели. Сценариите остават малко, а тестовете на компонент поемат техническите случаи.

Проблем

Петнадесет сценария за една граница

Програмистът реши да провери доставката добре: 1 цент, 10 €, 49,49 €, 49,50 €, 49,99 €, 50 €, 50,01 €, 100 €, 250 €, отрицателна сума. Записа всичко като сценарии.

Собственикът на магазина отвори файла със сценариите, за да види правилото за доставката. Видя петнадесет почти еднакви сценария и затвори файла. Живата документация вече не се четеше.

Идеята

Тестът вика кода направо

От курса по програмиране познаваш тестовете на ниво компонент: метод, който вика кода и сверява резултата. За правилото за доставката няма нужда от Gherkin и от стъпки. Тестът вика метода направо:

src/test/java/shop/DeliveryTest.java
@ParameterizedTest(name = "feeFor({0}) = {1}")
@CsvSource({ "4999, 490", "5000, 0", "1, 490", "25000, 0" })
void feeForTotal(int total, int fee) {
assertEquals(fee, Delivery.feeFor(total));
}

Това е параметризиран тест: една проверка и таблица от случаи. Прилича на рамката на сценарий с „Примери“, но е за програмиста. Нов случай е нов ред в таблицата.

Отказът се проверява с отделен тест. Правилото хвърля изключение, и тестът проверява, че го хвърля:

src/test/java/shop/DeliveryTest.java
@Test
@DisplayName("празна количка се отказва")
void emptyCartIsRefused() {
assertThrows(IllegalArgumentException.class, () -> Delivery.feeFor(0));
}
По-точно, за любопитните

Инструментите са стандартните за всеки език: JUnit 5 (параметризираните тестове са в модула junit-jupiter-params), xUnit с [Theory] и [InlineData], pytest и вграденият в Node node:test. В курса по програмиране Python тестовете бяха с unittest. pytest е по-разпространеният: тестът е обикновена функция с assert, а parametrize прави таблицата.

Идеята

Кое правило на какво ниво

Правилото е едно. Проверките са на две нива, и всяко има своя читател:

  • Сценарият е за примерите, които бизнесът е договорил: точно 50,00 €, 49,99 €, празна количка. По един-два примера на правило. Той е живата документация и бизнесът го чете.
  • Тестът на ниво компонент е за техническите случаи: 1 цент, голяма сума, отрицателна сума, закръгляне. Те пазят кода, но не казват нищо ново за правилото.

Това, че границата е проверена и на двете нива, не е загуба. Сценарият казва какво е договорено. Тестът на компонента пада първи и точно там, където е грешката.

Правилото за избор е кратко: би ли бизнесът искал да прочете този пример? Ако да, сценарий. Ако не, тест на ниво компонент.

Пробвай

Сценарий или тест на компонент?

1. „Количка за точно 50,00 €: доставката е безплатна.“ Бизнесът го договори на срещата за сторито.
2. Сума от 1 цент, сума от 250 € и сума от 99 999,99 €.
3. Отрицателна сума, например -5,00 €, която никой клиент не може да въведе.

Идеята

Пускането

Целият файл с тестовете, пуснат срещу правилния код. Всичко минава:

src/test/java/shop/DeliveryTest.java
package shop;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class DeliveryTest {
@ParameterizedTest(name = "feeFor({0}) = {1}")
@CsvSource({ "4999, 490", "5000, 0", "1, 490", "25000, 0" })
void feeForTotal(int total, int fee) {
assertEquals(fee, Delivery.feeFor(total));
}
@Test
@DisplayName("празна количка се отказва")
void emptyCartIsRefused() {
assertThrows(IllegalArgumentException.class, () -> Delivery.feeFor(0));
}
}
Изход
ОК feeFor(1) = 490
ОК feeFor(25000) = 0
ОК feeFor(4999) = 490
ОК feeFor(5000) = 0
ОК празна количка се отказва
Тестове: 5, минали: 5, паднали: 0

Сега програмистът „опростява“ сравнението и закръгля сумата до цели евро преди него. 49,99 € става 50 €, и доставката става безплатна:

src/main/java/shop/Delivery.java
package shop;
public class Delivery {
public static final int FREE_FROM = 5000;
public static final int FEE = 490;
// Таксата за доставка в центове. total е сумата на количката, също в центове.
public static int feeFor(int total) {
if (total <= 0) {
throw new IllegalArgumentException("Празна количка не се поръчва");
}
return Math.round(total / 100.0) * 100 >= FREE_FROM ? 0 : FEE;
}
}

Същите тестове, срещу новия код:

src/test/java/shop/DeliveryTest.java
package shop;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class DeliveryTest {
@ParameterizedTest(name = "feeFor({0}) = {1}")
@CsvSource({ "4999, 490", "5000, 0", "1, 490", "25000, 0" })
void feeForTotal(int total, int fee) {
assertEquals(fee, Delivery.feeFor(total));
}
@Test
@DisplayName("празна количка се отказва")
void emptyCartIsRefused() {
assertThrows(IllegalArgumentException.class, () -> Delivery.feeFor(0));
}
}
Изход
ОК feeFor(1) = 490
ОК feeFor(25000) = 0
ОК feeFor(5000) = 0
ОК празна количка се отказва
ПАДА feeFor(4999) = 490: expected: <490> but was: <0>
Тестове: 5, минали: 4, паднали: 1

Пада точно един случай: 49,99 €. Името на теста казва сумата, а съобщението казва очакваното и полученото. Не трябва да търсиш коя стъпка от кой сценарий.

По-точно, за любопитните

Изходът тук е кратък отчет, подреден по име: инструментите се пускат наистина, но в обвивка, която печата само реда за всеки тест и броя. Имената на тестовете са различни на всеки език, защото всеки инструмент ги прави по своему: JUnit от name на параметризирания тест, xUnit от името на метода и аргументите, pytest от името на функцията и стойностите, node:test от текста, който му даваш.

Идеята

Защо не всичко с Gherkin

Gherkin е за разговора с бизнеса: примерите, договорени от трите амиго, записани така, че всеки да ги чете. Като инструмент за тестове той е скъп:

  • всеки нов ред в сценарий иска стъпка, а стъпките се поддържат;
  • сценарият минава през повече код, затова е по-бавен;
  • когато падне, причината е по-далеч от съобщението.

И най-важното: петнадесет технически сценария скриват трите, които казват правилото. Живата документация е жива, докато някой я чете.

Затова сценариите са малко и казват правилото. Тестовете на ниво компонент са много и пазят кода. Това е и пирамидата: широка основа от бързи тестове, а отгоре по-малко, по-бавни и по-близо до клиента.

По-точно, за любопитните

Аслак Хелесьой, създателят на Cucumber, е писал неведнъж, че Cucumber е преди всичко инструмент за общ език и сътрудничество, а тестовете са страничен резултат. Ако сценариите не се пишат и четат заедно с бизнеса, остава скъп начин да се пишат тестове.

Упражнение

Двете нива

1. Кой тест какво пази. Въпросите са за сценариите от предишния урок и тестовете от този.

1. Махаш случая 250,00 € (25000 цента) от параметризирания тест. Коя грешка вече няма да се хване от нито един тест в урока?
2. Кой тест хваща грешката „празната количка не се отказва, а плаща 4,90 €“?
3. Програмистът иска да добави 49,50 € и в сценариите, и в теста на ниво компонент. Какво казваш?

Тест

Провери се

1. Каква е разликата между сценарий и тест на ниво компонент за едно правило?
2. Защо тестът на ниво компонент е по-бърз от сценария?
3. Какво прави параметризираният тест?
4. Защо не се пише всичко с Gherkin?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·