Для начала посмотрим, как вообще можно проверить факт вызова callback-метода с помощью Mockito. Допустим у нас есть некий listener-интерфейс с методом onMessage. Этот метод вызывается, когда к нам по сети приходит сообщение. Получение сообщения мы сымитируем созданием отдельного потока и вызовом callback-метода в нем.
public final class Message { private final int id; public Message(int id) { this.id = id; } public int getId() { return id; } } public interface MessageListener { void onMessage(Message message); } public class Test { private MessageListener listener; @Before public void setUp() throws Exception { listener = mock(MessageListener.class); } @Test public void test() throws Exception { sendMessage(new Message(1)); ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class); verify(listener).onMessage(messageCaptor.capture()); assertEquals(1, messageCaptor.getValue().getId()); } private void sendMessage(final Message message) { new Thread() { @Override public void run() { try { Thread.sleep(100); } catch (InterruptedException e) { } listener.onMessage(new Message(message.getId())); } }.start(); } }
Этот тест почти наверняка не пройдет, верификация вызова метода onMessage отработает быстрее, чем метод будет вызван. Можно, конечно, поставить задержку перед проверкой или воспользоваться режимом верификации timeout. Но, во-первых, непонятно, какое минимальное время задержки выбрать, чтобы тест гарантированно проходил. Во-вторых, если подобных тестов много, то это может сильно увеличить время, которое будет затрачено на исполнение всех тестов: даже если асинхронный вызов отработал быстро, задержка все равно будет присутствовать.
Попробуем исправить ситуацию. Сделаем заглушку для метода onMessage, в которой будем нотифицировать поток теста, а в потоке теста будем, соответственно, ожидать нотификацию и выполнять верификацию вызова в цикле до тех пор, пока верификация не пройдет, или когда закончится время ожидания завершения теста.
@Before public void setUp() throws Exception { listener = mock(MessageListener.class); doAnswer(new Answer<Void>() { @Override public Void answer(InvocationOnMock invocation) throws Throwable { synchronized (listener) { listener.notifyAll(); } return null; } }).when(listener).onMessage(any(Message.class)); } @Test(timeout = 10000) public void test() throws Exception { sendMessage(new Message(1)); ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class); while (true) { synchronized (listener) { try { verify(listener).onMessage(messageCaptor.capture()); break; } catch (TooLittleActualInvocations | WantedButNotInvoked e) { } try { listener.wait(); } catch (InterruptedException e) { throw new MockitoAssertionError("interrupted"); } } } assertEquals(1, messageCaptor.getValue().getId()); }
Данное решение работает, но выглядит не очень красиво. А если таких асинхронных вызовов несколько, то также возникнет много дублирующегося кода.
Mockito предоставляет богатые возможности для расширения, поэтому служебный код, не относящийся непосредственно тесту, мы можем спрятать «под капот» и сделать его повторно используемым. Для этого мы повесим на наш mock-объект специальный слушатель, который будет нотифицироваться каждый раз, когда будет вызываться какой-либо метод данного mock-объекта. В этот слушатель мы спрячем код нашей заглушки.
@Before public void setUp() throws Exception { listener = mock(MessageListener.class, async()); } private static MockSettings async() { return withSettings().defaultAnswer(RETURNS_DEFAULTS).invocationListeners( new InvocationListener() { @Override public void reportInvocation(MethodInvocationReport methodInvocationReport) { DescribedInvocation invocation = methodInvocationReport.getInvocation(); if (invocation instanceof InvocationOnMock) { Object mock = ((InvocationOnMock) invocation).getMock(); synchronized (mock) { mock.notifyAll(); } } } }); }
Также мы напишем свою реализацию интерфейса VerificationMode, поместив туда логику верификации асинхронного вызова.
public final class Await implements VerificationMode { private final VerificationMode delegate; public Await() { this(Mockito.times(1)); } public Await(VerificationMode delegate) { this.delegate = delegate; } @Override public void verify(VerificationData data) { Object mock = data.getWanted().getInvocation().getMock(); while (true) { synchronized (mock) { try { delegate.verify(data); break; } catch (TooLittleActualInvocations | WantedButNotInvoked e) { } try { mock.wait(); } catch (InterruptedException e) { throw new MockitoAssertionError("interrupted"); } } } } }
И модифицируем наш первоначальный тест.
@Test(timeout = 10000) public void test() throws Exception { sendMessage(new Message(1)); ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class); verify(listener, await()).onMessage(messageCaptor.capture()); assertEquals(1, messageCaptor.getValue().getId()); } private static VerificationMode await() { return new Await(); }
Теперь мы можем легко тестировать асинхронные вызовы, сохраняя читабельность кода тестов. Тесты при этом будут выглядеть так, как будто асинхронные вызовы в тестируемом коде отсутствуют.
ссылка на оригинал статьи http://habrahabr.ru/post/169621/
Добавить комментарий