Тестирование асинхронных вызовов при помощи Mockito

от автора

Тестирование кода, содержащего асинхронные вызовы, представляет собой определенную проблему. Callback-методы как правило получают управление в потоке, отличном от основного потока, в котором работает код теста. И чтобы проверить, был ли вызван такой метод с нужными параметрами, приходится прилагать некоторые усилия. При этом код теста получается громоздким и трудным для понимания. В статье предлагается решение данной проблемы с помощью библиотеки для тестирования Mockito и небольшого расширения к ней.

Для начала посмотрим, как вообще можно проверить факт вызова 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *