Единичното тестване стана задължително в ерата на Agile и има много инструменти, които да помогнат при автоматизираното тестване. Един такъв инструмент е Mockito, рамка с отворен код, която ви позволява да създавате и конфигурирате подигравани обекти за тестове.
В тази статия ще разгледаме създаването и конфигурирането на макети и ще ги използваме, за да проверим очакваното поведение на системата, която се тества. Също така ще се потопим малко във вътрешностите на Mockito, за да разберем по-добре неговия дизайн и предупреждения. Ще използваме JUnit като модулна рамка за тестване, но тъй като Mockito не е обвързан с JUnit, можете да следвате, дори ако използвате различна рамка.
Получаването на Mockito е лесно в наши дни. Ако използвате Gradle, е въпрос да добавите този един ред към вашия скрипт за изграждане:
testCompile 'org.mockito:mockito−core:2.7.7'
Що се отнася до такива като мен, които все още предпочитат Maven, просто добавете Mockito към зависимостите си така:
org.mockito mockito-core 2.7.7 test
Разбира се, светът е много по-широк от Maven и Gradle. Можете да използвате всеки инструмент за управление на проекти, за да вземете артефакта на Mockito jar от Централно хранилище на Maven .
Единичните тестове са предназначени да тестват поведението на конкретни класове или методи, без да разчитат на поведението на техните зависимости. Тъй като тестваме най-малката „единица“ код, не е необходимо да използваме действителни реализации на тези зависимости. Освен това ще използваме малко по-различни реализации на тези зависимости, когато тестваме различно поведение. Традиционен, добре познат подход към това е създаването на „заглушки“ - специфични реализации на интерфейс, подходящ за даден сценарий. Такива реализации обикновено имат твърдо кодирана логика. Стъблото е вид тест двойник. Други видове включват фалшификати, подигравки, шпиони, манекени и др.
Ще се съсредоточим само върху два вида тестови двойки, „макети“ и „шпиони“, тъй като те са силно използвани от Mockito.
Какво е подигравка? Очевидно не е мястото, където се подигравате на колегите си разработчици. Подигравките за единично тестване са, когато създавате обект, който реализира поведението на реална подсистема по контролирани начини. Накратко, подигравките се използват като заместител на зависимост.
С Mockito създавате макет, казвате на Mockito какво да прави, когато са извикани специфични методи и след това използвате макетния екземпляр в теста си вместо истинския. След теста можете да попитате макета, за да видите какви конкретни методи са били извикани, или да проверите страничните ефекти под формата на променено състояние.
По подразбиране Mockito предоставя изпълнение за всеки метод на макет.
Шпионинът е другият тип тестов двойник, който Mockito създава. За разлика от макетите, създаването на шпионин изисква екземпляр, който да шпионира. По подразбиране шпионин делегира всички извиквания на метода към реалния обект и записва какъв метод е бил извикан и с какви параметри. Това го прави шпионин: Той шпионира реален обект.
Помислете дали да не използвате шпиони, когато е възможно. Шпионите може да са полезни за тестване на стария код, който не може да бъде преработен, за да бъде лесно проверяем, но необходимостта от използване на шпионин за частична подигравка с клас е показател, че даден клас прави твърде много, като по този начин нарушава принципа на единната отговорност.
Нека разгледаме проста демонстрация, за която можем да напишем тестове. Да предположим, че имаме UserRepository
интерфейс с един метод за намиране на потребител по неговия идентификатор. Разполагаме и с концепцията за кодиращ парола, който превръща паролата с ясен текст в хеш на паролата. И двете UserRepository
и PasswordEncoder
са зависимости (наричани още сътрудници) на UserService
инжектиран чрез конструктора. Ето как изглежда нашият демо код:
public interface UserRepository { User findById(String id); }
public class User { private String id; private String passwordHash; private boolean enabled; public User(String id, String passwordHash, boolean enabled) { this.id = id; this.passwordHash = passwordHash; this.enabled = enabled; } ... }
public interface PasswordEncoder { String encode(String password); }
public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public boolean isValidUser(String id, String password) { User user = userRepository.findById(id); return isEnabledUser(user) && isValidPassword(user, password); } private boolean isEnabledUser(User user) { return user != null && user.isEnabled(); } private boolean isValidPassword(User user, String password) { String encodedPassword = passwordEncoder.encode(password); return encodedPassword.equals(user.getPasswordHash()); } }
Този примерен код може да бъде намерен на GitHub , за да можете да го изтеглите за преглед заедно с тази статия.
Използвайки нашия примерен код, нека разгледаме как да приложим Mockito и да напишем някои тестове.
С Mockito създаването на макет е толкова лесно, колкото извикването на статичен метод Mockito.mock()
:
import static org.mockito.Mockito.*; ... PasswordEncoder passwordEncoder = mock(PasswordEncoder.class);
Забележете статичния импорт за Mockito. В останалата част на тази статия неявно ще разгледаме добавянето на този импорт.
След импортирането се подиграваме PasswordEncoder
, интерфейс. Mockito се подиграва не само с интерфейси, но и с абстрактни класове и конкретни нефинални класове. Извън кутията Mockito не може да се подиграва с окончателни класове и окончателни или статични методи, но ако наистина имате нужда от тях, Mockito 2 предоставя експерименталната MockMaker плъгин.
Също така имайте предвид, че методите equals()
и hashCode()
не може да се подиграва.
За да създадете шпионин, трябва да извикате статичния метод на Mockito spy()
и му предайте екземпляр за шпиониране. Методите за извикване на върнатия обект ще извикат реални методи, освен ако тези методи не са блокирани. Тези обаждания се записват и фактите от тези повиквания могат да бъдат проверени (вижте допълнителното описание на verify()
). Нека направим шпионин:
DecimalFormat decimalFormat = spy(new DecimalFormat()); assertEquals('42', decimalFormat.format(42L));
Създаването на шпионин не се различава много от създаването на макет. Освен това всички методи на Mockito, използвани за конфигуриране на макет, са приложими и за конфигуриране на шпионин.
Шпионите се използват рядко в сравнение с макети, но може да ги намерите полезни за тестване на стария код, който не може да бъде рефакториран, когато тестването изисква частично подиграване. В тези случаи можете просто да създадете шпионин и да заглушите някои от методите му, за да постигнете желаното от вас поведение.
Обажда се mock(PasswordEncoder.class)
връща екземпляр на PasswordEncoder
. Можем дори да извикаме методите му, но какво ще върнат? По подразбиране всички методи на макет връщат „неинициализирани“ или „празни“ стойности, напр. Нули за числови типове (както примитивни, така и в полета), false за логически стойности и нули за повечето други типове.
Помислете за следния интерфейс:
interface Demo { int getInt(); Integer getInteger(); double getDouble(); boolean getBoolean(); String getObject(); Collection getCollection(); String[] getArray(); Stream getStream(); Optional getOptional(); }
Сега разгледайте следния фрагмент, който дава представа какви стойности по подразбиране да очаквате от методите на макет:
Demo demo = mock(Demo.class); assertEquals(0, demo.getInt()); assertEquals(0, demo.getInteger().intValue()); assertEquals(0d, demo.getDouble(), 0d); assertFalse(demo.getBoolean()); assertNull(demo.getObject()); assertEquals(Collections.emptyList(), demo.getCollection()); assertNull(demo.getArray()); assertEquals(0L, demo.getStream().count()); assertFalse(demo.getOptional().isPresent());
Пресни, непроменени подигравки са полезни само в редки случаи. Обикновено искаме да конфигурираме макета и да определим какво да правим, когато са извикани специфични методи на макета. Това се казва стърчащ .
Mockito предлага два начина за смазване. Първият начин е „ кога този метод се нарича, тогава направи нещо.' Обмислете следния фрагмент:
when(passwordEncoder.encode('1')).thenReturn('a');
Той гласи почти като английски: „Когато passwordEncoder.encode(“1”)
се извика, върнете a
. '
Вторият начин на забиване гласи по-скоро като „Направете нещо, когато методът на този макет е извикан със следните аргументи“. Този начин на забиване е по-труден за четене, тъй като причината е посочена в края. Обмисли:
doReturn('a').when(passwordEncoder).encode('1');
Фрагментът с този метод на забиване би гласил: „Връщане a
когато passwordEncoder
’s encode()
метод се извиква с аргумент от 1
. '
Първият начин се счита за предпочитан, тъй като е безопасен за типа и защото е по-четлив. Рядко обаче сте принудени да използвате втория начин, например когато побивате истински метод на шпионин, защото извикването му може да има нежелани странични ефекти.
Нека да разгледаме накратко методите за зашеметяване, предоставени от Mockito. В нашите примери ще включим и двата начина за смазване.
thenReturn
или doReturn()
се използват за задаване на стойност, която да се връща при извикване на метод.
//”when this method is called, then do something” when(passwordEncoder.encode('1')).thenReturn('a');
или
//”do something when this mock’s method is called with the following arguments” doReturn('a').when(passwordEncoder).encode('1');
Можете също така да посочите множество стойности, които ще бъдат върнати като резултати от последователни извиквания на методи. Последната стойност ще се използва като резултат за всички следващи извиквания на метода.
//when when(passwordEncoder.encode('1')).thenReturn('a', 'b');
или
//do doReturn('a', 'b').when(passwordEncoder).encode('1');
Същото може да се постигне със следния фрагмент:
when(passwordEncoder.encode('1')) .thenReturn('a') .thenReturn('b');
Този модел може да се използва и с други методи за засилване, за да се дефинират резултатите от последователни повиквания.
then()
, псевдоним на thenAnswer()
и doAnswer()
постигнете същото нещо, което създава персонализиран отговор, който да се връща при извикване на метод, по следния начин:
when(passwordEncoder.encode('1')).thenAnswer( invocation -> invocation.getArgument(0) + '!');
или
doAnswer(invocation -> invocation.getArgument(0) + '!') .when(passwordEncoder).encode('1');
Единственият аргумент thenAnswer()
take е изпълнение на Answer
интерфейс. Той има един метод с параметър от тип InvocationOnMock
.
Можете също така да хвърлите изключение в резултат на извикване на метод:
when(passwordEncoder.encode('1')).thenAnswer(invocation -> { throw new IllegalArgumentException(); });
... или извикайте реалния метод на клас (неприложим за интерфейси):
Date mock = mock(Date.class); doAnswer(InvocationOnMock::callRealMethod).when(mock).setTime(42); doAnswer(InvocationOnMock::callRealMethod).when(mock).getTime(); mock.setTime(42); assertEquals(42, mock.getTime());
Прав си, ако мислиш, че изглежда тромаво. Mockito предоставя thenCallRealMethod()
и thenThrow()
за да рационализирате този аспект от вашето тестване.
Както подсказва името му, thenCallRealMethod()
и doCallRealMethod()
извикайте реалния метод на фиктивен обект:
Date mock = mock(Date.class); when(mock.getTime()).thenCallRealMethod(); doCallRealMethod().when(mock).setTime(42); mock.setTime(42); assertEquals(42, mock.getTime());
Извикването на реални методи може да е полезно при частични подигравки, но се уверете, че извиканият метод няма нежелани странични ефекти и не зависи от състоянието на обекта. Ако го направи, шпионинът може да е по-подходящ от макет.
Ако създадете макет на интерфейс и се опитате да конфигурирате заглушител, за да извикате реален метод, Mockito ще изведе изключение с много информативно съобщение. Обмислете следния фрагмент:
when(passwordEncoder.encode('1')).thenCallRealMethod();
Mockito ще се провали със следното съобщение:
Cannot call abstract real method on java object! Calling real methods is only possible when mocking non abstract method. //correct example: when(mockOfConcreteClass.nonAbstractMethod()).thenCallRealMethod();
Поздрави за разработчиците на Mockito, че се грижат достатъчно, за да предоставят толкова задълбочени описания!
thenThrow()
и doThrow()
конфигурирайте подиграван метод, за да хвърлите изключение:
when(passwordEncoder.encode('1')).thenThrow(new IllegalArgumentException());
или
doThrow(new IllegalArgumentException()).when(passwordEncoder).encode('1');
Mockito гарантира, че изключението, което се хвърля, е валидно за този специфичен метод с изключение и ще се оплаче, ако изключението не е в проверения списък с изключения на метода. Помислете за следното:
when(passwordEncoder.encode('1')).thenThrow(new IOException());
Това ще доведе до грешка:
org.mockito.exceptions.base.MockitoException: Checked exception is invalid for this method! Invalid: java.io.IOException
Както можете да видите, Mockito откри, че encode()
не може да хвърли IOException
.
Можете също така да предадете клас на изключение, вместо да предадете екземпляр на изключение:
when(passwordEncoder.encode('1')).thenThrow(IllegalArgumentException.class);
или
doThrow(IllegalArgumentException.class).when(passwordEncoder).encode('1');
Въпреки това, Mockito не може да провери клас на изключение по същия начин, както ще провери екземпляр на изключение, така че трябва да бъдете дисциплиниран и да не предавате незаконни обекти на класа. Например, следното ще хвърли IOException
все пак encode()
не се очаква да изведе проверено изключение:
when(passwordEncoder.encode('1')).thenThrow(IOException.class); passwordEncoder.encode('1');
Заслужава да се отбележи, че когато създава макет за интерфейс, Mockito се подиграва с всички методи на този интерфейс. Тъй като Java 8, интерфейсите могат да съдържат методи по подразбиране, заедно с абстрактни. Тези методи също се подиграват, така че трябва да се погрижите да действат като методи по подразбиране.
Помислете за следния пример:
interface AnInterface { default boolean isTrue() { return true; } } AnInterface mock = mock(AnInterface.class); assertFalse(mock.isTrue());
В този пример, assertFalse()
ще успее. Ако не това сте очаквали, уверете се, че сте накарали Mockito да извика истинския метод, така:
AnInterface mock = mock(AnInterface.class); when(mock.isTrue()).thenCallRealMethod(); assertTrue(mock.isTrue());
В предишните раздели конфигурирахме нашите подигравани методи с точни стойности като аргументи. В тези случаи Mockito просто извиква equals()
вътрешно, за да провери дали очакваните стойности са равни на действителните стойности.
Понякога обаче не знаем тези стойности предварително.
Може би просто не ни интересува действителната стойност, която се предава като аргумент, или може би искаме да определим реакция за по-широк диапазон от стойности. Всички тези сценарии (и повече) могат да бъдат адресирани чрез съвпадение на аргументи. Идеята е проста: Вместо да предоставите точна стойност, вие предоставяте съвпадение на аргументи за Mockito, за да съответства на аргументите на метода.
Обмислете следния фрагмент:
when(passwordEncoder.encode(anyString())).thenReturn('exact'); assertEquals('exact', passwordEncoder.encode('1')); assertEquals('exact', passwordEncoder.encode('abc'));
Можете да видите, че резултатът е един и същ, независимо на каква стойност предаваме encode()
защото използвахме anyString()
съвпадение на аргументи в този първи ред. Ако пренапишем този ред на обикновен английски, това ще звучи като „когато кодерът на парола бъде помолен да кодира всеки низ, след това върнете низа„ точно “.“
Mockito изисква да предоставите всички аргументи или чрез съвпадения или по точни стойности. Така че, ако методът има повече от един аргумент и искате да използвате съвпадение на аргументи само за някои от неговите аргументи, забравете го. Не можете да пишете код по този начин:
abstract class AClass { public abstract boolean call(String s, int i); } AClass mock = mock(AClass.class); //This doesn’t work. when(mock.call('a', anyInt())).thenReturn(true);
За да коригираме грешката, трябва да заменим последния ред, за да включим eq
съвпадение на аргументи за a
, както следва:
when(mock.call(eq('a'), anyInt())).thenReturn(true);
Тук използвахме eq()
и anyInt()
аргументи, но има много други налични. За пълен списък на съответстващите аргументи вижте документацията на org.mockito.ArgumentMatchers
клас.
Важно е да се отбележи, че не можете да използвате съвпадение на аргументи извън проверка или забиване. Например не можете да имате следното:
//this won’t work String orMatcher = or(eq('a'), endsWith('b')); verify(mock).encode(orMatcher);
Mockito ще открие неуместно съвпадение на аргументи и ще изведе InvalidUseOfMatchersException
. Проверката със съвпадения на аргументи трябва да става по този начин:
verify(mock).encode(or(eq('a'), endsWith('b')));
Съпоставянето на аргументи също не може да се използва като върната стойност. Mockito не може да се върне anyString()
или каквото и да било; точна стойност се изисква при зашеметяващи разговори.
Персонализираните съвпадения идват на помощ, когато трябва да предоставите някаква логика за съвпадение, която вече не е налична в Mockito. Решението за създаване на персонално съвпадение не трябва да се приема леко, тъй като необходимостта от съвпадение на аргументите по нетривиален начин показва или проблем в дизайна, или че тестът става твърде сложен.
Като такъв си струва да проверите дали можете да опростите тест, като използвате някои от по-леките съвпадения на аргументи като isNull()
и nullable()
преди да напишете собствено съвпадение. Ако все още изпитвате нужда да напишете аргумент за съвпадение, Mockito предлага семейство от методи за това.
Помислете за следния пример:
FileFilter fileFilter = mock(FileFilter.class); ArgumentMatcher hasLuck = file -> file.getName().endsWith('luck'); when(fileFilter.accept(argThat(hasLuck))).thenReturn(true); assertFalse(fileFilter.accept(new File('/deserve'))); assertTrue(fileFilter.accept(new File('/deserve/luck')));
Тук създаваме hasLuck
аргумент съвпадение и използване argThat()
да предаде съвпадението като аргумент на подиграван метод, като го забие, за да върне true
ако името на файла завършва с „късмет“. Можете да лекувате ArgumentMatcher
като функционален интерфейс и създайте неговия екземпляр с ламбда (което направихме в примера). По-малко кратък синтаксис ще изглежда така:
ArgumentMatcher hasLuck = new ArgumentMatcher() { @Override public boolean matches(File file) { return file.getName().endsWith('luck'); } };
Ако трябва да създадете съвпадение на аргументи, което работи с примитивни типове, има няколко други метода за това в org.mockito.ArgumentMatchers
:
Не винаги си струва да създавате потребителско съвпадение на аргументи, когато условието е твърде сложно, за да се обработва с основни съвпадения; понякога комбинирането на мачове ще свърши работа. Mockito осигурява съвпадение на аргументи за реализиране на общи логически операции („не“, „и“, „или“) на съвпадения на аргументи, които съответстват както на примитивни, така и на непримитивни типове. Тези съвпадения са внедрени като статични методи в org.mockito.AdditionalMatchers
клас.
Помислете за следния пример:
when(passwordEncoder.encode(or(eq('1'), contains('a')))).thenReturn('ok'); assertEquals('ok', passwordEncoder.encode('1')); assertEquals('ok', passwordEncoder.encode('123abc')); assertNull(passwordEncoder.encode('123'));
Тук сме комбинирали резултатите от две съвпадения на аргументи: eq('1')
и contains('a')
. Крайният израз, or(eq('1'), contains('a'))
, може да се интерпретира като „аргументният низ трябва да бъде равен до „1“ или съдържат 'да се'.
Имайте предвид, че има по-рядко срещани съвпадения, изброени в org.mockito.AdditionalMatchers
клас, като geq()
, leq()
, gt()
и lt()
, които са сравнения на стойности, приложими за примитивни стойности и случаи на java.lang.Comparable
.
След като се използва макет или шпионин, можем да verify
че са се осъществили специфични взаимодействия. Буквално казваме „Хей, Mockito, уверете се, че този метод е извикан с тези аргументи.“
Помислете за следния изкуствен пример:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); when(passwordEncoder.encode('a')).thenReturn('1'); passwordEncoder.encode('a'); verify(passwordEncoder).encode('a');
Тук сме създали макет и сме го нарекли encode()
метод. Последният ред проверява дали макетът encode()
е извикан със специфичната стойност на аргумента a
. Моля, обърнете внимание, че потвърждаването на извикване с прекъсване е излишно; целта на предишния фрагмент е да покаже идеята за извършване на проверка, след като са се случили някои взаимодействия.
Ако променим последния ред, за да има различен аргумент - да речем, b
- предишният тест ще се провали и Mockito ще се оплаче, че действителното извикване има различни аргументи (b
вместо очакваните a
).
Съпоставянето на аргументи може да се използва за проверка, точно както за забиване:
verify(passwordEncoder).encode(anyString());
По подразбиране Mockito проверява дали методът е бил извикан веднъж, но можете да проверите произволен брой извиквания:
// verify the exact number of invocations verify(passwordEncoder, times(42)).encode(anyString()); // verify that there was at least one invocation verify(passwordEncoder, atLeastOnce()).encode(anyString()); // verify that there were at least five invocations verify(passwordEncoder, atLeast(5)).encode(anyString()); // verify the maximum number of invocations verify(passwordEncoder, atMost(5)).encode(anyString()); // verify that it was the only invocation and // that there're no more unverified interactions verify(passwordEncoder, only()).encode(anyString()); // verify that there were no invocations verify(passwordEncoder, never()).encode(anyString());
Рядко използвана функция на verify()
е способността му да се провали по време на изчакване, което е полезно главно за тестване на едновременен код. Например, ако кодерът ни за парола е извикан в друга нишка едновременно с verify()
, можем да напишем тест, както следва:
usePasswordEncoderInOtherThread(); verify(passwordEncoder, timeout(500)).encode('a');
Този тест ще успее, ако encode()
се извиква и завършва в рамките на 500 милисекунди или по-малко. Ако трябва да изчакате целия период, който сте посочили, използвайте after()
вместо timeout()
:
verify(passwordEncoder, after(500)).encode('a');
Други режими за проверка (times()
, atLeast()
и т.н.) могат да се комбинират с timeout()
и after()
за да направите по-сложни тестове:
// passes as soon as encode() has been called 3 times within 500 ms verify(passwordEncoder, timeout(500).times(3)).encode('a');
Освен times()
, поддържаните режими за проверка включват only()
, atLeast()
и atLeastOnce()
(като псевдоним на atLeast(1)
).
Mockito също така ви позволява да проверите реда на повикванията в група макети. Това не е функция, която да се използва много често, но може да е полезно, ако е важен редът на извикванията. Помислете за следния пример:
PasswordEncoder first = mock(PasswordEncoder.class); PasswordEncoder second = mock(PasswordEncoder.class); // simulate calls first.encode('f1'); second.encode('s1'); first.encode('f2'); // verify call order InOrder inOrder = inOrder(first, second); inOrder.verify(first).encode('f1'); inOrder.verify(second).encode('s1'); inOrder.verify(first).encode('f2');
Ако пренаредим реда на симулираните повиквания, тестът ще се провали с VerificationInOrderFailure
.
Липсата на извиквания също може да бъде проверена чрез verifyZeroInteractions()
. Този метод приема макет или подигравки като аргумент и ще се провали, ако са извикани някакви методи на предадените макети.
Също така си струва да споменем verifyNoMoreInteractions()
метод, тъй като приема аргументи като аргументи и може да се използва, за да се провери дали всяко повикване на тези макети е проверено.
Освен проверка, че методът е извикан с конкретни аргументи, Mockito ви позволява да уловите тези аргументи, за да можете по-късно да изпълнявате потребителски твърдения върху тях. С други думи, казвате „Хей, Mockito, провери дали този метод е извикан и ми даде стойностите на аргументите, с които е извикан“.
Нека създадем макет от PasswordEncoder
, извикаме encode()
, заснемем аргумента и проверим стойността му:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode('password'); ArgumentCaptor passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals('password', passwordCaptor.getValue());
Както можете да видите, минаваме passwordCaptor.capture()
като аргумент на encode()
за проверка; това вътрешно създава съвпадение на аргументи, което запазва аргумента. След това извличаме уловената стойност с passwordCaptor.getValue()
и го проверете с assertEquals()
.
Ако трябва да уловим аргумент за множество повиквания, ArgumentCaptor
ви позволява да извличате всички стойности с getAllValues()
, по следния начин:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode('password1'); passwordEncoder.encode('password2'); passwordEncoder.encode('password3'); ArgumentCaptor passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder, times(3)).encode(passwordCaptor.capture()); assertEquals(Arrays.asList('password1', 'password2', 'password3'), passwordCaptor.getAllValues());
Същата техника може да се използва за улавяне на аргументи от метода на променливи arity (известни също като varargs).
Сега, когато знаем много повече за Mockito, е време да се върнем към нашата демонстрация. Нека напишем isValidUser
тест за метод. Ето как може да изглежда:
public class UserServiceTest { private static final String PASSWORD = 'password'; private static final User ENABLED_USER = new User('user id', 'hash', true); private static final User DISABLED_USER = new User('disabled user id', 'disabled user password hash', false); private UserRepository userRepository; private PasswordEncoder passwordEncoder; private UserService userService; @Before public void setup() { userRepository = createUserRepository(); passwordEncoder = createPasswordEncoder(); userService = new UserService(userRepository, passwordEncoder); } @Test public void shouldBeValidForValidCredentials() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), PASSWORD); assertTrue(userIsValid); // userRepository had to be used to find a user with id='user id' verify(userRepository).findById(ENABLED_USER.getId()); // passwordEncoder had to be used to compute a hash of 'password' verify(passwordEncoder).encode(PASSWORD); } @Test public void shouldBeInvalidForInvalidId() { boolean userIsValid = userService.isValidUser('invalid id', PASSWORD); assertFalse(userIsValid); InOrder inOrder = inOrder(userRepository, passwordEncoder); inOrder.verify(userRepository).findById('invalid id'); inOrder.verify(passwordEncoder, never()).encode(anyString()); } @Test public void shouldBeInvalidForInvalidPassword() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), 'invalid'); assertFalse(userIsValid); ArgumentCaptor passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals('invalid', passwordCaptor.getValue()); } @Test public void shouldBeInvalidForDisabledUser() { boolean userIsValid = userService.isValidUser(DISABLED_USER.getId(), PASSWORD); assertFalse(userIsValid); verify(userRepository).findById(DISABLED_USER.getId()); verifyZeroInteractions(passwordEncoder); } private PasswordEncoder createPasswordEncoder() { PasswordEncoder mock = mock(PasswordEncoder.class); when(mock.encode(anyString())).thenReturn('any password hash'); when(mock.encode(PASSWORD)).thenReturn(ENABLED_USER.getPasswordHash()); return mock; } private UserRepository createUserRepository() { UserRepository mock = mock(UserRepository.class); when(mock.findById(ENABLED_USER.getId())).thenReturn(ENABLED_USER); when(mock.findById(DISABLED_USER.getId())).thenReturn(DISABLED_USER); return mock; } }
Mockito предлага четлив, удобен API, но нека разгледаме някои от вътрешните му работи, за да разберем ограниченията му и да избегнем странни грешки.
Нека разгледаме какво се случва в Mockito, когато се изпълни следният фрагмент:
// 1: create PasswordEncoder mock = mock(PasswordEncoder.class); // 2: stub when(mock.encode('a')).thenReturn('1'); // 3: act mock.encode('a'); // 4: verify verify(mock).encode(or(eq('a'), endsWith('b')));
Очевидно първият ред създава макет. Mockito използва ByteBuddy за създаване на подклас на дадения клас. Новият обект на клас има генерирано име като demo.mockito.PasswordEncoder$MockitoMock53422997
, неговото equals()
ще действа като проверка на самоличността и hashCode()
ще върне хеш код на самоличност. След като класът е генериран и зареден, неговият екземпляр се създава с помощта на Обенезис .
Нека разгледаме следващия ред:
when(mock.encode('a')).thenReturn('1');
Подреждането е важно: Първият израз, изпълнен тук, е mock.encode('a')
, който ще извика encode()
на макета с върната стойност по подразбиране от null
. Така че наистина минаваме null
като аргумент на when()
. Mockito не се интересува каква точно стойност се предава на when()
защото съхранява информация за извикване на подиграван метод в така нареченото „непрекъснато забиване“, когато този метод е извикан. По-късно, когато се обаждаме when()
, Mockito изтегля този непрекъснат обект и го връща в резултат на when()
Тогава извикваме thenReturn(“1”)
върху върнатия продължаващ обект.
Третият ред, mock.encode('a');
е просто: Извикваме метода на заглушаване. Вътрешно Mockito запазва това извикване за по-нататъшна проверка и връща отговора на извикването; в нашия случай това е низът 1
.
В четвъртия ред (verify(mock).encode(or(eq('a'), endsWith('b')));
) искаме Mockito да провери дали е било извикано | | + _ | с тези конкретни аргументи.
encode()
се изпълнява първо, което превръща вътрешното състояние на Mockito в режим на проверка. Важно е да разберете, че Mockito поддържа състоянието си в verify()
. Това прави възможно прилагането на хубав синтаксис, но от друга страна, това може да доведе до странно поведение, ако рамката се използва неправилно (ако се опитате да използвате съвпадение на аргументи извън проверка или забиване, например).
И така, как Mockito създава ThreadLocal
съвпадение? Първо, or
се извиква и eq('a')
matcher се добавя към стека за съвпадения. Второ, equals
се извиква и endsWith('b')
matcher се добавя към стека. Най-накрая, endsWith
се нарича — използва двата съвпадения, които изскача от стека, създава or(null, null)
съвпадение и го избутва в стека. И накрая, or
е наречен. След това Mockito проверява дали методът е бил извикан с очаквания брой пъти и с очакваните аргументи.
Докато съвпаденията на аргументи не могат да бъдат извлечени към променливи (защото това променя реда на извикванията), те могат да бъдат извлечени в методи. Това запазва реда на обажданията и поддържа стека в правилното състояние:
encode()
В предишните раздели създадохме нашите подигравки по такъв начин, че когато се извикат всякакви подигравани методи, те връщат „празна“ стойност. Това поведение е конфигурируемо. Можете дори да предоставите собствено изпълнение на verify(mock).encode(matchCondition()); … String matchCondition() { return or(eq('a'), endsWith('b')); }
ако предоставените от Mockito не са подходящи, но това може да е индикация, че нещо не е наред, когато модулните тестове станат твърде сложни. Запомнете принципа KISS!
Нека разгледаме предлагането от Mockito на предварително дефинирани отговори по подразбиране:
org.mockito.stubbing.Answer
е стратегията по подразбиране; не си струва да се споменава изрично при настройване на макет.
RETURNS_DEFAULTS
прави неустановени извиквания да извикват реални методи.
CALLS_REAL_METHODS
избягва a RETURNS_SMART_NULLS
чрез връщане NullPointerException
вместо SmartNull
при използване на обект, върнат от неустановено извикване на метод. Пак ще се провалите с null
, но NullPointerException
ви дава по-хубава трасировка на стека с линията, където е бил извикан unstubbed метод. Това си заслужава да имате SmartNull
бъдете отговор по подразбиране в Mockito!
RETURNS_SMART_NULLS
първо се опитва да върне обикновени „празни“ стойности, след това се подиграва, ако е възможно, и RETURNS_MOCKS
в противен случай. Критериите за празнота се различават малко от това, което сме виждали по-рано: Вместо да се връщаме null
за низове и масиви, макетите, създадени с null
връща съответно празни низове и празни масиви.
RETURNS_MOCKS
е полезно за подигравателни строители. С тази настройка макетът ще върне екземпляр от себе си, ако се извика метод, който връща нещо от тип, равен на класа (или суперклас) на подигравания клас.
RETURNS_SELF
отива по-дълбоко от RETURNS_DEEP_STUBS
и създава макети, които могат да върнат макети от макети от макети и др. За разлика от RETURNS_MOCKS
, правилата за празнота са по подразбиране в RETURNS_MOCKS
, така че връща RETURNS_DEEP_STUBS
за низове и масиви:
null
Mockito ви позволява да назовете макет, функция, полезна, ако имате много макети в тест и трябва да ги различите. Въпреки това, необходимостта от назоваване на подигравки може да е симптом на лош дизайн. Помислете за следното:
interface We { Are we(); } interface Are { So are(); } interface So { Deep so(); } interface Deep { boolean deep(); } ... We mock = mock(We.class, Mockito.RETURNS_DEEP_STUBS); when(mock.we().are().so().deep()).thenReturn(true); assertTrue(mock.we().are().so().deep());
Mockito ще се оплаче, но тъй като официално не сме нарекли подигравките, не знаем кой:
PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class); verify(robustPasswordEncoder).encode(anyString());
Нека ги назовем, като предадем низ в конструкцията:
Wanted but not invoked: passwordEncoder.encode();
Сега съобщението за грешка е по-приятелско и ясно сочи към PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class, 'robustPasswordEncoder'); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class, 'weakPasswordEncoder'); verify(robustPasswordEncoder).encode(anyString());
:
robustPasswordEncoder
Понякога може да пожелаете да създадете макет, който реализира няколко интерфейса. Mockito е в състояние да направи това лесно, като например:
Wanted but not invoked: robustPasswordEncoder.encode();
Мокет може да бъде конфигуриран да извиква слушател на извикване всеки път, когато е извикан метод на макета. Вътре в слушателя можете да разберете дали извикването е породило стойност или е било хвърлено изключение.
PasswordEncoder mock = mock( PasswordEncoder.class, withSettings().extraInterfaces(List.class, Map.class)); assertTrue(mock instanceof List); assertTrue(mock instanceof Map);
В този пример изхвърляме или върнатата стойност, или проследяване на стека към системния изходен поток. Нашата реализация прави приблизително същото като Mockito’s InvocationListener invocationListener = new InvocationListener() { @Override public void reportInvocation(MethodInvocationReport report) { if (report.threwException()) { Throwable throwable = report.getThrowable(); // do something with throwable throwable.printStackTrace(); } else { Object returnedValue = report.getReturnedValue(); // do something with returnedValue System.out.println(returnedValue); } } }; PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().invocationListeners(invocationListener)); passwordEncoder.encode('1');
(не използвайте това директно, това са вътрешни неща). Ако регистрирането на извиквания е единствената функция, от която се нуждаете от слушателя, тогава Mockito предоставя по-чист начин да изразите намерението си с org.mockito.internal.debugging.VerboseMockInvocationLogger
настройка:
verboseLogging()
Обърнете внимание обаче, че Mockito ще се обади на слушателите, дори когато сте зашеметяващи методи. Помислете за следния пример:
PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging());
Този фрагмент ще даде резултат, подобен на следния:
PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging()); // listeners are called upon encode() invocation when(passwordEncoder.encode('1')).thenReturn('encoded1'); passwordEncoder.encode('1'); passwordEncoder.encode('2');
Имайте предвид, че първото регистрирано извикване съответства на извикване ############ Logging method invocation #1 on mock/spy ######## passwordEncoder.encode('1'); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) has returned: 'null' ############ Logging method invocation #2 on mock/spy ######## stubbed: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) passwordEncoder.encode('1'); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:89) has returned: 'encoded1' (java.lang.String) ############ Logging method invocation #3 on mock/spy ######## passwordEncoder.encode('2'); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:90) has returned: 'null'
докато го забива. Това е следващото извикване, което съответства на извикването на метода за заглушаване.
Mockito предлага още няколко настройки, които ви позволяват да направите следното:
encode()
.withSettings().serializable()
.withSettings().stubOnly()
. Когато се подигравате с вътрешни нестатични класове, добавете withSettings().useConstructor()
настройка, така: outerInstance()
.Ако трябва да създадете шпионин с персонализирани настройки (като персонализирано име), има withSettings().useConstructor().outerInstance(outerObject)
настройка, така че Mockito да създаде шпионин на екземпляра, който предоставяте, така:
spiedInstance()
Когато е посочен шпиониран екземпляр, Mockito ще създаде нов екземпляр и ще попълни своите нестатични полета със стойности от оригиналния обект. Ето защо е важно да се използва върнатият екземпляр: Само неговите извиквания на методи могат да бъдат блокирани и проверени.
Имайте предвид, че когато създавате шпионин, вие по същество създавате макет, който извиква реални методи:
UserService userService = new UserService( mock(UserRepository.class), mock(PasswordEncoder.class)); UserService userServiceMock = mock( UserService.class, withSettings().spiedInstance(userService).name('coolService'));
Нашите лоши навици правят нашите тестове сложни и неподдържани, а не Mockito. Например може да почувствате нужда да се подигравате с всичко. Този начин на мислене води до тестване на подигравки вместо производствен код. Подигравките на API на трети страни също могат да бъдат опасни поради потенциални промени в този API, които могат да нарушат тестовете.
Въпреки че лошият вкус е въпрос на възприятие, Mockito предлага няколко противоречиви функции, които могат да направят вашите тестове по-малко поддържаеми. Понякога забиването не е тривиално или злоупотребата с инжектиране на зависимост може да направи пресъздаването на макети за всеки тест трудно, неразумно или неефективно.
Mockito дава възможност за изчистване на извиквания за макети, като същевременно се запазва смачкване, по следния начин:
// creating a spy this way... spy(userService); // ... is a shorthand for mock(UserService.class, withSettings() .spiedInstance(userService) .defaultAnswer(CALLS_REAL_METHODS));
Прибягвайте до изчистване на извиквания, само ако пресъздаването на макет би довело до значителни режийни разходи или ако конфигуриран макет е предоставен от рамка за инжектиране на зависимост и забиването е нетривиално.
Нулиране на макет с PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); UserRepository userRepository = mock(UserRepository.class); // use mocks passwordEncoder.encode(null); userRepository.findById(null); // clear clearInvocations(passwordEncoder, userRepository); // succeeds because invocations were cleared verifyZeroInteractions(passwordEncoder, userRepository);
е друга противоречива характеристика и трябва да се използва в изключително редки случаи, например когато макет се инжектира от контейнер и не можете да го пресъздадете за всеки тест.
Друг лош навик се опитва да замени всяко твърдение с Mockito’s reset()
Важно е ясно да се разбере какво се тества: взаимодействията между сътрудници могат да бъдат проверени с verify()
, като същевременно се потвърждават наблюдаваните резултати от изпълнено действие с твърдения.
Използването на Mockito не е просто въпрос на добавяне на друга зависимост, то изисква промяна на начина, по който мислите за вашите модулни тестове, като същевременно премахвате много образци.
С множество фалшиви интерфейси, извикване на слушане, съвпадения и улавяне на аргументи, видяхме как Mockito прави тестовете ви по-чисти и по-лесни за разбиране, но като всеки инструмент, той трябва да се използва подходящо, за да бъде полезен. Сега въоръжени със знанието за вътрешното функциониране на Mockito, можете да изведете модулното тестване на следващото ниво.