Роль модульного тестирования тяжело переоценить, но теория тестирования не стоит на месте. Еще не все успели привыкнуть к хипстерскому понятию TDD, как на всех углах звучит очередное трех-буквенное сокращение BDD. Исчерпывающее описание того, что же такое BDD, можно найти в статье Введение в BDD. В данной статье речь пойдет о фреймворке cucumber, позволяющем наглядно воплотить в жизнь те идеи, которые заложены в тестировании через поведение.
Cucumber
Cucumber - библиотека для тестирования, которая предлагает описывать сценарии тестирования на естественном языке в обычном текстовом файле с расширением .feature:Feature: Calculator Scenario Outline: Sum of the two numbers Given two numbers <a> and <b> When we try to find sum of our numbers Then result should be <result> Examples: | a | b | result | | 3 | 2 | 5 |
И связывать их с реализующим их кодом:
public class CalculatorSteps { private Calculator calc; double a; double b; double result; @Given("^two numbers (\\d) and (\\d)") public void given(double a, double b) { this.a = a; this.b = b; this.calc = new Calculator(); } @When("^we try to find sum of our numbers") public void when() { result = calc.sum(a, b); } @Then("^result should be (\\d)") public void then(double res) { Assert.assertEquals(res, result, 0.0001); } }Код класса Calculator
public class Calculator { public double sum(double a, double b) { return a + b; } public double pow(double a, double b) { return a * b; } }
Основой описания сценария тестирования являются шаги, такие как Given, When, Then. Каждому шагу соответствует аннотация, которая с помощью регулярного выражения связывает метод, над которым объявлена, со строкой в текстовом описании сценария.
Шаги тестирования группируются в сценарии (Scenario), которые в свою очередь описывают некоторую функциональность (Feature).
Структура проекта
src ├── main │ ├── java │ │ └── com │ │ └── programming086 │ │ └── cucumber │ │ └── Calculator.java │ └── resources └── test ├── java │ └── com │ └── programming086 │ └── cucumber │ ├── CalculatorSteps.java │ └── CucumberTestRunner.java └── resources └── com └── programming086 └── cucumber └── calculator.featureОсновные файлы в cucumber - это текстовый .feature файл с описанием сценариев и .java файлы с описанием реализации шагов выполнения сценариев. Никаких обязательств, кроме расширения, на имена этих файлов не накладывается. Но отсюда следует еще один момент - шаг, описанный в одном текстовом файле будет искаться во всех java-реализациях. Т.е. надо понимать, что две реализации одинаково описанных шагов недопустимы!
Интеграция с популярными библиотеками тестирования
Cucumber прекрасно интегрируется в существующие библиотеки для запуска тестов, такие как JUnit и TestNG.
Интеграция с JUnit
Для запуска сценариев с помощью JUnit, в проект необходимо добавить зависимость от, собственно, Cucumber-а и библиотеки для его интеграции с JUnit:
<!-- Реализация Cucumber для Java --> <dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-java</artifactId> <version>1.1.8</version> </dependency> <!-- Библиотека для интеграции Cucumber с JUnit -->Для Cucumber реализован свой org.junit.runner.Runner:<groupId>info.cukes</groupId> <artifactId>cucumber-junit</artifactId> <version>1.1.8</version> </dependency>
import cucumber.api.junit.Cucumber; import org.junit.runner.RunWith; @RunWith(Cucumber.class) public class CucumberTestRunner { }
Интеграция с TestNG
В случае с TestNG, CucumberTestRunner должен наследоваться от AbstractTestNGCucumberTests:import cucumber.api.testng.AbstractTestNGCucumberTests; public class CucumberTests extends AbstractTestNGCucumberTests { }Как и в случае с JUnit, вы должны добавить в зависимости проекта библиотеку интеграции Cucumber с TestNG:
<!-- Реализация Cucumber для Java --> <dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-java</artifactId> <version>1.1.8</version> </dependency> <!-- Библиотека для интеграции Cucumber с TestNG --><groupId>info.cukes</groupId> <artifactId>cucumber-testng</artifactId> <version>1.1.8</version> </dependency>
Способы описать параметры
В самом начале статьи я привел пример описания параметризованного сценария. Возможность описывать аргументы непосредственно в текстовом файле - очень мощная фича cucumber!Этот фреймворк предоставляет несколько механизмов описания параметров тестов прямо в текстовом файле.
Первый - это группы в регулярном выражении, связывающем описание шага с его реализацией:
Feature: Example of taking the test arguments Scenario: Take argument from string Given some number value '12.3' Given some string value 'Hello world!' Given some date value 01.08.2015
public class ExampleSteps { @Given("^some number value '(.*)'") public void givenNumber(double number) { System.out.println(number); } @Given("^some string value '(.*)'") public void givenString(String str) { System.out.println(str); } @Given("^some date value (.*)") public void givenDate(@Format("dd.MM.yyyy") Date date) { System.out.println(date); } }
12.3 Hello world! Sat Aug 01 00:00:00 MSK 2015Не плохо! Но что, если надо выполнить один и тот же сценарий с разными аргументами? На этот случай в Cucumber есть возможно перечислить все возможные значения аргумента в ASCII таблице:
Given some list of values |a| |b| |c|
@Given("^some list of values") public void givenList(List<String> list) { for (String s : list) { System.out.println(s); } }
a b cИли описать ассоциативный массив:
Given some map of values |a|1| |b|2| |c|3|
@Given("^some map of values") public void givenMap(Map<String, Integer> map) { for (Map.Entry entry : map.entrySet()) { System.out.println(entry); } }
a=1 b=2 c=3Для сценария с несколькими аргументами и вариантами, можно использовать ASCII таблицу с именованными столбцами:
Scenario Outline: Take few arguments from table Given argument <a>, argument <b> Examples: | a | b | | 1 | one | | 2 | two |
@Given("^argument (.*), argument (.*)") public void givenFewArguments(int a, String b) { System.out.println(a + "\t" + b); }
1 one 2 twoОбратите внимание на описание сценария, при реализации данного подхода. Вместо ключевого слова Scenario здесь используется Scenario Outline.
На этом функциональность библиотеки не заканчивается и Cucumber предлагает описывать в качестве аргументов целые классы!
Given some users | name | age | | Jon | 18 | | Anna | 23 |
public class User { String name; int age; @Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
@Given("^some users$") public void givenSomeUsers(List<User> users) throws Throwable { for (User user : users) { System.out.println(user); } }
User{name='Jon', age=18} User{name='Anna', age=23}Здесь стоит обратить внимание на то, что видимость полей в классе User не имеет значения, а для его инстанцирования используется хитрая ReflectionFactory, что приводит к тому, что конструктор вызван не будет!.
Предустановки
В начале каждого сценария cucumber запускает шаги описанные в блоке background:Feature: Example Background: prepare for scenario Given any action before every scenarioТакже есть возможность определить методы, выполняющиеся до и после каждого шага. Для этого существуют аннотации @Before и @After:
public class ExampleSteps { @Before public void setUp() { } @After public void tearDown() { } }Аннотаций, аналогичных @BeforeClass и @AfterClass в JUnit, в cucumber нет. Но если они очень понадобятся, можно использовать обходной путь:
Feature: Example Background: do it at onece Given do something at once
public class ExampleSteps { boolean isFirstExecution = true; @Given("^do something at once") public void initialization(String str) { if (isFirstExecution) { // do something... isFirstExecution = false; } } }
Не просто Cucumber - Огурец!
Еще одной эффектной особенностью Cucumber, является поддержка множества языков при описании сценариев. Среди них есть и Русский!# language: ru Функционал: Калькулятор Структура сценария: Суммирование двух чисел Допустим дано два числа <a> и <b> Если сложить их То получим <результат> Примеры: | a | b | результат | | 3 | 2 | 5 |Хорошо это или плохо, но аннотации тоже переведены на Русский:
@Дано("^дано два числа (-?\\d+.?\\d*) и (-?\\d+.?\\d*)") public void given(double a, double b) { ... } @Если("^сложить их") public void when_sum() { ... } @Тогда("^получим (-?\\d+.?\\d*)") public void then(double res) { ... }Впрочем, здесь фреймворк оставляет за вами выбор: использовать ли локализованные имена аннотаций, или пользоваться английскими. Вот перечень ключевых слов и их эквиваленты для русской локали:
"name": "Russian", "native": "русский", "feature": "Функция|Функционал|Свойство", "background": "Предыстория|Контекст", "scenario": "Сценарий", "scenario_outline": "Структура сценария", "examples": "Примеры", "given": "*|Допустим|Дано|Пусть", "when": "*|Если|Когда", "then": "*|То|Тогда", "and": "*|И|К тому же|Также", "but": "*|Но|А"Полный перечень соответствия для всех поддерживаемых языков можно найти в файле i18n.json, который находится в gherkin-2.12.2.jar в пакете gherkin.
Не всем огурцы по нраву...
The Dangers of Cucumber from Þorgeir Ingvarsson
Суть презентации, на мой взгляд:
если вы стремитесь к тому, чтобы вашим фреймворком могли пользоваться даже дебилы, именно они им и будут пользоваться.
...но
Если подходить к пользованию Cucumber с умом и к месту, можно поиметь не мало счастья.Предложенный автором статьи "Введение в BDD" подход к написанию тестов выглядит действительно многообещающим. Но на практике имена из серии shouldDoSomething очень трудно воспринимаются при чтении кода и требуют достаточно много усилий для того, чтобы понять, какой именно смысл вложен в их название. Немногим лучше обстоят дела в Scala с фреймворком specs2. Но код тестов все еще остается трудно читаемый.
Пример теста на spec2
Почему на мой взгляд важно, чтобы код тестов был прост для понимания? При анализе отчетов о прогоне тестов, найдя упавший тест, первый вопрос на который вам надо найти ответ: "а что, собственно, тест делает?", и только потом разбираться с тем, как он это делает. В моем представлении Cucumber - отличное решение, позволяющее минимизировать усилия и время для ответа на первый вопрос, и сосредоточиться на решении более важного, второго.
Замечу, что если речь идет о модульных тестах, которые по своему определению просты и компактны, то использование Cucumber скорее всего будет лишним. Но в случае больших и подчас запутанных функциональных тестов, очень важно, чтобы их сценарии были описаны в простом и строго структурированном виде. И именно здесь Cucumber может проявить себя с лучшей стороны!
Немає коментарів:
Дописати коментар