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.