Как проверить вызовы одного и того же метода mock с тем же аргументом, который изменяет состояние между invocations в mockito?

У меня есть следующий код для модульного тестирования:

public void foo() {
    Entity entity = //...
    persistence.save(entity);
    entity.setDate(new Date());
    persistence.save(entity);
}

Я хотел бы проверить, что при первом вызове persistence.save entity.getDate() возвращает null.

Поэтому я не могу использовать Mockito.verify(/*...*/), потому что в тот момент был вызван метод foo и entity.setDate(Date).

Итак, я думаю, что мне нужно делать проверки вызовов уже в то время, когда происходят вызовы. Как это сделать с помощью Mockito?

Ответ 1

Я создал следующую реализацию Answer:

public class CapturingAnswer<T, R> implements Answer<T> {

    private final Function<InvocationOnMock, R> capturingFunction;

    private final List<R> capturedValues = new ArrayList<R>();

    public CapturingAnswer(final Function<InvocationOnMock, R> capturingFunction) {
        super();
        this.capturingFunction = capturingFunction;
    }

    @Override
    public T answer(final InvocationOnMock invocation) throws Throwable {
        capturedValues.add(capturingFunction.apply(invocation));
        return null;
    }

    public List<R> getCapturedValues() {
        return Collections.unmodifiableList(capturedValues);
    }

}

В этом ответе фиксируются свойства вызываемых вызовов. Тогда capturedValues можно использовать для простых утверждений. В реализации используется Java 8 API. Если это не доступно, нужно использовать интерфейс, способный преобразовать InvocationOnMock в захваченное значение. Использование в тестовом сценарии выглядит следующим образом:

@Test
public void testSomething() {
    CapturingAnswer<Void,Date> captureDates = new CapturingAnswer<>(this::getEntityDate)
    Mockito.doAnswer(captureDates).when(persistence).save(Mockito.any(Entity.class));

    service.foo();

    Assert.assertNull(captureDates.getCapturedValues().get(0));
}

private Date getEntityDate(InvocationOnMock invocation) {
    Entity entity = (Entity)invocation.getArguments()[0];
    return entity.getDate();
}

Захват, выполняемый представленной реализацией Answer, не может быть достигнут с помощью Mockitos ArgumentCaptor, потому что это используется только после вызова тестируемого метода.

Ответ 2

В моем первоначальном комментарии это был ответ, который я имел в виду.

Класс для издевательства:

class MockedClass{
    void save(SomeBean sb){
        //doStuff
    }
}

Класс, который нам нужен для проверки объекта Date, имеет значение null.

class SomeBean{
    Date date;

    Date getDate(){return date;}

    void setDate(Date date){this.date=date;}
}

Испытуемый класс:

class TestClass{
    MockedClass mc;

    TestClass(MockedClass mc){this.mc = mc;}

    void doWork(){
        SomeBean sb = new SomeBean();
        mc.save(sb);
        sb.setDate(new Date());
        mc.save(sb);
    }
}

И тестовый пример:

@Test
public void testAnswer(){
    MockedClass mc = Mockito.mock(MockedClass.class);
    Mockito.doAnswer(new Answer<Void>(){
        boolean checkDate = true;
        @Override
        public Void answer(InvocationOnMock invocation) throws Throwable {
            SomeBean sb = (SomeBean) invocation.getArguments()[0];
            if(checkDate && sb.getDate() != null){
                throw new NullPointerException(); //Or a more meaningful exception
            }
            checkDate = false;
            return null;
        }}).when(mc).save(Mockito.any(SomeBean.class));;

    TestClass tc =  new TestClass(mc);
    tc.doWork();
}

В первый раз через этот Answer (термин, который я должен был использовать в своем исходном комментарии), это вызовет исключение и завершит тест, если дата не равна нулю. Второй раз через checkDate будет false, поэтому проверка не будет выполнена.