Тестирование - DmitryGontarenko/usefultricks GitHub Wiki

Review

Модульное (Unit) - тестирование одного модуля (класса, метода) в полной изоляции от остальной программы, для всех своих зависимостей их поведение "имитируется" (используются заглушки, моки).
Интеграционное - тестирование кода в окружении, близком к фактическому окружению. Главная цель данной стратегии - убедиться в правильности взаимодействия с внешними ресурсами и взаимодействии различных технологий между собой. В интеграционных тестах часто используется работа базой данных.
Функциональное - тестирования ПО в целях проверки его способности решать задачи, нужные пользователям, проще говоря - тестирование какой-то отдельной функции.

Методологии:
TDD (Test-driven development, разработчка через тестирование) - это методология, которая предполагает сначала писать тест, а потом код реализации тестируемого метода.
BDD (Behavior-driven development, разработка через поведение) - методология, является ответвлением от TDD, она предполагает разработку, которая основана на описания поведения. Примером реализации BDD-фреймворка для Java является Cucumber.

JUnit 4

JUnit - наиболее популярный фрейморк для модульного тестирования.

Тестовые файлы должны находится по пути /src/test/java/.
В Intellij idea пакет java должен быть помечен как Test Source Root и благодоря этому не будет включаться в сборку проекта.
Все тестовые классы должны иметь приставку Test, например, так - ContactServiceTest.

Зависимость:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

Annotations

@Test – определяет что метод является тестовым.
@Before – указывает на то, что метод будет выполнятся перед каждым тестируемым методом.
@After – указывает на то что метод будет выполнятся после каждого тестируемого метода.
@BeforeClass – указывает на то, что метод будет выполнятся перед всеми тестами.
@AfterClass – указывает на то, что метод будет выполнятся после всех тестов.
@Ignore – говорит, что метод будет проигнорирован в момент проведения тестирования.
@Test(expected = Exception.class) – указывает на то, что в данном тестовом методе вы преднамеренно ожидаете Exception.
@Test(timeout = 100) – указывает, что тестируемый метод не должен занимать больше чем 100 миллисекунд.

Matchers

fail(message) – указывает на то что бы тестовый метод завалился и при этом выводилось текстовое сообщение.
assertTrue([message], boolean condition) – проверяет, что логическое условие истинно.
assertsEquals([message], expected, actual) – сравнивает два объекта методов equals().
Примечание: для массивов проверяются ссылки на объекты, а не содержание массивов. И тесты никогда не пройдут в случае если массив примитивного типа. Для сравнения значений массива специальные методы.
assertArrayEquals([message], expected, actual) - сравнение массивов по значению.
assertNull([message], object) – проверяет, что объект является пустым null.
assertNotNull([message], object) – проверяет, что объект не является пустым null.
assertSame([String], expected, actual) – сравнивает два объекта с помощью оператора ==, то есть проверяет, являются ли параметры ссылками на один и тот же объект.
assertNotSame([String], expected, actual) – проверяет, что обе переменные относятся к разным объектам.
Аргумент message, используемый в примерах выше, является опциональным.

Rules

Правила - это некое подобие утилит для тестов, которые добавляют функционал до и после выполнения теста. Есть множество встроенных правил, таких как задания таймаута, ожидаемые исключения и т.д. Для объявления правила необходимо создать поле нужного типа, входящего в пакет org.junit.rules и аннотировать такое поле с помощью @Rule:

public class JUnitTests {
    @Rule
    public Timeout timeout = new Timeout(1);

    @Test
    public void methodOneTest() throws InterruptedException {
        assertEquals("Paul", "Paul");
        Thread.sleep(10); // TestTimedOutException: test timed out after 1 milliseconds
    } 

    @Test
    public void methodTwoTest() {
        assertEquals("Sarah", "Sarah"); // true
    }
}

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

Runner

JUnit позволяет конфигурировать то, как запускается тест, с помощью @RunWith. При этом класс, указанный в аннотации должен наследоваться от Runner. Рассмотрим их поподробнее:
JUnit4.class - runner по умолчанию, необходим для запуска тестов JUnit 4;
Suite.class - запускает JUnit 4 тесты, но для настройки запускаемых тестов используется @Suite.SuiteClasses();

@Suite.SuiteClasses({ClassOneTest.class, ClassTwoTest.class})
@RunWith(Suite.class)
public class JUnitTestRunner { 
...

Parameterized.class - позволяет писать параметризированные тесты. Для этого в тест-классе объявляется статический метод возвращающий список данных, которые затем будут использованы в качестве аргументов конструктора класса.

@RunWith(Parameterized.class)
public class JUnitTestRunner {
    private String name;
    private int age;

    public JUnitTestRunner(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Test
    public void runOneTest() {
        assertEquals("Sarah", name); // true
        assertEquals(23, age); // ComparisonFailure: Expected: Sarah, Actual: Paul
    }

    @Parameterized.Parameters
    public static List<Object[]> isDataEmpty() {
        return Arrays.asList(new Object[][] {
                { "Sarah", 23 },
                { "Paul", 30 }
        });
    }
}

Theories.class - схож с предыдущим, но параметризирует тестовый метод, а не конструктор. Данные помечаются с помощью @DataPoints и/или @DataPoint, тестовый метод — с помощью @Theory:

@RunWith(Theories.class)
public class JUnitTestRunner {

    @DataPoint
    public static Object[] nullData = new Object[]{null, true};

    @DataPoints
    public static Object[][] isEmptyData = new Object[][]{
            {"", true},
            {"message", false}
    };

    @Theory
    public void runOneTest(Object... testData) {
        boolean actual = StringUtils.isEmpty((String) testData[0]);
        assertEquals(testData[1], actual);
    }
}

Результат во всех трех случаях будет успешным.

Схожие фреймворки:
TestNG - фреймворк для тестирования, схож с JUnit. Имеет такие преимущества, как: параметризированные тесты, зависимые тесты, выполнение тестов в многопоточном режиме, использование XML для конфигурирования. Фреймворк используется для модульного, интеграционного и функционального тестирования.

Mockito

Moсkito - это фреймворк, который используется для модульного тестирования и позволяет имитировать ожидаемое поведение.
Использовать Mokito в тестируемом классе можно двумя аналогичными способами:

  1. Статический импорт:
import static org.mockito.Mockito.*;

public class ContactTest {
    @Test
    public void testName() {
        ContactService contactService = mock(ContactService.class);  // создается имитация класса ContactService 
    }
}
  1. Через аннотации
@RunWith(MockitoJUnitRunner.class)
public class ContactTest {
    @Mock
    ContactService contactService; // создается имитация класса ContactService 
}

Создадим и инициализируем коллекцию, которая будет имитировать объект, получаемый из базы данных. Эта коллекция будет использоваться в последующих примерах:

    private final List<Contact> contacts = new ArrayList<Contact>() {{
        add(new Contact(1, "Sarah", "Smith"));
        add(new Contact(2, "John", "Wick"));
    }};

Класс ContactService будет представлять собой сервис, который взаимодействует с базой данных и содержит такие методы поиска контактов, как - findAll() и findByFirstName(Contact contact).

Определение поведения
when(mock).thenReturn(value) - этот метод позволяет определить возвращаемое значение при вызове метода mock с заданными параметрами:

    @Test
    public void testList() {
        when(contactService.findAll()).thenReturn(contacts); // contacts - это ArrayList
        assertEquals(contactService.findAll(), contacts); // true
    }

Если же требуется задать реакцию на вызов метода независимо от аргумента, можно воспользоваться методом any():

    @Test
    public void testName() {
        when(contactService.findByFirstName(any())).thenReturn(contacts.get(0));
        assertEquals(contactService.findByFirstName("Sarah"), contacts.get(0)); // true
    }

Mockito так же позволяет вызывать исключения при определенных условиях:

    @Test
    public void checkName() {
        when(contactService.findByFirstName(any())).thenReturn(contacts.get(0));
        Mockito.when(contactService.findByFirstName("123")).thenThrow(IllegalArgumentException.class);
        assertEquals(contactService.findByFirstName("123"), contacts.get(0)); // IllegalArgumentException
    }

Методы thenReturn и thenThrow имеют перегруженные версии, принимающие varargs:

    @Test
    public void testOne() {
        Mockito.when(contactService.findByFirstName(any()))
                .thenReturn(contacts.get(0), contacts.get(1))
                .thenThrow(IllegalArgumentException.class);

        assertEquals(contactService.findByFirstName(any()), contacts.get(0)); // true 
        assertEquals(contactService.findByFirstName(any()), contacts.get(1)); // true
        assertEquals(contactService.findByFirstName(any()), contacts.get(1)); // IllegalArgumentException
    }

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

Подсчет количества вызовов
Для проверки количества вызовов определенных методов, Mockito предоставляет следующие методы:
atLeast(int n) - не меньше n вызовов;
atLeastOnce() - хотя бы один вызов;
atMost(int n) - не более n вызовов;
times(int n) - n вызовов;
never() - вызовов не было.

Пример:

    @Test
    public void testName() {
        when(contactService.findByFirstName(any())).thenReturn(contacts.get(0));
        assertEquals(contactService.findByFirstName("Sarah"), contacts.get(0)); // true

        verify(contactService, atLeastOnce()).findByFirstName(any()); // true
        verify(contactService, never()).findAll(); // true
    }

Интерфейс Answer
Метод thenAnswer() принимает реализацию функционального интерфейса Answer<T>. Он используется когда необходимо описать сложное поведение mock-объекта:

    @Test
    public void testTwo() {
        Contact contact = new Contact(3, "Sarah", "Smith");
        when(contactService.findByFirstName("Sarah")) // определяем поведение
                .thenAnswer(new Answer<Contact>() {
                    @Override
                    public Contact answer(InvocationOnMock invocationOnMock) throws Throwable {
                        contacts.add(contact); // добавляем в коллекцию созданный выше объект Contact
                        return contact; // возвращаемым значением при обращении будет наш объект Contact
                    }
                });
        assertEquals(contactService.findByFirstName("Sarah"), contact); // true
    }

Spy
Mockito позволяет подключать к реальным объектам "шпиона" spy, который может отслеживать возвращаемые значения метода и количество вызовов метода:

    @Test
    public void testThree() {
        Calculator calculator = spy(new Calculator());
        when(calculator.sum()).thenReturn(10);

        calculator.sum(); // 1й вызов
        assertEquals(10, calculator.sum()); // 2й вызов

        verify(calculator, atLeast(2)).sum(); // true
    }

Источники:
Moсkito. Официальная документация

Hamcrest

Hamcrest - это фреймворк, который содержит в себе множество методов на соответствие (matcher`ов). Он используется для модульного тестирования в паре с JUnit или аналогичными фреймворками для тестирования.
Matcher – это выражение, тестирующее на совпадение с определенным условием входящие аргументы.

Подключаем зависимость:

<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-all</artifactId>
    <version>1.3</version>
    <scope>test</scope>
</dependency>

Для работы в классе используем статический импорт:

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

Рассмотрим простой пример сравнения:

public class HamcrestTest {
    @Test
    public void testOne() {
        assertThat("Paul", equalTo("Paul")); // true
    }
}

Единственным доступным предикатом фреймворка является функция assertThat().
Мы используем assertThat() с двумя аргументами. Первым аргументом является проверяемый объект, а вторым – matcher (в данном случае equalTo()), то есть условие проверки.

Logical
allOf() - И
anyOf() - ИЛИ
not() - НЕ

Например, у нас есть строка, и нам надо убедиться что она содержит подстроку "Pa" и заканчивается на "ul":

assertThat("Paul", anyOf(containsString("Pa"), endsWith("ul"))); // true

Text
equalToIgnoringCase() - проверка строки производится независимо от регистра.
equalToIgnoringWhiteSpace() - проверка происходит без учета лишних пробелов.
containsString(), endsWith(), startsWith() - проверка на содержание подстроки в целой строке, либо только в ее части (начале или конце).

Number
closeTo() - используется для сравнений чисел типа double с указанием погрешности.
greaterThan(), greaterThanOrEqualTo(), lessThan(), lessThanOrEqualTo() - сравнение >, >=, <, <=

Object
equalTo() - используется для сравнения двух объектов по значению.
hasToString() - проверяет возвращаемое значение метода toString() у объекта
instanceOf() - проверяет, является ли тестируемый объект экземпляром класса или его подкласса.
notNullValue(), nullValue() - проверка на null.
sameInstance() - проверяет, является ли объект тем же самым экземпляром.

Arrays
Создадим массив для дальнейшего тестирования:

Integer[] array = {1, 2, 3};

assertThat(array, not(emptyArray())); - проверка массива на пустоту
assertThat(array, arrayWithSize(3)); - проверка размера массива
assertThat(array, hasItemInArray(3)); - проверка на то, что массив содержит элемент "3"
assertThat(array, arrayContaining(1, 2, 3)); - проверяет, содержит ли массив переданные значения в строгом порядке
assertThat(array, arrayContainingInAnyOrder(3, 2, 1)); - проверка значений в произвольном порядке

Collections
Создадим коллекцию для дальнейшего тестировани:

        List<String> collection = new ArrayList<String>() {{
            add("Earth");
            add("Mars");
            add("Saturn");
        }};

assertThat(collection, hasSize(3)); - проверка размера коллекции
assertThat(collection, hasItem("Earth")); - проверка, содержит ли коллекция переданное значение
assertThat(collection, hasItems("Earth", "Mars")) - проверка, содержит ли коллекция переданные значения
assertThat(collection, contains("Earth", "Mars", "Saturn")); - проверка, содержи ли коллекция список переданных объектов в строгом порядке
assertThat(collection, containsInAnyOrder("Mars", "Earth", "Saturn")); - проверка объектов в произвольном порядке

assertThat(collection, not(empty())); - проверка коллекции на пустоту
assertThat(stringMap, equalTo(Collections.EMPTY_MAP)); - проверка Map на пустоту
assertThat(stringIterable, emptyIterable()); - проверка Iterable на пустоту

Map:
Создадим Map для тестирования:

        Map<String, String> map = new HashMap<String, String>() {{
            put("Mazda", "Red");
            put("BMW", "Black");
        }};

assertThat(map, hasEntry("BMW", "Black")); - проверка, существует ли такая запись в Map
assertThat(map, hasKey("Mazda")); - проверка, содержится ли переданный ключ в Map
assertThat(map, hasValue("Black")); - проверка, содержится ли переданное значение в Map

assertThat(collectionInt, everyItem(greaterThan(10))); - проверяет что каждый элемент коллекции больше, чем переданное значение. Используется для коллекция типа Integer.

Sugar
is() - это оболочка, которая не прибавляет дополнительного поведения, а просто стремится повысить удобночитаемость. Все примеры ниже эквивалентны:

        assertThat("Biscuit", equalTo("Biscuit"));
        assertThat("Biscuit", is(equalTo("Biscuit")));
        assertThat("Biscuit", is("Biscuit"));

Источники:
Hamcrest. Документация
Работа с Hamrest
Is Or equalsTo

Cucumber

Подлкючаем зависимости

		<dependency>
			<groupId>io.cucumber</groupId>
			<artifactId>cucumber-java</artifactId>
			<version>3.0.2</version>
		</dependency>
		<dependency>
			<groupId>io.cucumber</groupId>
			<artifactId>cucumber-junit</artifactId>
			<version>3.0.2</version>
		</dependency>
		<dependency>
			<groupId>io.cucumber</groupId>
			<artifactId>cucumber-testng</artifactId>
			<version>3.0.2</version>
		</dependency>

Selenide

Selenide - это фреймворк для автоматизированного тестирования веб-приложений на основе Selenium WebDriver.

Добавляем зависимость:

<dependency>
    <groupId>com.codeborne</groupId>
    <artifactId>selenide</artifactId>
    <version>5.5.1</version>
    <scope>test</scope>
</dependency>

Используем статическим импорт:

import static com.codeborne.selenide.Selenide.*;
import static com.codeborne.selenide.Condition.*;

Рассмотрим простой пример:

    @Test
    public void searchTest() {
        open("https://yandex.ru/");
        $(By.name("text")).setValue("Selenide").pressEnter();
        $$("li.serp-item").shouldHaveSize(10);
        $("li.serp-item div h2 a div.organic__url-text")
                .shouldHave(text("Selenide: лаконичные и стабильные UI тесты на Java"));
    }

Что здесь происходит:
Мы открывем браузер и переходим по url;
Вводим в поисковую строку запрос и нажимаем Enter;
Проверяем результат: в первом случае - список результатов должен быть равным 10, а во-втором - первый результат должен иметь заданное нами значение;
Важно отметить, что поиск элементов завязан на атрибутах html-тегов странички, а это значит, что при изменении верстки, тесты могут перестать работать.

Основные методы:
open(url) - открывает браузер по переданному url.
$(sccSelector) - возвращает первый найденный по CSS селектору объект на странице.
$$(cssSelector) - возвращает коллекцию найденных по CSS селектору объектов на страницу.
$(By) - возвращает первый найденный элемен по локатору типа By.
$$(By) - возвращает коллекцию элементов по локатору типа By.
Например: $(By.name("text")), мы ищем элемент, у которого в качестве атрибута name установлено значение "text".

После получения объекта (типа SelenideElement), над ним можно совершать одно или несколько действий, например: $(By.name("text")).setValue("Selenide").pressEnter();
Либо проверить какое-то условие: $$("li.serp-item").shouldHaveSize(10);

Методы поиска внутренних элементов:
find(cssSelector)/$(cssSelector)
findAll(cssSelector)/$$(cssSelector)
find(By)/$(By)
findAll(By)/$$(By)
Здесь $ и $$ просто алиасы для соответсвтующих команд.

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

        open("https://google.com/");
        $(By.name("q")).setValue("Selenide").pressEnter();
        $(".bkWMgd").find(".srg").findAll(".g")
                .shouldHaveSize(4);

Методы проверки состояния:
should(condition)/shouldBe(condition)/shouldHave(condition)
shouldNot(condition)/shouldNotBe(condition)/shouldNotHave(condition)

Применение состояний на примере поискового запроса:

        open("https://google.com/");
        $(By.name("q")).should(exist);
        $(By.name("q")).setValue("Selenide").pressEnter();
        $(By.name("q")).shouldHave(value("Selenide"));
        $(".rc").$(".r a")
                .shouldHave(text("Selenide: лаконичные и стабильные UI тесты на Java"));

Со всеми возможными состояниями можно ознакомиться в классе com.codeborne.selenide.Condition или в JavaDoc. А реализовав подкласс com.codeborne.selenide.Condition, можно создавать свои условия.

Основные методы действий над элементами:
click(), doubleClick(), setValue(String)/val(String), pressEnter, pressEscape, pressTab.
Например: $(By.name("q")).setValue("Selenide").pressEnter();

Основные методы получения статусов элементов и значений их атрибутов:
getValue()/val(), text()
Например: $(By.name("q")).val("Selenide")

В этом разделе описана лишь небольшая часть возможностей фреймворка Selenide, для более детального ознакомления можно воспользоваться ссылками из источников.

Источники:
Официальная документация
Selenide gitbooks
Конфигурирования, Настройка Браузера, Page Object и Билд скрипты

PODAM

About

PODAM - это инструмент для автоматического заполнения POJO-классов данными.
Этим можно воспользоваться при разработке интеграционных тестов и избавиться от анти-паттерна чрезмерной инициализации (excessive setup).

Dependencies

Maven

        <dependency>
            <groupId>uk.co.jemos.podam</groupId>
            <artifactId>podam</artifactId>
            <version>7.1.0.RELEASE</version>
        </dependency>

Setup

Для дальнейших примеров создадим класс Country, который будет содержать в себе коллекцию типа City:

@Data
public class Country {
    private String name;
    private List<City> cities;

    @Data
    private class City {
        private String name;
        private int population;
    }
}

Example

Создадим экземпляр Podam фабрики и заполним класс Country данными:

        PodamFactory factory = new PodamFactoryImpl();
        Country country = factory.manufacturePojo(Country.class);

        System.out.println(country); // Country(name=Qbn9xVjV0g, cities=[City(name=NoQaDM93yn, population=214630906) ... )

Готово! У нас сгенерировалась модель Country, которая содержит коллекцию City, состоящую из нескольких элементов.

Custom Annotations

У Podam существует ряд настраиваемых аннотаций.

PodamStrategyValue
Аннотация @PodamStrategyValue - позволяет изменить стратегию генерации данных на уровне атрибутов. Для этого необходимо реализовать интерфейс AttributeStrategy<T>, где T - тип возвращаемого значения.
Например, я хочу что бы в наименовании страны мне выводились только существующие страны. Рассмотрим на примере:

public class CountryNameStrategy implements AttributeStrategy<String> {
    
    @Override
    public String getValue(Class aClass, List list) {
        return CountryNames.getRandomCountry();
    }

    private enum CountryNames {
        RUSSIA, CANADA, USA, CHINA, BRAZIL, AUSTRALIA;

        private static final List<CountryNames> countryList =
                Collections.unmodifiableList(Arrays.asList(values()));
        private static final int size = countryList.size();
        private static final Random random = new Random();

        public static String getRandomCountry() {
            return countryList.get(random.nextInt(size)).toString();
        }
    }
}

Теперь осталось только применим аннотацию к нужному атрибуту класса Country и указать класс с собственной стратегией:

public class Country {
    @PodamStrategyValue(CountryNameStrategy.class)
    private String name;
}

PodamBooleanValue
Аннотация @PodamBooleanValue позволяет устанавливать логическое значение для атрибута:

public Person {
    @PodamBooleanValue(boolValue = true)
    private boolean isMarried;
}

Numbers Value
Аннотации @PodamByteValue, @PodamShortValue, @PodamCharValue, @PodamIntValue, @PodamLongValue, @PodamFloatValue и @PodamDoubleValue позволяют настраивать одноименные атрибуты:

public Numbers {
    @PodamIntValue(numValue = 5)
    private int intFieldPreciseValue;

    @PodamIntValue(minValue = 0)
    private int intFieldMinValue;

    @PodamIntValue(maxValue = 1000)
    private int intFieldMaxValue;

    @PodamIntValue(minValue = 0, maxValue = 1000)
    private int intFielMinAndMaxValue;
}

PodamStringValue
Аннотация @PodamStringValue позволяет настраивать строковый атрибут.

public Person {
    @PodamStringValue(strValue = "Sarah")
    private String name;

    @PodamStringValue(length = PodamTestConstants.STR_ANNOTATION_TWENTY_LENGTH)
    private String lastName;
}

PodamExclude
Аннотация @PodamExclude позволяет исключить атрибут из заполнения данными:

public Person {
    @PodamExclude
    private String name;
}

Sources

PODAM документация
Habr: PODAM Java объекты для Unit-тестирования
Habr: Анти-паттерны Test Driven Development

⚠️ **GitHub.com Fallback** ⚠️