Java со вкусом огурчика

воскресенье, 23 августа 2015 г.
Роль модульного тестирования тяжело переоценить, но теория тестирования не стоит на месте. Еще не все успели привыкнуть к хипстерскому понятию 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

Основой описания сценария тестирования являются шаги, такие как Given, When, Then. Каждому шагу соответствует аннотация, которая с помощью регулярного выражения связывает метод, над которым объявлена, со строкой в текстовом описании сценария.

Шаги тестирования группируются в сценарии (Scenario), которые в свою очередь описывают некоторую функциональность (Feature).

Структура проекта  

src
    ├── main
    │   ├── java
    │   │   └── ru
    │   │       └── dokwork
    │   │           └── cucumber
    │   │               └── Calculator.java
    │   └── resources
    └── test
        ├── java
        │   └── ru
        │       └── dokwork
        │           └── cucumber
        │               ├── CalculatorSteps.java
        │               └── CucumberTestRunner.java
        └── resources
            └── ru
                └── dokwork
                    └── 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 -->
<dependency>
    <groupId>info.cukes</groupId>
    <artifactId>cucumber-junit</artifactId>
    <version>1.1.8</version>
</dependency>
Для Cucumber реализован свой org.junit.runner.Runner:
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 с JUnit -->
<dependency>
    <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.

Не всем огурцы по нраву...



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

...но

Если подходить к пользованию Cucumber с умом и к месту, можно поиметь не мало счастья.

Предложенный автором статьи "Введение в BDD" подход к написанию тестов выглядит действительно многообещающим. Но на практике имена из серии shouldDoSomething очень трудно воспринимаются при чтении кода и требуют достаточно много усилий для того, чтобы понять, какой именно смысл вложен в их название. Немногим лучше обстоят дела в Scala с фреймворком specs2. Но код тестов все еще остается трудно читаемый.
Пример теста на spec2

Почему на мой взгляд важно, чтобы код тестов был прост для понимания? При анализе отчетов о прогоне тестов, найдя упавший тест, первый вопрос на который вам надо найти ответ: "а что, собственно, тест делает?", и только потом разбираться с тем, как он это делает. В моем представлении Cucumber - отличное решение, позволяющее минимизировать усилия и время для ответа на первый вопрос, и сосредоточиться на решении более важного, второго.

Замечу, что если речь идет о модульных тестах, которые по своему определению просты и компактны, то использование Cucumber скорее всего будет лишним. Но в случае больших и подчас запутанных функциональных тестов, очень важно, чтобы их сценарии были описаны в простом и строго структурированном виде. И именно здесь Cucumber может проявить себя с лучшей стороны!

2 комментария :

  1. Вова, кто бы мог подумать, что в поиске штук по cucumberjs и тестирования ангуляров мне выдаст эту статью)) Жаль она слишком обзорная

    ОтветитьУдалить
  2. Познавательно, спасибо!

    ОтветитьУдалить

Ваше мнение мне искренне интересно. Смелее!

Технологии Blogger.