2009-03-10 5 views
122

Как вы тестируете методы, которые запускают асинхронные процессы с Junit?Как использовать Junit для тестирования асинхронных процессов

Я не знаю, как сделать мой тест ждать, пока процесс до конца (это не совсем модульный тест, это больше похоже на интеграционном тест, как она включает в себя несколько классов, а не только один)

+0

Вы можете попробовать JAT (Java Asynchronous Test): https://bitbucket.org/csolar/jat – cs0lar

+2

JAT имеет 1 наблюдателя и не обновлялся через 1,5 года. Awaitility была обновлена ​​всего 1 месяц назад и находится на версии 1.6 на момент написания этой статьи. Я не связан ни с одним из проектов, но если бы я собирался инвестировать в дополнение к моему проекту, я бы дал больше доверия к Awaitility в это время. –

+0

JAT до сих пор не обновляется: «Последнее обновление 2013-01-19». Просто сохраните время, чтобы перейти по ссылке. – deamon

ответ

40

IMHO Плохая практика заключается в том, что модульные тесты создают или ждут потоки и т. Д. Тебе нужны эти тесты в течение нескольких секунд. Вот почему я хотел бы предложить двухэтапный подход к тестированию асинхронных процессов.

  1. Проверьте, что ваш процесс асинхронной передачи отправлен правильно. Вы можете издеваться над объектом, который принимает ваши асинхронные запросы, и убедиться, что отправленное задание имеет правильные свойства и т. Д.
  2. Проверьте, что ваши асинхронные обратные вызовы делают правильные действия. Здесь вы можете высмеять первоначально поданное задание и предположить, что оно правильно инициализировано и убедитесь, что ваши обратные вызовы верны.
+95

Конечно. Но иногда вам нужно проверить код, который специально предназначен для управления потоками. – lacker

+0

Оберните этот код в макет интерфейса: D – adam

+0

Я пишу фреймворк обмена сообщениями, это три уровня, прикладной уровень, уровень доступа к данным и транспортный уровень. Уровень приложения обрабатывает состояние сигнализации, уровень доступа к данным обрабатывает адресацию. Они не зависят от сетевых технологий, поэтому у меня есть для них модульные тесты. У меня также есть интеграционные тесты для всего, что работает на транспортном уровне на основе памяти. Как проверить сетевой транспортный уровень? Я могу запустить его в loopback, имея только миллисекунды задержки, но это все еще асинхронно. –

20

Запустите процесс и подождите результат с помощью Future.

136

Альтернативой является использование класса CountDownLatch.

public class DatabaseTest { 

    /** 
    * Data limit 
    */ 
    private static final int DATA_LIMIT = 5; 

    /** 
    * Countdown latch 
    */ 
    private CountDownLatch lock = new CountDownLatch(1); 

    /** 
    * Received data 
    */ 
    private List<Data> receiveddata; 

    @Test 
    public void testDataRetrieval() throws Exception { 
     Database db = new MockDatabaseImpl(); 
     db.getData(DATA_LIMIT, new DataCallback() { 
      @Override 
      public void onSuccess(List<Data> data) { 
       receiveddata = data; 
       lock.countDown(); 
      } 
     }); 

     lock.await(2000, TimeUnit.MILLISECONDS); 

     assertNotNull(receiveddata); 
     assertEquals(DATA_LIMIT, receiveddata.size()); 
    } 
} 

Примечания вы не можете просто использовали синхронизирован с обычным объектом как замок, так же быстро, обратные вызовы могут освободить блокировку перед методом ждать Локка называются. См. Сообщение this блога Джо Уолнса.

EDIT Удалены синхронизированы блоки вокруг CountDownLatch благодаря комментариям от @jtahlborn и @Ring

+7

, пожалуйста, не следуйте этому примеру, это неверно. вам не следует синхронизировать с CountDownLatch, поскольку он защищает поточную безопасность. – jtahlborn

+1

Это был хороший совет вплоть до синхронизированной части, которая съела, вероятно, около 3-4 часов отладки. http://stackoverflow.com/questions/11007551/synchronize-is-blocking-when-i-try-to-countdown#11007569 – Ring

+1

Извините за ошибку. Я отредактировал ответ соответствующим образом. – Martin

53

Вы можете попробовать использовать Awaitility библиотеки. Это позволяет легко тестировать системы, о которых вы говорите.

+2

Дружественный отказ от ответственности: Йохан является основным вкладчиком в проект. – dbm

4

Как насчет вызова SomeObject.wait и notifyAll, как описано here OR с использованием RobotiumsSolo.waitForCondition(...) метод или использовать class i wrote, чтобы сделать это (см комментарии и тестовый класс для того, как использовать)

+1

Проблема с подходом wait/notify/interrupt заключается в том, что тестируемый вами код может помешать ожиданиям (я видел, как это происходит). Вот почему [ConcurrentUnit] (https://github.com/jhalterman/concurrentunit) использует частную [схему] (https://github.com/jhalterman/concurrentunit/blob/master/src/main/java/net/ jodah/concurrentunit/Waiter.java # L18), на которые могут ждать потоки, которые не могут быть непреднамеренно помечены прерываниями для основного тестового потока. – Jonathan

13

Один метод я нашел очень полезно для тестирования асинхронных методов встраивается экземпляр Executor в конструктор объекта-объекта. В процессе производства экземпляр-исполнитель настроен для асинхронного запуска, в то время как в тесте его можно издеваться, чтобы работать синхронно.

Так предположу, что я пытаюсь проверить асинхронный метод Foo#doAsync(Callback c),

class Foo { 
    private final Executor executor; 
    public Foo(Executor executor) { 
    this.executor = executor; 
    } 

    public void doAsync(Callback c) { 
    executor.execute(new Runnable() { 
     @Override public void run() { 
     // Do stuff here 
     c.onComplete(data); 
     } 
    }); 
    } 
} 

В производстве, я бы построить Foo с экземпляром Executors.newSingleThreadExecutor() палача в то время как в тесте, я бы, наверное, построить его с синхронным исполнителем, что делает следующее -

class SynchronousExecutor implements Executor { 
    @Override public void execute(Runnable r) { 
    r.run(); 
    } 
} 

Теперь мой JUnit тест асинхронного метода довольно чистый -

@Test public void testDoAsync() { 
    Executor executor = new SynchronousExecutor(); 
    Foo objectToTest = new Foo(executor); 

    Callback callback = mock(Callback.class); 
    objectToTest.doAsync(callback); 

    // Verify that Callback#onComplete was called using Mockito. 
    verify(callback).onComplete(any(Data.class)); 

    // Assert that we got back the data that we expected. 
    assertEquals(expectedData, callback.getData()); 
} 
33

Если вы используете CompletableFuture (введен в Java 8) или SettableFuture (от Google Guava), вы можете сделать свой тест закончить, как только это будет сделано, а не ждать, заранее установленное количество времени. Ваш тест будет выглядеть примерно так:

CompletableFuture<String> future = new CompletableFuture<>(); 
executorService.submit(new Runnable() {   
    @Override 
    public void run() { 
     future.complete("Hello World!");     
    } 
}); 
assertEquals("Hello World!", future.get()); 
+3

... и если вы застряли с java-less-than-eight try guavas [SettableFuture] (http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/util/ concurrent/SettableFuture.html), который делает почти то же самое –

4

Там нет ничего плохого в тестировании резьбовым кода/асинхронного, особенно если потоковый является точкой кода, который вы тестируете. Общий подход к тестированию этого материала заключается в следующем:

  • Блок основной тестовой нити
  • Capture не удалось утверждения из других потоков
  • Разблокировать основной тест нить
  • Rethrow любые неудачи

Но это много шаблонов для одного теста. Лучше/простой подход просто использовать ConcurrentUnit:

final Waiter waiter = new Waiter(); 

    new Thread(() -> { 
    doSomeWork(); 
    waiter.assertTrue(true); 
    waiter.resume(); 
    }).start(); 

    // Wait for resume() to be called 
    waiter.await(1000); 

Преимущество этого над CountdownLatch подхода заключается в том, что это менее многословным, поскольку неудачи утверждение, что происходят в любом потоке правильно сообщается в основной поток, то есть тест терпит неудачу, когда это необходимо. Запись, сравнивающая подход CountdownLatch к ConcurrentUnit, - here.

Я также написал blog post по теме для тех, кто хочет узнать немного подробнее.

2

Я предпочитаю использовать wait и уведомлять. Это просто и понятно.

@Test 
public void test() throws Throwable { 
    final boolean[] asyncExecuted = {false}; 
    final Throwable[] asyncThrowable= {null}; 

    // do anything async 
    new Thread(new Runnable() { 
     @Override 
     public void run() { 
      try { 
       // Put your test here. 
       fail(); 
      } 
      // lets inform the test thread that there is an error. 
      catch (Throwable throwable){ 
       asyncThrowable[0] = throwable; 
      } 
      // ensure to release asyncExecuted in case of error. 
      finally { 
       synchronized (asyncExecuted){ 
        asyncExecuted[0] = true; 
        asyncExecuted.notify(); 
       } 
      } 
     } 
    }).start(); 

    // Waiting for the test is complete 
    synchronized (asyncExecuted){ 
     while(!asyncExecuted[0]){ 
      asyncExecuted.wait(); 
     } 
    } 

    // get any async error, including exceptions and assertationErrors 
    if(asyncThrowable[0] != null){ 
     throw asyncThrowable[0]; 
    } 
} 

В принципе, нам нужно создать окончательную ссылку на массив, который будет использоваться внутри анонимного внутреннего класса. Я бы предпочел создать boolean [], потому что я могу установить значение для контроля, если нам нужно ждать(). Когда все будет сделано, мы просто освободим asyncExecuted.

+1

Если ваше утверждение не удается, основной тестовый поток не будет знать об этом. – Jonathan

+0

Спасибо за решение, помогает мне отлаживать код с подключением к сети. –

+0

@ Jonathan, я обновил код, чтобы поймать любое утверждение и исключение и сообщить об этом основному тестовому потоку. – Paulo

0

Если вы хотите протестировать логику, просто не проверяйте ее асинхронно.

Например, чтобы проверить этот код, который работает с результатами асинхронного метода.

public class Example { 
    private Dependency dependency; 

    public Example(Dependency dependency) { 
     this.dependency = dependency;    
    } 

    public CompletableFuture<String> someAsyncMethod(){ 
     return dependency.asyncMethod() 
       .handle((r,ex) -> { 
        if(ex != null) { 
         return "got exception"; 
        } else { 
         return r.toString(); 
        } 
       }); 
    } 
} 

public class Dependency { 
    public CompletableFuture<Integer> asyncMethod() { 
     // do some async stuff  
    } 
} 

В тесте обманывают зависимость с синхронной реализацией. Единичный тест полностью синхронный и работает в 150 мс.

public class DependencyTest { 
    private Example sut; 
    private Dependency dependency; 

    public void setup() { 
     dependency = Mockito.mock(Dependency.class);; 
     sut = new Example(dependency); 
    } 

    @Test public void success() throws InterruptedException, ExecutionException { 
     when(dependency.asyncMethod()).thenReturn(CompletableFuture.completedFuture(5)); 

     // When 
     CompletableFuture<String> result = sut.someAsyncMethod(); 

     // Then 
     assertThat(result.isCompletedExceptionally(), is(equalTo(false))); 
     String value = result.get(); 
     assertThat(value, is(equalTo("5"))); 
    } 

    @Test public void failed() throws InterruptedException, ExecutionException { 
     // Given 
     CompletableFuture<Integer> c = new CompletableFuture<Integer>(); 
     c.completeExceptionally(new RuntimeException("failed")); 
     when(dependency.asyncMethod()).thenReturn(c); 

     // When 
     CompletableFuture<String> result = sut.someAsyncMethod(); 

     // Then 
     assertThat(result.isCompletedExceptionally(), is(equalTo(false))); 
     String value = result.get(); 
     assertThat(value, is(equalTo("got exception"))); 
    } 
} 

Вы не тестируете асинхронное поведение, но можете проверить правильность логики.

0

Это то, что я использую сейчас, если результат теста производится асинхронно.

public class TestUtil { 

    public static <R> R await(Consumer<CompletableFuture<R>> completer) { 
     return await(20, TimeUnit.SECONDS, completer); 
    } 

    public static <R> R await(int time, TimeUnit unit, Consumer<CompletableFuture<R>> completer) { 
     CompletableFuture<R> f = new CompletableFuture<>(); 
     completer.accept(f); 
     try { 
      return f.get(time, unit); 
     } catch (InterruptedException | TimeoutException e) { 
      throw new RuntimeException("Future timed out", e); 
     } catch (ExecutionException e) { 
      throw new RuntimeException("Future failed", e.getCause()); 
     } 
    } 
} 

Использование статического импорта, тест читает добрый. (обратите внимание, в этом примере я начинаю нить, чтобы проиллюстрировать идею)

@Test 
    public void testAsync() { 
     String result = await(f -> { 
      new Thread(() -> f.complete("My Result")).start(); 
     }); 
     assertEquals("My Result", result); 
    } 

Если f.complete не вызывается, тест проваливается после тайм-аута. Вы также можете использовать f.completeExceptionally, чтобы провалиться раньше.

1

Есть много ответов здесь, но просто один, чтобы просто создать законченную CompletableFuture и использовать его:

CompletableFuture.completedFuture("donzo") 

Таким образом, в моем тесте:

this.exactly(2).of(mockEventHubClientWrapper).sendASync(with(any(LinkedList.class))); 
this.will(returnValue(new CompletableFuture<>().completedFuture("donzo"))); 

Я только убедившись, что все этот материал в любом случае называется. Этот метод работает, если вы используете этот код:

CompletableFuture.allOf(calls.toArray(new CompletableFuture[0])).join(); 

Он пронестись сквозь нее, как все CompletableFutures закончены!

1

Я нахожу библиотеку socket.io для проверки асинхронной логики. Это выглядит просто и непросто, используя LinkedBlockingQueue. Вот example:

@Test(timeout = TIMEOUT) 
public void message() throws URISyntaxException, InterruptedException { 
    final BlockingQueue<Object> values = new LinkedBlockingQueue<Object>(); 

    socket = client(); 
    socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() { 
     @Override 
     public void call(Object... objects) { 
      socket.send("foo", "bar"); 
     } 
    }).on(Socket.EVENT_MESSAGE, new Emitter.Listener() { 
     @Override 
     public void call(Object... args) { 
      values.offer(args); 
     } 
    }); 
    socket.connect(); 

    assertThat((Object[])values.take(), is(new Object[] {"hello client"})); 
    assertThat((Object[])values.take(), is(new Object[] {"foo", "bar"})); 
    socket.disconnect(); 
} 

Использование LinkedBlockingQueue принять API, чтобы блокировать до тех пор, чтобы получить результат, как и синхронным образом. И установите тайм-аут, чтобы избежать слишком много времени, чтобы ждать результата.

1

Стоит отметить, что очень полезно главе Testing Concurrent Programs в Concurrency in Practice, которая описывает некоторые подходы модульного тестирования и дает решения проблем ,

0

Избегайте тестирования параллельными нитями, когда вы можете (что большую часть времени). Это только сделает ваши тесты шероховатыми (иногда проходят, иногда терпят неудачу).

Только тогда, когда вам нужно позвонить в другую библиотеку/систему, вам, возможно, придется ждать других потоков, в этом случае всегда используйте библиотеку Awaitility вместо Thread.sleep().

Никогда не вызывайте в своих тестах get() или join(), иначе ваши тесты могут запускаться вечно на вашем сервере CI в случае, если будущее никогда не завершится. Всегда указывайте isDone() первым в своих тестах перед вызовом get(). Для CompletionStage, то есть .toCompletableFuture().isDone().

При тестировании метода нелипкая так:

public static CompletionStage<Foo> doSomething(BarService service) { 
    CompletionStage<Bar> future = service.getBar(); 
    return future.thenApply(bar -> fooToBar()); 
} 

, то вы должны не только проверить результат, передавая законченное будущее в тесте, вы также должны убедиться, что ваш метод doSomething() не по вызову join() или get(). Это важно, особенно если вы используете неблокирующую структуру.

Чтобы сделать это, тест с не завершенным будущим, которые вы установили для завершения вручную:

@Test 
public void testDoSomething() { 
    CompletableFuture<Bar> innerFuture = new CompletableFuture<>(); 
    fooResult = doSomething(() -> innerFuture).toCompletableFuture(); 
    assertFalse(fooResult.isDone()); 

    // this triggers the future to complete 
    innerFuture.complete(new Bar()); 
    assertTrue(fooResult.isDone()); 

    // futher asserts about fooResult here 
} 

Таким образом, если добавить к future.join() DoSomething(), тест проваливается.

Если служба использует ExecutorService, например, в thenApplyAsync(..., executorService), то в ваших тесты вводят однопоточный ExecutorService, например, как один из гуавы:

ExecutorService executorService = Executors.newSingleThreadExecutor(); 

Если ваш код использует forkJoinPool таких как thenApplyAsync(...), перепишите код для использования ExecutorService (есть много веских причин) или используйте Awaitility.

Чтобы сократить пример, я сделал BarService аргументом метода, реализованным в качестве лямбды Java8 в тесте, как правило, это будет введенная ссылка, которую вы бы издевались.