Миграция на Mockito 2.1
11 Oct 2016На днях мир был ошеломлён неожиданной новостью.
Шутки ли, вышла Mockito 2.1.0 - после стольких лет ожиданий! Признаться, я уж было отчаялся.
В Mockito 2 обещается много всяких вкусняшек, включая:
- Поддержка Java 8
- Переход с CGLIB на ByteBuddy
- Мокинг final классов и методов
Ура! Надо брать!
Какое же меня ждало разочарование…
Тотальный облом
Я попробовал перевести на Mockito 2.1.0 свой рабочий проект, и … облом. Я получил больше 100 красных юнит-тестов (из ~6000).
Моя первая эмоция - какого чёрта! В топку этот Mockito 2!
Но при ближайшем рассмотрении оказалось, что Mockito 2 молодец, а вот эти тесты - плохие. Новый Mockito обнаружил в моих тестах целый ряд проблем, который старый Mockito не замечал. Вот оно чо, Мокитыч…
Что надо сделать
Ниже - путеводитель по миграции на Mockito 2. Протяни руку, читатель, и проведу тебя по этому тернистому пути к счастливому финалу.
Первым делом ты заменишь импорты
Много, много импортов. Придётся поменять много файлов.
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyVararg; // не нужен - меняем на any()
на
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
Затем ты обновишь API функции doAnswer
Было:
when(userDeviceService.save(any(UserDevice.class)))
.then(invocation -> invocation.getArgumentAt(0, UserDevice.class));
Стало (getArgumentAt
-> getArgument
, убираешь класс):
when(userDeviceService.save(any()))
.then(invocationOnMock -> invocationOnMock.getArgument(0));
Дальше ты обновишь API isMock
Было:
import org.mockito.internal.util.MockUtil;
assertFalse(new MockUtil().isMock(expected));
Стало:
import static org.mockito.internal.util.MockUtil.isMock;
assertFalse(isMock(expected));
Новый вариант и правда лучше, не поспоришь. Не нужно создавать ненужный объект.
На какие грабли ты наступишь
При переходе на Mockito 2.1 cотня-другая твоих тестов сломается, потому что:
1) Матчер any()
больше не срабатывает на null
Раньше это работало:
doNothing().when(service).calculateAmounts(any(BigDecimal.class), anyString());
Теперь не работает, если хотя бы один из параметров - null. Да-да, мой друг, тебе придётся перелопатить все тесты, которые почему-то передают null вместо настоящего значения, и прописать там правильные значения.
Но это и к лучшему, ведь и правда новый код лучше:
doNothing().when(service).calculateAmounts(order.amount, order.currency);
или, может, так:
doNothing().when(service).calculateAmounts(new BigDecimal("100.00"), "EEK");
Забыл сказать: если там действительно должен передаваться null, то это в тесте надо прописать явно:
doNothing().when(service).calculateAmounts(any(BigDecimal.class), isNull());
2) Матчер anyInt() больше не срабатывает на параметр типа long
Работало с Mockito 1.x, падает с Mockito 2.x:
when(mqService.send(anyString(), anyInt())).thenReturn("transfer-ref");
Для Mockito 2.1 придётся поменять anyInt()
на anyLong()
:
when(mqService.send(anyString(), anyLong())).thenReturn("transfer-ref");
Да-да, мой друг, тебе придётся перелопатить все тесты, которые вместо long передают int и т.п. Но это и к лучшему, ведь эти тесты были неточные.
3) Ты обнаружишь у себя плохие тесты
Просто плохие. Негодные. Например, вот такой:
@Test
public void checkMoratoriumRunsSilentlyWhenNoMoratorium() {
doReturn("false").when(service).parseMoratoriumMessage(any(Mandate.class), any(LoanApplication.class));
...
service.checkForMoratorium(any(Mandate.class), any(LoanApplication.class)); // Какую хрень мы сюда передаём?
...
}
С Mockito 1.x этот тест работал, а с Mockito 2.1 уже не хочет. И правильно!
Очевидно, во второй строке хотели использовать mock
, а не any
:
service.checkForMoratorium(mock(Mandate.class), mock(LoanApplication.class));
Хотя мы-то с вами понимаем, что тут не нужен ни mock
, ни any
, а достаточно просто создать объекты:
service.checkForMoratorium(new Mandate(), new LoanApplication());
4) Ты обнаружишь у себя много неряшливых тестов
… которые проверяют лишь часть параметров и не замечают, что остальные - null.
doReturn(user).when(loginService).tokenLogin(eq("bob"), eq("login-key"), anyString());
security.login("bob", "login-key", null);
Как видите, во второй строке тест передаёт параметр null
. И только null
.
Расследование показало, что ни один тест в системе не передавал туда ничего, кроме null.
Новый тест гораздо точнее, и не нужны все эти eq
и anyString
:
request.remoteAddress = "127.0.0.2";
doReturn(user).when(loginService).tokenLogin("bob", "login-key", "127.0.0.2");
...
5) Ты обнаружишь мистические красные тесты
Ты обнаружишь красные тесты, причину падения которых очень сложно раскопать
Например, такой:
@Test
public void requestByReferenceNumberNeverCreatesSubscription() {
RequestByReferenceNumber requestByReferenceNumber = new RequestByReferenceNumber(user, "12345678901234567890");
when(gisgmpService.request(any(RequestByDrivingLicense.class))).thenReturn(requestByReferenceNumber);
GISGMP.requestCharges("12345678901234567890");
...
С этим я долго провозился. Я не мог понять, почему он перестал работать.
Обратите внимание на вторую строку. Очевидно, там хотели написать не
any(RequestByDrivingLicense.class)
, а any(RequestByReferenceNumber.class)
(они оба наследуют один суперкласс).
Это похоже на багу Mockito 1: он позволял использовать any(НеверныйКласс.class)
,
и этот некорректный тест оставался зелёным несколько лет. :(
6) Ты нарвёшься на то, что anyList() и anyCollection() - теперь разные вещи
Например, со старым mockito этот тест работал:
@Test
public void domesticPaymentInForeignCurrencyCanBeEnabled() {
doCallRealMethod().when(Payments.accountService).forOperation(anyList(), eq(DOMESTIC));
Collection<Account> accounts = ...
return accountService.forOperation(accounts, DOMESTIC);
Обратите внимание, что в моке в первой строчке используется anyList()
,
а на самом деле передаётся переменная accounts типа Collection
(хотя в душе она List).
Mockito 2 больше не позволяет таких шалостей. Изволь прописать anyCollection()
.
И будешь доволен как слон
В общем, мой друг, придётся помучаться, но в конце будешь доволен. Тесты стали лучше, мир стал светлее.