Миграция на Mockito 2.1

На днях мир был ошеломлён неожиданной новостью.

Шутки ли, вышла Mockito 2.1.0 - после стольких лет ожиданий! Признаться, я уж было отчаялся.

В Mockito 2 обещается много всяких вкусняшек, включая:

Ура! Надо брать!

Какое же меня ждало разочарование…

Тотальный облом

Я попробовал перевести на 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().

И будешь доволен как слон

В общем, мой друг, придётся помучаться, но в конце будешь доволен. Тесты стали лучше, мир стал светлее.

Андрей Солнцев

asolntsev.github.io