Статьи

Параметризованные тесты и теории

В этой серии статей о модульном тестировании с помощью JUnit мы узнали о нескольких аспектах модульного тестирования и о том, как реализовать их с помощью JUnit. Мы можем подвести итоги серии до сих пор как:

  • Часть 1. Создание базового модульного теста с использованием Maven и IntelliJ
  • Часть 2 : Использование утверждений и аннотаций
  • Часть 3 : Использование  assertThat  с совпадениями Hamcrest

В этом посте мы узнаем о параметризованных тестах и ​​теориях.

JUnit параметризованные тесты

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

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

EmailIdUtility.java

package guru.springframework.unittest.parameterized;

import java.util.regex.Matcher;
import java.util.regex.Pattern;


public class EmailIdUtility {
    public static String createEmailID(String firstPart,String secondPart){
        String generatedId = firstPart+"."+secondPart+"@testdomain.com";
        return generatedId;
    }
    public static boolean isValid(String email){
        String regex = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$";
        Pattern pattern = Pattern.compile(regex);
        Matcher m = pattern.matcher(email);
        return m.matches();
    }
}

EmailIdUtility Класс выше , имеет два вспомогательных методов.  createEmailID() Метод принимает два  String параметра и генерирует электронный идентификатор в формате конкретного. Формат прост — если вы передаете  mark  и  doe в  качестве параметров этому методу, он возвращает  [email protected] . Второй  isValid() метод принимает идентификатор электронной почты как a  String, использует  регулярное выражение  для проверки его формата и возвращает результат проверки.

Сначала мы проверим  isValid() метод с помощью параметризованного теста. JUnit запускает параметризованный тест со специальным средством запуска,  Parameterized и нам нужно объявить его с  @RuntWith аннотацией. В параметризованном тестовом классе мы объявляем переменные экземпляра, соответствующие количеству входных данных для теста и выходных данных. Поскольку  isValid() тестируемый метод принимает один  String параметр и возвращает a  boolean, мы объявляем две соответствующие переменные. Для параметризованного теста нам нужно предоставить конструктор, который будет инициализировать переменные. 

EmailIdValidatorTest.class

. . .
@RunWith(value = Parameterized.class)
public class EmailIdValidatorTest {

    private String emailId;
    private boolean expected;

    public EmailIdValidatorTest(String emailId, boolean expected) {
        this.emailId = emailId;
        this.expected = expected;
    }
. . .

Мы также должны предоставить публичный статический метод с  @Parameters аннотацией. Этот метод будет использоваться тестером для подачи данных в наши тесты.

. . .
@Parameterized.Parameters(name= "{index}: isValid({0})={1}")
public static Iterable<Object[]> data() {
    return Arrays.asList(new Object[][]{
                    {"[email protected]", true},
                    {"[email protected]", true},
                    {"[email protected]", true},
                    {"mary@testdomaindotcom", false},
                    {"mary-smith@testdomain", false},
                    {"testdomain.com", false}
            }
    );
}
. . . 

Приведенный  @Parametersвыше аннотированный метод возвращает набор элементов тестовых данных (которые, в свою очередь, хранятся в массиве). Элементы тестовых данных — это различные варианты данных, включая входные данные, а также ожидаемые выходные данные, необходимые для теста. Количество элементов тестовых данных в каждом массиве должно совпадать с количеством параметров, которые мы объявили в конструкторе.

Когда тест выполняется, бегун создает экземпляр класса теста один раз для каждого набора параметров, передавая параметры конструктору, который мы написали. Затем конструктор инициализирует объявленные нами переменные экземпляра.

Обратите внимание на необязательный  name атрибут, который мы написали в  @Parameters аннотации для определения параметров, используемых в тестовом прогоне. Этот атрибут содержит заполнители, которые заменяются во время выполнения.

  • {index} : текущий индекс параметра, начиная с 0.
  • {0}, {1},… : первое, второе и т. Д. Значение параметра. Например, для параметра [email protected]», true} , тогда  {0} = [email protected]  и  {1} = true .

Наконец, мы пишем тестовый метод с пометкой  @Test. Полный код параметризованного теста таков.

EmailIdValidatorTest.java

package guru.springframework.unittest.parameterized;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import static org.hamcrest.CoreMatchers.*;

import java.util.Arrays;

import static org.junit.Assert.*;
@RunWith(value = Parameterized.class)
public class EmailIdValidatorTest {

    private String emailId;
    private boolean expected;

    public EmailIdValidatorTest(String emailId, boolean expected) {
        this.emailId = emailId;
        this.expected = expected;
    }
    @Parameterized.Parameters(name= "{index}: isValid({0})={1}")
    public static Iterable<Object[]> data() {
        return Arrays.asList(new Object[][]{
                        {"[email protected]", true},
                        {"[email protected]", true},
                        {"[email protected]", true},
                        {"mary@testdomaindotcom", false},
                        {"mary-smith@testdomain", false},
                        {"testdomain.com", false}
                }
        );
    }
    @Test
    public void testIsValidEmailId() throws Exception {
        boolean actual= EmailIdUtility.isValid(emailId);
        assertThat(actual, is(equalTo(expected)));
    }
}

Вывод при запуске параметризованного теста в IntelliJ таков.

Параметризованный тестовый вывод

Теории Юнит

В параметризованном тесте элементы тестовых данных определяются статически, и вы, как программист, несете ответственность за выяснение того, какие данные необходимы для определенного диапазона тестов. Время от времени вы, вероятно, захотите сделать тесты более обобщенными. Скажем, вместо проверки конкретных значений вам может потребоваться проверить более широкий диапазон допустимых входных значений. Для этого сценария JUnit предоставляет теории.

Теория — это специальный метод тестирования, который выполняет специальный бегун JUnit ( Теории ). Чтобы использовать бегун, пометьте свой тестовый класс  @RunWith(Theories.class) аннотацией. В  теории  бегуна выполняет теорию против нескольких входных данных , называемых  точками данных . Теория помечается  @Theory , но в отличие от обычных  @test  способов описан  @Theory  метод имеет параметры. Чтобы заполнить эти параметры значениями,   бегунок Теории использует значения точек данных, имеющих один и тот  же тип .

Есть два типа точек данных. Вы используете их через следующие две аннотации:

  • @DataPoint : аннотирует поле или метод как одну точку данных. Значение поля или того, что возвращает метод, будет использоваться в качестве потенциального параметра для теорий, имеющих один и тот  же тип .
  • @DataPoints : аннотирует массив или поле или метод итерируемого типа как полный массив точек данных. Значения в массиве или итерируемые будут использоваться в качестве потенциальных параметров для теорий, имеющих один и тот  же тип . Используйте эту аннотацию, чтобы избежать загромождения вашего кода единичными полями данных.

Примечание . Все поля и методы точки данных должны быть объявлены как  открытые  и  статические .

. . .
@DataPoint
public static String name="mary";

@DataPoints
public static String[] names() {
    return new String[]{"first","second","abc","123"};
}
. . .

В приведенном выше примере кода мы аннотировали  String поле с  @DataPoint аннотацией и  names() метод, который возвращает a  String[] с  @DataPoints аннотацией.

Создание JUnit Threory

Вспомните метод  createEmailID (),  который мы написали ранее в этом посте: « Метод createEmailID () принимает два параметра String и генерирует идентификатор электронной почты в определенном формате. «Теория тестирования, которую мы можем установить:« Если stringA и stringB, переданные createEmailID (), не равны NULL, он вернет идентификатор электронной почты, содержащий как stringA, так и stringB  ». Вот как мы можем представить теорию.

. . .
@Theory
public void testCreateEmailID(String firstPart, String secondPart) throws Exception {
 String actual= EmailIdUtility.createEmailID(firstPart,secondPart);
 assertThat(actual, is(allOf(containsString(firstPart), containsString(secondPart))));
}
. . .

testCreateEmailID() Теория мы писали принимает два  String параметра. Во время выполнения  бегун Теорий будет вызывать  testCreateEmailID() прохождение каждой возможной комбинации точек данных, которые мы определили, типа  String . Например ( Мария, Мария ), ( Мария, первая ), ( Мария, вторая ) и так далее.

Предположения

Это очень характерно для теории  НЕ  быть действительным в определенных случаях. Вы можете исключить их из теста, используя предположения, что в основном означает « не запускать этот тест, если эти условия не применяются ». В нашей теории предполагается, что параметры, передаваемые  тестируемому  методу createEmailID (), являются ненулевыми значениями .

Если предположение не выполняется, точка данных игнорируется. Программно мы добавляем допущения в теории с помощью одного из многих методов   класса Assume .
Вот наша модифицированная теория с допущениями.

. . .
@Theory
public void testCreateEmailID(String firstPart, String secondPart) throws Exception {
    assumeNotNull(firstPart, secondPart);
    assumeThat(firstPart, notNullValue());
    assumeThat(secondPart, notNullValue());
    String actual= EmailIdUtility.createEmailID(firstPart,secondPart);
     assertThat(actual, is(allOf(containsString(firstPart),  containsString(secondPart))));
}
. . .

В приведенном выше коде мы использовали,  assumeNotNull потому что мы предполагаем, что переданные параметры  createEmailID() являются  ненулевыми  значениями. Следовательно, даже если существует  нулевая  точка данных и тестовый исполнитель передает ее в нашу теорию, предположение не будет выполнено, и точка данных будет проигнорирована.
Два, которые  assumeThat мы написали вместе, выполняют точно такую ​​же функцию, что и  assumeNotNull. Я включил их только для демонстрации использования  предположения. То , что вы видите, очень похоже на  утверждение, которое  мы рассмотрели в предыдущем посте.

Ниже приведен полный код, использующий теорию для проверки  метода createEmailID ()  .

EmailIDCreatorTest.java

package guru.springframework.unittest.parameterized;

import org.junit.Test;
import org.junit.experimental.theories.DataPoint;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.util.Arrays;

import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeNotNull;
import static org.junit.Assume.assumeThat;

@RunWith(Theories.class)
public class EmailIDCreatorTest {

    @DataPoints
    public static String[] names() {
        return new String[]{"first","second","abc","123",null};
    }
    @DataPoint
    public static String name="mary";
    /*Generated Email ID returned by EmailIdUtility.createEmailID must contain first part and second part passed to it*/
    @Theory
    public void testCreateEmailID(String firstPart, String secondPart) throws Exception {
        System.out.println(String.format("Testing with %s and %s", firstPart, secondPart));
        assumeNotNull(firstPart, secondPart);
        /*Same assumptions as assumeNotNull(). Added only  to demonstrate usage of assertThat*/
        assumeThat(firstPart, notNullValue());
        assumeThat(secondPart, notNullValue());
        String actual= EmailIdUtility.createEmailID(firstPart,secondPart);
        System.out.println(String.format("Actual: %s \n", actual));
        assertThat(actual, is(allOf(containsString(firstPart), containsString(secondPart))));
    }
}

В приведенном выше тестовом классе я включил  null в качестве точки данных в оператор возврата строки 23 наши предположения и пару  System.out.println() операторов для отслеживания того, как параметры передаются в теории во время выполнения.

Вот результат теста в IntelliJ: 
Выход Теорий Юнит

Кроме того, вот вывод, который я получил при выполнении теста с Maven для вашего обзора:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running guru.springframework.unittest.parameterized.EmailIDCreatorTest
Testing with mary and mary
Actual: [email protected] 

Testing with mary and first
Actual: [email protected] 

Testing with mary and second
Actual: [email protected] 

Testing with mary and abc
Actual: [email protected] 

Testing with mary and 123
Actual: [email protected] 

Testing with mary and null
Testing with first and mary
Actual: [email protected] 

Testing with first and first
Actual: [email protected] 

Testing with first and second
Actual: [email protected] 

Testing with first and abc
Actual: [email protected] 

Testing with first and 123
Actual: [email protected] 

Testing with first and null
Testing with second and mary
Actual: [email protected] 

Testing with second and first
Actual: [email protected] 

Testing with second and second
Actual: [email protected] 

Testing with second and abc
Actual: [email protected] 

Testing with second and 123
Actual: [email protected] 

Testing with second and null
Testing with abc and mary
Actual: [email protected] 

Testing with abc and first
Actual: [email protected] 

Testing with abc and second
Actual: [email protected] 

Testing with abc and abc
Actual: [email protected] 

Testing with abc and 123
Actual: [email protected] 

Testing with abc and null
Testing with 123 and mary
Actual: [email protected] 

Testing with 123 and first
Actual: [email protected] 

Testing with 123 and second
Actual: [email protected] 

Testing with 123 and abc
Actual: [email protected] 

Testing with 123 and 123
Actual: [email protected] 

Testing with 123 and null
Testing with null and mary
Testing with null and first
Testing with null and second
Testing with null and abc
Testing with null and 123
Testing with null and null
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.076 sec

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

Резюме

Параметризованные тесты в JUnit помогают удалить тестовый код пластины котла, что экономит время при написании тестового кода. Это особенно полезно при разработке  корпоративных приложений  с помощью Spring Framework. Однако распространенная жалоба заключается в том, что при неудачном параметризованном тесте очень трудно увидеть параметры, которые привели к его сбою. Правильно называя  @Parameters аннотации и отличная поддержка модульного тестирования, которую предоставляют современные IDE, такие жалобы быстро не обосновываются. Хотя теории используются реже, они являются мощным инструментом в любом инструменте тестирования программистов. Теории не только делают ваши тесты более выразительными, но вы увидите, как ваши тестовые данные становятся более независимыми от кода, который вы тестируете. Это улучшит качество вашего кода, так как вы с большей вероятностью попадете на крайние случаи, которые вы, возможно, ранее пропустили.