JUnit

Введение

Тестирование – это процесс проверки функционала программы с целью подтверждения того, что она работает в соответствии с определенными требованиями. Unit-тестирование – это тестирование, которое пишется, непосредственно, на уровне разработчика (тестирование определенной сущности – метод или класс). Это крайне важный этап разработки ПО, который помогает создавать качественный продукт.

Юнит-тестирование делится на две большие группы:

  • Ручное тестирование
  • Автоматизированное тестирование
Ручное тестирование Автоматизированное тестирование
Ручное выполнение тестов без помощи каких-либо средств. Использование специальных средств для автоматизированного тестирования
Не программируется Нет возможности для написания сложных тестов для тестирования сложных моделей поведения. Программируется Тестировщики могут написать сложные тесты для тестирования сложных моделей программирования
Низкая надежность Ручное тестирование имеет низкую надежность, так как крайне подвержено влиянию человеческого фактора. Высокая надежность Автоматизированное тестирование точное и надежное.
Большие затраты времени Связано с тем, что человек имеет крайне ограниченные возможности в скорости работы. Быстро Автоматизированные тесты выполняются на порядок быстрее, чем это может сделать человек.

JUnit

JUnit – это фреймворк, разработанный для тестирования программ, написанных с использованием технологии Java. Он лежит в основе TDD (Test-Driven Development) и входит в семейство фрейморков для тестирования xUnit.

Главная идея данного фреймворка – “сначала тесты, потом код”. Это означает, что сначала мы определяем, что должно получиться в результате работы того или иного куска кода и пишем тесты, которые проверяют идентичность результата с требуемым, после чего пишем сам кусок кода, который и будем тестировать. Данный подход увеличивает эффективность работы разработчика и позволяет писать более стабильный код. В результате этого мы получаем меньшее количество времени, которое затрачивается на отладку программы.


Свойства JUnit

  • Фреймворк с открытым исходным кодом, который используется для написания и выполнения тестов.
  • Позволяет писать код более быстро и качественно.
  • Крайне прост в использовании.
  • Поддерживает аннотации для идентификации методов.
  • Поддерживает утверждения для тестирования получаемых результатов.
  • Тесты могут быть организованы в связки тестов (test suites).
  • Имеет визуальную индикацию состояния тестов (красные – не пройдены, зелёные – пройдены).

Тестовый случай

Тестовый случай (Test Case) в юнит тестировании – это часть кода, которая проверяет, что другая часть кода (в частности – метод) работает в соответствии с определенными требованиями.

Формально описанный тестовый случай характеризуется известными входными данными и ожидаемым выводом программы, который известен до начала выполнения теста.

Необходимо создавать, как минимум, два тестовых случая для каждого требования – положительный и отрицательный. Если требование имеет подтребования, каждое из них должно тестироваться отдельно.

Простой пример

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

Для начала создадим новый проект:

После успешной сборки мы получим проект со следующей структурой:

Приступим к написанию тестов:
Создадим простой класс ArrayHolder, который будет содержать массив целых чисел:

package net.proselyte.tutorials;

import java.util.Arrays;

/**
 * This is simple class that contains array of integers
 *
 * @author Eugene Suleimanov
 * @version 1.0
 */
public class ArrayHolder {
    private int[] integers;

    public ArrayHolder() {
        this.integers = new int[]{1, 2, 3, 4, 5};
    }

    public int[] getIntegers() {
        return integers;
    }

    public void setIntegers(int[] integers) {
        this.integers = integers;
    }

    @Override
    public String toString() {
        return Arrays.toString(integers);
    }
}

По умолчанию создается экземпляр класса с массивом [1,2,3,4,5].
Создадим тестовый класс для проверки работы ArrayHolder – ArrayHolderTests:

package net.proselyte.tutorials;

import org.junit.Test;

import static org.junit.Assert.assertEquals;

/**
 * Test class for {@link ArrayHolder}
 *
 * @author Eugene Suleimanov
 * @version 1.0
 */

public class ArrayHolderTests {
    ArrayHolder arrayHolder = new ArrayHolder();

    @Test
    public void shouldCreateDefaultArrayTest() {
        assertEquals(arrayHolder.toString(), "[1, 2, 3, 4, 5]");
    }
}

Удалим классы, созданные по умолчанию и получим следующую структуру проекта:

В результате работы класса ArrayHolderTests мы получим следующий результат:

В данном классе мы тестировали, что выводит метод toString() класса ArrayHolder. Метод assertEquals проверяет, идентичны ли заданное значение (правое) и значение, полученное в результате работы метода (левое).

На этом мы заканчиваем обзор первого простого примера.

Архитектура

Unit – Это регрессивный фреймворк, предназначенный для тестирования, который используется разработчиками для реализации юнит-тестирования при разработке на языке Java. Он позволяет ускорить процесс написания программ и улучшить их качество.

JUnit может быть легко итегрирован в:

  • Maven
  • Ant

Свойства JUnit

Фреймворка JUnit состоит из следующих ключевых групп:

  • Fixtures (каркас)
  • Test Suites (группа тестов)
  • Test runners (сущности, которые выполняют тесты)
  • Классы JUnit

Разберем каждую из них отдельно:


Fixtures

Это фиксированное состояние множества (набора) объектов, которые служат базисом для выполнения тестов. Их цель – гарантировать, что существует хорошо известное и фиксированное окружение, в котором тесты выполняются таким образом, что результаты повторяемы. Он включает в себя следующие методы:

  • setUp()

    Выполняется перед каждым запуском тестов

  • tearDown()

    Выполняется после каждого тестового метода


Test Suite

Это группа, состоящая из нескольких тестов, которые запускаются вместе. Для запуска групповых тестов используются аннотации @RunWithи@Suite.


Test Runners

Используются для выполнения тестовых случаев.


Классы JUnit

Классы JUnit играют огромную роль и используются для написания тестов и их выполнения. Наиболее важные классы указаны ниже:

  • Assert
    Содержит множество методов утверждений.

  • TestCase
    Содержит тестовые случаи, который определяют каркас для выполнения нескольких тестов.

  • TestResult
    Содержит методы для хранения данных, полученных в результате выполнения тестовых случаев.

API

Базовый функционал фреймворка JUnit содержится в классах пакета junit.framework, который содержит все базовые классы, наиболее важные из которых:

  • Assert

    Набор методов-утверждений

  • TestCase

    Определяет каркас для выполнения тестов (fixtures)

  • TestResult

    Хранит результаты выполнения тестовых случаев

  • TestSuite

    Набор тестов

Рассмотрим по очереди эти классы и их базовые методы:


Класс Assert

Данный класс содержит набор методов-утверждений, которые крайне помогают разработчикам при написании тестов. Записываются только те утверждения, которые оказываются ложными (false). Ниже приведен список наиболее важных методов данного класса:

Метод и описание
1 void assertEquals(boolean expected, boolean actual) Проверяет равенство значений двух примитивных типов данных.
2 void assertFalse(boolean condition) Проверяет, является ли утверждение ложным.
3 void assertNotNull(Object object) Проверяет, что объект не является null
4 void assertNull(Object object) Проверяет, что данный объект не является типом null
5 void assertTrue(boolean condition) Проверяет, что указанное выражение является истинным (true)
6 void fail() Проваливает тест без вывода сообщений.

Рассмотрим пример использования данного класса:

Откроем наш проект JUnitTutorial и создадим простой класс Calculator, который только считает сумму двух чисел:

package net.proselyte.tutorials;

/**
 * Simple class that represents very primitive calculator
 *
 * @author Eugene Suleimanov
 * @version 1.0
 */
public class Calculator {

    private int firstNumber;

    private int secondNumber;

    public Calculator() {
    }

    public Calculator(int firstNumber, int secondNumber) {
        this.firstNumber = firstNumber;
        this.secondNumber = secondNumber;
    }

    public int getFirstNumber() {
        return firstNumber;
    }

    public void setFirstNumber(int firstNumber) {
        this.firstNumber = firstNumber;
    }

    public int getSecondNumber() {
        return secondNumber;
    }

    public void setSecondNumber(int secondNumber) {
        this.secondNumber = secondNumber;
    }

    public int calculateSum() {
        return (firstNumber + secondNumber);
    }
}

Напишем простой тест CalculatorTests:

package net.proselyte.tutorials;

import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

/**
 * Test class for {@link Calculator}
 *
 * @author Eugene Suleimanov
 * @version 1.0
 */
public class CalculatorTests {
    private Calculator calculator = new Calculator(10, 20);

    @Test
    public void shouldCreateCalculatorInstance() {
        assertNotNull(calculator);
    }

    @Test
    public void shouldReturnCorrectSum() {
        assertEquals(30, calculator.calculateSum());
    }
}

В методе shouldCreateCalculatorInstance() мы проверяем, что экземпляр Calculator действительно создан и не является типом null.
В методе shouldReturnCorrectSum() мы проверяем, что метод calculateSum класса Calculator возвращает корректное значение (10 + 20 = 30).

В результате работы класса CalculatorTests мы получим следующий результат:


Класс TestCase

Классorg.junit.TestCaseопределяет ряд каркас (fixture) для выполнения нескольких тестов. Наиболее использованые методы данного класса приведены ниже:

Методы и описание
1 int countTestCases()Возвращает количество выполнняемых тестовых случаев (test cases).
2 TestResult createResult()Создаёт экземпялр класса TestResult по умолчанию.
3 String getName()Вовращает имя тестового случая
4 TestResult run()Выполняет тест, собирая результаты с помощью стандартного объекта TestResult.
5 void run(TestResult result)Выполняет тестовый случай и собирает результаты в экземпляре TestResult
6 void setName(String name)Задаёт имя тестовому случаю
7 void setUp()Устанавливает каркас (fixture), например, открывает сетевое соединение
8 void tearDown()Уничтожает каркас (fixture), например, закрывает сетевое соединение

Рассмотрим небольшой пример:

Мы будем использовать уже созданный класс Calculator:

package net.proselyte.tutorials;

/**
 * Simple class that represents very primitive calculator
 *
 * @author Eugene Suleimanov
 * @version 1.0
 */
public class Calculator {

    private int firstNumber;

    private int secondNumber;

    public Calculator() {
    }

    public Calculator(int firstNumber, int secondNumber) {
        this.firstNumber = firstNumber;
        this.secondNumber = secondNumber;
    }

    public int getFirstNumber() {
        return firstNumber;
    }

    public void setFirstNumber(int firstNumber) {
        this.firstNumber = firstNumber;
    }

    public int getSecondNumber() {
        return secondNumber;
    }

    public void setSecondNumber(int secondNumber) {
        this.secondNumber = secondNumber;
    }

    public int calculateSum() {
        return (firstNumber + secondNumber);
    }
}

И создадим новый тестовый класс:

CalculatorAdvancedTests

package net.proselyte.tutorials;

import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.assertEquals;

/**
 * Test class for {@link Calculator}
 *
 * @author Eugene Suleimanov
 * @version 1.0
 */
public class CalculatorAdvancedTests {

    private Calculator calculator = new Calculator();

    private int firstNumber;
    private int secondNumber;

    @Before
    public void setUp() {
        firstNumber = 100;
        secondNumber = 200;
        calculator.setFirstNumber(firstNumber);
        calculator.setSecondNumber(secondNumber);
    }

    @Test
    public void shouldCreateCalculatorWithSetUpValues() {
        assertEquals(firstNumber, 100);
        assertEquals(secondNumber, 200);
    }

    @Test
    public void shouldReturnCorrectSum() {
        assertEquals(300, calculator.calculateSum());
    }
}

В методе setUp() мы указываем операции, которые запускаются перед каждым тестом.
В методе shouldCreateCalculatorWithSetUpValues() мы проверяем, что значения целых чисел в экземпляре calculator установлены в методе setUp (100 и 200).
А метод shouldReturnCorrectSum() проверяет, что метод calculateSum корректно вычисляет сумму элементов.
В результате работы данного класса мы получим следующий результат:


Класс TestResult

Класс org.junit.TestResult хранит результаты работы test case. Он отличает failures (сбои) и errors (ошибки). Сбои ожидаются и проверяются с помощью утверждений. Ошибки же, в свою очередь, не ожидаются (например OutOfMemoryError).

Ниже приведены наиболее важные методы класса TestResult:

Методы и описания
1 void addError(Test test, Throwable t) Добавляет ошибку в список ошибок
2 void addFailure(Test test, AssertionFailedError t) Добавляет сбой в список сбоев.
3 void endTest(Test test) Выводит результаты завершенных тестов.
4 int errorCount() Возвращает количество обнаруженных ошибок.
5 Enumeration<TestFailure> errors() Возвращает перечисление для ошибок.
6 int failureCount() Возвращает количество обнаруженных сбоев.
7 void run(TestCase test) Запускает TestCase.
8 int runCount() Возвращает количество запущенных тестов.
9 void startTest(Test test) Выводит результат теста, который передается в параметре.
10 void stop() Останавливает запущенный тест.

Класс TestSuite

Класс org.junit.TestSuite представляет собой группу тестов, которые могут быть запущены вместе.

Ниже приведены основные методы данного класса:

Методы и описание
1 void addTest(Test test) Добавляет тест в suite.
2 void addTestSuite(Class<? extends TestCase> testClass) Добавляет тесты из указанного тестового класса.
3 int countTestCases() Возвращает количество тестовых случаев, которые будут запущены в данном тесте.
4 String getName() Возвращает имя suite.
5 void run(TestResult result) Запускает тесты и записывает их результаты в TestResult.
6 void setName(String name)Указывает имя suite.
7 Test testAt(int index) Возвращает тест с указанным индексом
8 int testCount() Возвращает количество тестов в данном сьюте.
9 static Test warning(String message) Возвращает тест, который не пройдет и выводит сообщение

Создадим простой TestSuite, в который добавим имеющиеся тестовые классы:

TestSuiteDemo.java

package net.proselyte.tutorials;

import junit.framework.TestResult;
import junit.framework.TestSuite;
import org.junit.Test;

/**
 * Demonstration of {@link TestSuite}
 *
 * @author Eugene Suleimanov
 * @version 1.0
 */
public class TestSuiteDemo {
    TestSuite testSuite = new TestSuite(CalculatorTests.class, CalculatorAdvancedTests.class);
    TestResult result = new TestResult();

    @Test
    public void testSuiteInAction() {
        testSuite.run(result);
        System.out.println("Amount of test cases: " + testSuite.countTestCases());
        testSuite.setName("SimpleTestSuite");
        System.out.println("Name of Test Suite: " + testSuite.getName());
    }
}

В начале мы создаем экземпляр TestSuite и добавляем в него классы CalculatorTests и CalculatorAdvancedTests.
После этого мы выводим количество TestCases, задаем TestSuite имя и выводим его в консоль.
В результате работы данного класса мы получим следующий результат:

Написание тестов

Мы создадим POJO класс, класс с его бизнес-логикой и, непосредственно, тестовый класс.

Создадим пакет developer

Создадим простой POJO (Plain Old Java Object) класс Developer.java

package net.proselyte.tutorials.junit.developer;

import java.util.Currency;

/**
 * Simple JavaBean domain object that represents a Developer
 *
 * @author Eugene Suliemanov
 * @version 1.0
 */
public class Developer {
    private String firsName;

    private String lastName;

    private String specialty;

    private int salary;

    public Developer() {
    }

    public Developer(String firsName, String lastName, String specialty, int salary) {
        this.firsName = firsName;
        this.lastName = lastName;
        this.specialty = specialty;
        this.salary = salary;
    }

    public String getFirsName() {
        return firsName;
    }

    public void setFirsName(String firsName) {
        this.firsName = firsName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getSpecialty() {
        return specialty;
    }

    public void setSpecialty(String specialty) {
        this.specialty = specialty;
    }

    public int getSalary() {
        return salary;
    }

    public void setSalary(int salary) {
        this.salary = salary;
    }

    @Override
    public String toString() {
        return "Firs Name: " + firsName +
                "\nLast Name: " + lastName +
                "\nSpecialty:" + specialty +
                "\nSalary: $" + salary / 100 + "." + salary % 100;
    }
}

Класс DeveloperLogic.java

package net.proselyte.tutorials.junit.developer;

/**
 * Business logic for class {@link Developer}
 *
 * @author Eugene Suliemanov
 * @version 1.0
 */
public class DeveloperLogic {

    public int calculateHourRate(Developer developer) {
        return developer.getSalary() / 20 / 8;
    }

    public int calculateAnnualSalary(Developer developer) {
        return developer.getSalary() * 12;
    }
}

Данный класс вычисляет часовую ставку и годовой доход разработчика.

Создадим тестовый класс DeveloperTests.java

package net.proselyte.tutorials;

import net.proselyte.tutorials.junit.developer.Developer;
import net.proselyte.tutorials.junit.developer.DeveloperLogic;
import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.assertEquals;

/**
 * Test class for class {@link Developer}
 *
 * @author Eugene Suliemanov
 * @version 1.0
 */
public class DeveloperTests {
    Developer developer = new Developer();
    DeveloperLogic logic = new DeveloperLogic();

    @Before
    public void setUp(){
        developer.setFirsName("Simple");
        developer.setLastName("Developer");
        developer.setSpecialty("Java");
        developer.setSalary(2000_00);
    }

    @Test
    public void shouldCreateDeveloperInstanceTest(){
        assertEquals("Simple", developer.getFirsName());
        assertEquals("Developer", developer.getLastName());
        assertEquals("Specialty", developer.getSpecialty());
        assertEquals(2000_00, developer.getSalary());
    }

    @Test
    public void shouldCalculateAnnualSalaryTest(){
        assertEquals(24_000_00, logic.calculateAnnualSalary(developer));
    }

    @Test
    public void shouldCalculateHourRateTest(){
        assertEquals(12, logic.calculateHourRate(developer)); 
    }
}

Метод shouldCreateDeveloperInstanceTest() проверяет работу getters и setters.
Метод shouldCalculateAnnualSalaryTest() проверяет корректность вычисления годового дохода разработчика.
Метод shouldCalculateHourRateTest() проверяет правильность вычисления часовой ставки разработчика.

В результате работы данного теста, мы получим следующий результат:

Утверждения

При написании тестов мы практически всегда используем утверждения, большинство из которых находятся в классеAssert.

Данный класс содержит набор утверждений, которые крайне облегчают процесс тестирования. Записываются только те утверждения, которые не прошли.

Ниже приведены наиболее важные методы класса Assert:

Методы и описание
1
2 void assertTrue(boolean expected, boolean actual)Проверяет истинность утверждения
3 void assertFalse(boolean condition)Проверяет ложность утверждения
4 void assertNotNull(Object object)Проверяет, что объект не являетсяnull
5 void assertNull(Object object)Проверяет, является ли объектnull
6 void assertSame(boolean condition)Проверяет, ссылаются ли ссылки двух объектов на один и тот же объект.
7 void assertNotSame(boolean condition)Проверяет, не ссылаются ли ссылки двух объектов на один и тот же объект.

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

Класс AssertionTests.java

package net.proselyte.tutorials;

import org.junit.Assert;
import org.junit.Test;

import static org.junit.Assert.*;

/**
 * Simple test class that demonstrates {@link Assert} in action
 *
 * @author Eugene Suleimanov
 * @version 1.0
 */
public class AssertionTests {

    @Test
    public void shouldDemonstrateAssertionsTest() {
        Integer first = 1;
        Integer second = 2;
        Integer repeatFirst = 1;

        Integer[] expectedIntegerArray = {1, 2, 3, 4, 5};
        Integer[] resultIntegerArray = {1, 2, 3, 4, 5};

        assertEquals(first,repeatFirst);

        assertNotNull(second);

        assertSame(first, repeatFirst);

        assertArrayEquals(expectedIntegerArray, resultIntegerArray);
    }

}

В результате работы данного теста мы получим следующий результат:


Аннотации

Здесь и в предыдущих примерах мы использовали аннотации.

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

  • игнорируют методы и классы
  • запускаются перед и после всех методов
  • запускаются до и после всех тестовых методов

Сейчас мы подробно рассмотрим наиболее часто используемые из них:

Аннотации и описание
1 @Test Указывает, что данный метод (public void) может быть запущен как тестовый случай.
2 @Before Методы с данной аннотацией запускаются перед каждым тестом.
3 @After Методы с данной аннотацией запускаются после каждого теста.
4 @BeforeClass Запускается один раз перед запуском любого тестового метода в классе (метод должен быть статическим).
5 @AfterClass Запускается один раз после запуска любого тестового метода в классе (метод должен быть статическим).
6 @Ignore Тестовые методы с данной аннотацией не будут выполнены.

Рассмотрим следующий пример:
Создадим класс AnnotationTests.java

package net.proselyte.tutorials.junit;

import org.junit.*;

/**
 * Test class to demonstrate annotations in action
 *
 * @author Eugene Suliemanov
 * @version 1.0
 */
public class AnnotationTests {
    @BeforeClass
    public static void beforeClass(){
        System.out.println("This method has been executed first...");
    }

    @AfterClass
    public static void afterClass(){
        System.out.println("This method has been executed last...");
    }

    @Before
    public void before(){
        System.out.println("Before each test");
    }

    @After
    public void after(){
        System.out.println("After each test");
    }

    @Test
    public void simpleTest(){
        System.out.println("This is simple test");
    }

    @Ignore
    @Test
    public void ignoreTest(){
        System.out.println("This test will be ignored");
    }
}

В результате работы данного класса мы получим следующий результат:

На этом мы заканчиваем изучение фреймворка JUnit.

results matching ""

    No results matching ""