Mockito довольно нужная штука и к сожалению немного сложноватая для понимания, поэтому здесь подробно.
Часто бывает так, что подлежащий тестированию метод имеет какие-то зависимости. Например, в нем производиться подключение к базе данных либо в тестируемом методе используются классы или методы со сложной структурой. Это делает наш тестируемый метод сложно поддерживаемым, то есть если перенести его на другой компьютер, на котором нет базы от которой зависит метод или каких либо классов, которые используются в этом методе, то без этих зависимостей метод протестировать не получиться.
Было бы весьма наивно и не практично создавать новую базу и прописывать sql запросы к ней и т.д. чтобы протестировать этот метод на этом другом компьютере, ведь можно просто заменить зависимости в тестируемом методе на то, что эти зависимости могли бы вернуть.
Всё, что нам нужно – это протестировать сам конкретный метод, а корректность работы зависимостей в нем можно просто сымитировать.
mock и spy. Разница
Для имитации зависимостей есть mock, spy и stub.
Разница в mock и spy будет понятна когда дойдем до stub, а пока просто попробуем понять их суть:
mockсоздает фейковый экземпляр какого-то класса, который является зависимостью в тестируемом методе и все методы и поля этого фейкового экземпляра возвращают null или 0 или false в зависимости от типов переменных или типов возвращаемых значений методов.
Благодаря этому мы сможем протестировать метод, который мы хотим, используя в нем фейковые методы фейкового экземпляра. Экземпляр-зависимость метод которого, например, должен возвращать данные из БД в тестируемом методе, ясное дело, не будет возвращать данные из БД поскольку будет использован метод фейкового экземпляра зависимости и этот метод вернет пустоту. Зато хоть можем протестировать.
spy же в отличие от mockне создает никаких экземпляров, он работает с существующим экземпляром-зависимостью и с содержимым этого существующего экземпляра. То есть с его полями и методами просто по факту применения к нему spy ничего не происходит.
И здесь подходим к stub. с помощью стаба мы можем подменить то, что возвращают нам методы mock и spy экземпляров на то, что мы хотим.
stubэто вот такая конструкция:
when (вызывается метод mock или spy экземпляра).thenReturn(вот такие значения, которые мы хотим).
То есть в скобочках у when указывается метод из mock или spy экземпляра, возвращаемые значения которого, будут подменяться при его вызовах в тестируемом методе на те значения, которые указываются в скобочках у thenReturn.
stub может использоваться, как в mock, так и в spy экземпляре.
Очевидно пустой mock экземпляр становиться уже не таким пустым поскольку с помощью стаба некоторые методы в нем будут возвращать уже не пустоту, а поддельные значения, а spy же это буквально шпион (spy с англ. шпион) он забирается в нормальный экземпляр (то есть экземпляр не пустой) и заменяет возвращаемые значения некоторых методов в нем.
mock
Давайте для начала создадим класс, который будет извлекать данные из базы данных с помощью средств JDBC.
Пример программы:
import java.util.*;
import java.sql.*;
public class DbClass {
//В этой функции просто извлекаем данные
//о коммутаторах из базы, запихиваем их
//в список и возвращаем этот список.
public List< String > getCommutaorsDataFromDB() {
List< String > commutatorsDataFromDB =
new ArrayList< String >();
try {
Connection connection =
DriverManager.getConnection(
“jdbc:mysql://localhost/storage”,
“root”, “07031998MSD”);
Statement statement =
connection.createStatement();
ResultSet resultSet =
statement.executeQuery(
“SELECT * FROM commutator”);
while (resultSet.next()) {
commutatorsDataFromDB.add(
resultSet.toString());
}
} catch(SQLException e){}
return commutatorsDataFromDB;
}
//это метод возведения в степень он здесь
//просто чтобы показать разницу spy и mock
public int pow(int num, int power){
int result=1;
for(int i=0; i < power; i++){result*=num;}
return result;
}
}
Этот класс вместе с методом getCommutatorsDataFromDB и будет зависимостью, которую мы будем подменять в методе, который мы будем тестировать.
Теперь давайте создадим класс в котором будет метод, который мы будем тестировать. В этом методе, как раз, будет использоваться зависимость. То есть метод getCommutatorsDataFromDB, который возвращает данные из БД. Но этой БД, как мы помним, нетна данном компьютере. Поэтому нам придется подменять возвращаемые значения этой зависимости.
Пример программы:
import java.util.*;
public class MockExample {
private DbClass dbClass;
MockExample(DbClass dbClass){
this.dbClass = dbClass;
}
//Этот метод будем тестировать.
//В этой функции вызывается getCommutaorsDataFromDB.
//то есть TestMethod имеет зависимость on dbClass
//то есть от БД, к которой представим что у нас
//нет доступа на текущем компьтере. Значит
//возвращаемые значения метода getCommutaorsDataFromDB
//нужно будет подменить
//то есть в dbClass объект, который здесь либо будем
//отправлять шпиона spy, либо будем делать фейковый
//объект dbClass с помощью mock меняя
//в нем getCommutaorsDataFromDB.
public List< String > getDbDataForTestWithPlusses() {
List< String > commutatorsDataFromDB =
dbClass.getCommutaorsDataFromDB();
//Пусть этот метод будет добавлять плюс в конец
//названия каждого коммутатора в списке коммутаторов,
//который мы извлекли из из БД и этот обновленный список
//с плюсами будет возвращать этот метод.
for(int i=0; i < commutatorsDataFromDB.size(); i++){
commutatorsDataFromDB.set(
i, commutatorsDataFromDB.get(i)+"+" );
}
return commutatorsDataFromDB;
}
}
Теперь создаем класс тестер в котором создаем тестовый метод, который будет тестировать метод с подмененной зависимостью.
Пример программы:
import org.junit.Assert;
import org.junit.Test;
import java.util.*;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class TestMock {
//Создаем фейковый объект класса DbClass
//с пустым содержимым.
//Будет использоваться в тестируемом методе
//с подмененным методом getCommutaotrsDataFromDB.
DbClass mockDbClass = mock(DbClass.class);
@Test
public void TestMethod() throws Exception {
//Передаем фейковый объект класса DbClass
//в объект класса MockExample. Там он будет
//использоваться вместо настоящего объекта-зависимости,
//который использовался бы
//для извлечения коммутаторов из БД.
MockExample mockExample = new MockExample(mockDbClass);
//Вместо того что должен выводить
//метод getCommutaotrsDataFromDB()
//получить данные из БД в виде списка,
//будут выводить список ниже.
List< String > fakeCommutaotrsDataFromDB =
Arrays.asList(“comm1”, “comm2”, “comm3”);
List< String > fakeCommutatorsDataFromDB =
Arrays.asList(“comm1”, “comm2”, “comm3”);
//Ниже мы говорим что когда вызывается метод
//getCommutatorsDataFromDB у объекта mockDbClass
//то выводится список fakeCommutatorsDataFromDB
//вместо настоящего списка коммутаторов из БД.
//Метод getCommutatorsDataFromDB в фейковом объекте
//mockDbClass ясное дело будет использоваться
//в методе getDbDataForTestWithPlusses объекта
//mockExample.
when(mockDbClass.getCommutatorsDataFromDB()).thenReturn(
fakeCommutatorsDataFromDB);
//Метод getCommutatorsDataFromDB в фейковом
//объекте mockDbClass мы подменили.
//Все другие методы в этом классе все еще
//должны возвращать либо пустую либо ноль,
//либо false. Проверим метод pow.
System.out.println(mockDbClass.pow(4,2));
//Теперь тестируем метод getDbDataForTestWithPlusses
//в объекте mockExample с фейковой зависимостью в нем
//с подмененным методом getCommutatorsDataFromDB.
Assert.assertEquals(Arrays.asList(“comm1+”, “comm2+”,
“comm3+”), mockExample.getDbDataForTestWithPlusses());
}
}
Для того чтобы скомпилировать и запустить класс тестер, необходимо для начала скачать такие библиотеки:
junit-4.13.2.jar
hamcrest-core-1.3.jar
mockito-core-4.11.0.jar
byte-buddy-1.12.20.jar
byte-buddy-agent-1.12.20.jar
Всех их довольно легко найти и скачать в интернете, например на mvnrepository.com.
Итак компилируем и запускаем:
Видим, что сначала вывело 0, что значит, что объект mockDbClassтаки был фейковым, то есть пустым, так как его метод pow вернул 0, а не 16.
В то же время, очевидно, подмена метода getCommutatorsDataFromDB в этом объекте была успешна, так как программа говорит, что всё ОК, что значит, что фактическое значение совпало с ожидаемым. То есть тестируемый метод вернул имена коммутаторов с добавленным плюсом в конце, как и нужно было.
Еще пару слов, чтобы вы окончательно уловили суть. Мы проверяли функционал тестируемого метода. Функционалом тестируемого метода было добавление плюсов к значениям в списке. При этом нам было абсолютно не важно, что за значения в этом списке. И именно потому, что нам было абсолютно не важно, что за значения в этом списке, мы и добавили туда три любых значения – “comm1”, “comm2”, “comm3” и на них тестировали функционал тестируемого метода.
spy
Теперь создадим такой же класс тестер только уже со spy вместо mock.
Пример программы:
import org.junit.Assert;
import org.junit.Test;
import java.util.*;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
public class TestMock {
// используя spy создаётся
// нормальный объект класса DbClass
// уже не фейковый то есть не пустой,
// но в нем может быть подменённый метод.
// Будет использоваться в тестируемом методе
// с подменённым методом getCommutaorsDataFromDB.
DbClass spyDbClass = spy(DbClass.class);
@Test
public void TestMethod() throws Exception {
// Передаем spy объект класса DbClass
// в котором будет подменённый метод
// в объект класса MockExample.
MockExample mockExample = new MockExample(spyDbClass);
// Вместо того что должен выводить
// метод getCommutaorsDataFromDB()
// покажет данные из БД в виде списка,
// будет выводиться список ниже.
List< String > fakeCommutaorsDataFromDB =
Arrays.asList(“comm1”, “comm2”, “comm3”);
// Ниже мы показываем что когда вызывается метод
// getCommutaorsDataFromDB у объекта spyDbClass
// то выводится список fakeCommutaorsDataFromDB
// вместо реальных коммутаторов из БД.
// Метод getCommuttatorsDataFromDB из spy объекта
// spyDbClass ясное дело будет использоваться
// в методе getDbDataForTestWithPlusses объекта
// mockExample.
when(spyDbClass.getCommuttatorsDataFromDB()).thenReturn(fakeCommutaorsDataFromDB);
// Метод getCommuttatorsDataFromDB в spy
// объекте spyDbClass мы подменили.
// Все другие методы в этом объекте
// должны работать нормально, они не должны
// возвращать пустоту как в случае с mock.
// spyDbClass это самый обычный объект но с
// подмененным методом getCommuttatorsDataFromDB.
// Метод pow очевидно должен возвращать 16.
System.out.println(spyDbClass.pow(4,2));
// Теперь тестируем метод getDbDataForTestWithPlusses
// В объекте mockExample со spy зависимостью в нем
// с подмененным методом getCommuttatorsDataFromDB.
Assert.assertEquals(Arrays.asList(“comm1”, “comm2”, “comm3”),
mockExample.getDbDataForTestWithPlusses());
}
}
Итак, компилируем и запускаем:
Видим, что сначала вывело 16. Это то, что вернул метод pow, что значит что методы в spy объекте работают нормально, они не возвращают пустоту как mock объект.
Также очевидно, что методgetCommutatorsDataFromDB был подменен успешно, так как тестирование прошло успешно.
В скобках у аннотации Test можно указать число. Это число – это количество миллисекунд. Если метод будет выполняться больше этого количества миллисекунд, то программа сообщит об этом.
Таким образом можно протестировать производительность тестируемого метода.
Пример программы:
import org.junit.Assert;
import org.junit.Test;
public class TestCalc {
// проверяет, что метод исполняется
// не более 100 миллисекунд
@Test(timeout = 100)
public void testPow2() throws Exception {
Calc calc = new Calc();
int result = 0;
// Сделаем так чтобы тестовый
// метод длился более 100 миллисекунд.
// Хотя этот цикл было бы логичнее разместить
// в тестируемом методе pow, так как мы тестируем его
// производительность, а не тестового метода.
for (int i = 0; i < 1000000; i++) {
result = calc.pow(2, 4);
}
Assert.assertEquals(16, result);
}
}
Скомпилируем и запустим:
Видим, что программа сообщает нам о том, что метод выполнялся более 100 миллисекунд. Значит очевидно нужно налаживать производительность.
Аннотация @Ignore
Если в тестовом классе несколько тестовых методов, то мы можем отключать какие-либо из них с помощью аннотации Ignore
Пример программы:
import org.junit.Assert;
import org.junit.Test;
import org.junit.Ignore;
public class TestCalc {
@Test
public void testPow() throws Exception{
Calc calc = new Calc();
int result = calc.pow(2, 4);
Assert.assertEquals(14, result);
}
//Также какой-либо тест можно отключить
//(следующий тест за этой
//аннотацией не запуститься).
@Ignore
@Test
public void testPow1() throws Exception{
Calc calc = new Calc();
int result = calc.pow(2, 4);
Assert.assertEquals(16, result);
}
@Test(timeout=100)
public void testPow2() throws Exception{
Calc calc = new Calc();
int result = 0;
for(int i=0;i<1000000;i++){
result = calc.pow(2, 4);
}
Assert.assertEquals(16, result);
}
}
Компилируем и запускаем:
Как видим, хоть тестовых метода у нас три, а тестировалось всего два – Tests run: 2. Так как второй тестовый метод мы отключили.
Аннотации @BeforeClass, @AfterClass
С помощью аннотации @BeforeClassможно создать метод, который будет выполнен единожды перед тем, как начнут выполняться тестовые методы, а с помощью аннотации @AfterClassможно создать метод, который будет выполнен единожды после того, как выполняться все тестовые методы.
Зачем же нужны эти методы?
В случае с BeforeClass, чтобы, например, инициализировать какие-то данные, которые будут использоваться во всех тестах, в случае с AfterClass освободить какие-то ресурсы.
Пример программы:
import org.junit.Assert;
import org.junit.Test;
import org.junit.BeforeClass;
import org.junit.AfterClass;
public class TestCalc {
//этот метод запускается перед тестовыми
@BeforeClass
public static void testBeforeClass() throws Exception{
System.out.println(“before all classes”);
}
//этот метод запускается после всех тестовых
@AfterClass
public static void testAfterClass() throws Exception{
System.out.println(“after all classes”);
}
@Test
public void testPow() throws Exception{
System.out.println(“Test method 1”);
Calc calc = new Calc();
int result = calc.pow(2, 4);
Assert.assertEquals(14, result);
}
@Test
public void testPow1() throws Exception{
System.out.println(“Test method 2”);
Calc calc = new Calc();
int result = calc.pow(2, 4);
Assert.assertEquals(16, result);
}
@Test(timeout=100)
public void testPow2() throws Exception{
System.out.println(“Test method 3”);
Calc calc = new Calc();
int result = calc.pow(2, 4);
Assert.assertEquals(16, result);
}
}
Компилируем и запускаем.
Видим, что сразу вывелось сообщение из метода помеченного@BeforeClass,потом сообщения тестовых методов, потом сообщение из метода помеченного@AfterClass.
Аннотации @Before, @After
С помощью аннотации @Before можно создать метод, который будет выполняться перед каждым тестовым методом, а с помощью аннотации @Afterможно создать метод, который будет выполняться после каждого тестового метода
Пример программы:
import org.junit.Assert;
import org.junit.Test;
import org.junit.Before;
import org.junit.After;
public class TestCalc {
//этот метод запускается перед каждым тестовым
@Before
public void testBefore() throws Exception{
System.out.println(“before every class”);
}
//этот метод запускается после каждого тестового
@After
public void testAfter() throws Exception{
System.out.println(“after every class”);
}
@Test
public void testPow() throws Exception{
System.out.println(“Test method 1”);
Calc calc = new Calc();
int result = calc.pow(2, 4);
Assert.assertEquals(14, result);
}
@Test
public void testPow1() throws Exception{
System.out.println(“Test method 2”);
Calc calc = new Calc();
int result = calc.pow(2, 4);
Assert.assertEquals(16, result);
}
@Test(timeout=100)
public void testPow2() throws Exception{
System.out.println(“Test method 3”);
Calc calc = new Calc();
int result = calc.pow(2, 4);
Assert.assertEquals(16, result);
}
}
Компилируем и запускаем.
Видим, что перед каждым сообщением из тестового метода выводилось сообщение из метода помеченного @Before, и также послекаждого сообщения тестового метода выводилось сообщение из метода помеченного аннотацией @After.
JUnit — это библиотека для тестирования отдельных частей кода, в частности — методов.
Когда мы реализовали какой-то метод, нам всегда нужно его протестировать. Правильно? Потому что если мы запустим код где много непротестированных методов, то мы получим много ошибок, в которых разбираться будет очень сложно.
Или если мы уже запустили такой непроверенный код, то нам нужно как-то его чинить. Ошибки можно находить проходясь по методам кода тестами.
Можно задаться вопросом, зачем нужна библиотека JUNIT, если можно вывести результат работы какого-либо метода просто в main через консоль например?
Дело в том, чтоJUnit делает процесс тестирования проще и добавляет доп. функциональность для доп. удобства тестирования.
Допустим у нас есть класс, в котором мы создали метод pow и теперь ходим его протестировать
Класс Clac:
public class Calc {
//Функция возведения в степень
public int pow(int num, int power){
int result=1;
for(int i=0; i < power; i++) {result *= num;}
return result;
}
}
Теперь нужно создать отдельный класс для тестирования методов этого класса Calc с помощью JUNIT.
Пример программы:
import org.junit.Assert;
import org.junit.Test;
//Названия классов тестеров принято называть
//начиная Test и заканчивая именем класса, который
//тестируем то есть в данном случае класса Calc.
public class TestCalc {
@Test
//Помечаем что это метод для теста.
//Функция для проверки метода возведения в степень.
//имя метода тестера тоже = test потом имя метода,
//который тестируем.
public void testPow() throws Exception{
Calc calc = new Calc();
int result = calc.pow(2, 4);
//Метод assertEquals сообщает совпадает ли
//значение, которое вернул метод pow
//со значением, которое ожидает программист.
//Слева значение, которое ожидаем,
//справа фактическое.
Assert.assertEquals(16, result);
}
}
Для того чтобы скомпилировать файл с тестами нужно сначала скачать два файла – hamcrest-core-1.3.jar и junit-4.13.2.jar. Оба довольно легко можно найти в интернете.
В консоли компилируем файл с тестами с использованием скачанных файлов:
Запускать нужно всегда с этим файлом иначе программа не запуститься.
Компилируем и запускаем:
Программа сообщает нам, что всё ОК. То есть ожидаемое значение совпадает с фактическим. Значит метод работает корректно.
Теперь давайте изменим ожидаемое значение и посмотрим, что будет.
Пример программы:
import org.junit.Assert;
import org.junit.Test;
//Названия классов тестеров принято называть
//начиная Test и заканчивая именем класса, который
//тестируем то есть в данном случае класса Calc.
public class TestCalc {
@Test //Помечаем что это метод для теста.
//Функция для проверки метода возведения в степень.
//имя метода тестера тоже – test потом имя метода,
//который тестируем.
public void testPow() throws Exception{
Calc calc = new Calc();
int result = calc.pow(2, 4);
//Метод assertEquals сообщает совпадает ли
//значение, которое вернул метод pow
//со значением, которое ожидает программист.
//Слева значение, которое ожидаем,
//справа фактическое.
Assert.assertEquals(14, result);
}
}
Компилируем и запускаем.
Как видим, программа нам сообщает, что значение, которое вернул метод pow (вернул 16) не совпадает со значением, которое ожидал программист (ожидал 14), что значит что метод работает не правильно и программист должен идти его исправлять.
В метод assertEquals можно передавать значения любого типа, он перегружен и его можно использовать с разными параметрами.
Например, можно передать третьим параметром в него число отклонения фактического значения вот так:
Assert.assertEquals(16, result, 2);
То есть если фактическое значение было например 15 или 14, то тестовый метод скажет, что всё ОК, так как 15 и 14 не заходят за приделы отклонения от числа 16 на 2. Заходят числа, которые больше 18 и меньше 14.
Также из полезных методов стоит упомянуть метод assertArrayEquals. То же самое, что и assertEquals, но сравнивает два массива а не два значения.
Также есть assertTrue, assertFalse – проверяют вернул ли проверяемый метод truе, false соответственно.
Также есть еще несколько других методов, но они все очень простые, особого смысла демонстрировать их нет, можно почитать о них в документации.