Cara menggunakan JUnit untuk menguji proses asinkron

204

Bagaimana Anda menguji metode yang menjalankan proses asinkron dengan JUnit?

Saya tidak tahu bagaimana membuat tes saya menunggu proses berakhir (ini bukan tes unit, ini lebih seperti tes integrasi karena melibatkan beberapa kelas dan bukan hanya satu).

Sam
sumber
Anda bisa mencoba JAT (Java Asynchronous Test): bitbucket.org/csolar/jat
cs0lar
2
JAT memiliki 1 pengamat dan belum diperbarui dalam 1,5 tahun. Awaitility telah diperbarui hanya 1 bulan yang lalu dan ada di versi 1.6 pada saat penulisan ini. Saya tidak berafiliasi dengan salah satu proyek, tetapi jika saya akan berinvestasi dalam tambahan untuk proyek saya, saya akan memberikan kepercayaan yang lebih kepada Awaitility saat ini.
Les Hazlewood
JAT masih belum memiliki pembaruan: "Terakhir diperbarui 2013-01-19". Cukup hemat waktu untuk mengikuti tautan.
deamon
@LesHazlewood, satu pengamat buruk untuk JAT, tetapi hampir tidak ada pembaruan selama bertahun-tahun ... Hanya satu contoh. Seberapa sering Anda memperbarui tumpukan TCP tingkat rendah dari OS Anda, jika hanya berfungsi? Alternatif untuk JAT dijawab di bawah ini stackoverflow.com/questions/631598/… .
user1742529

Jawaban:

46

IMHO itu praktik buruk untuk membuat unit test membuat atau menunggu utas, dll. Anda ingin tes ini berjalan dalam hitungan detik. Itu sebabnya saya ingin mengusulkan pendekatan 2 langkah untuk menguji proses async.

  1. Uji bahwa proses async Anda dikirimkan dengan benar. Anda dapat mengejek objek yang menerima permintaan async Anda dan memastikan bahwa pekerjaan yang dikirimkan memiliki properti yang benar, dll.
  2. Uji apakah panggilan balik async Anda melakukan hal yang benar. Di sini Anda dapat mengejek pekerjaan yang semula diajukan dan menganggapnya diinisialisasi dengan benar dan memverifikasi bahwa panggilan balik Anda sudah benar.
Cem Catikkas
sumber
148
Tentu. Tetapi kadang-kadang Anda perlu menguji kode yang secara khusus seharusnya mengelola utas.
kurang
77
Bagi kita yang menggunakan Junit atau TestNG untuk melakukan pengujian integrasi (dan bukan hanya pengujian unit), atau pengujian penerimaan pengguna (misalnya dengan Ketimun), menunggu penyelesaian async dan memverifikasi hasilnya benar-benar diperlukan.
Les Hazlewood
38
Proses asinkron adalah beberapa kode yang paling rumit untuk dikerjakan dengan benar dan Anda mengatakan Anda tidak boleh menggunakan pengujian unit untuk mereka dan hanya menguji dengan satu utas? Itu ide yang sangat buruk.
Charles
18
Tes tiruan sering gagal membuktikan bahwa fungsionalitas berfungsi ujung ke ujung. Fungsionalitas Async perlu diuji secara asinkron untuk memastikannya berfungsi. Sebut saja tes integrasi jika Anda mau, tetapi ini adalah tes yang masih diperlukan.
Scott Boring
4
Ini seharusnya bukan jawaban yang diterima. Pengujian melampaui pengujian unit. OP menyebutnya lebih sebagai Tes Integrasi daripada Tes Unit.
Jeremiah Adams
190

Alternatifnya adalah menggunakan kelas 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());
    }
}

CATATAN Anda tidak bisa hanya menggunakan sinkronisasi dengan objek biasa sebagai kunci, karena panggilan balik cepat dapat melepaskan kunci sebelum metode tunggu kunci dipanggil. Lihat ini posting blog oleh Joe Walnes.

EDIT Menghapus blok yang disinkronkan di sekitar CountDownLatch berkat komentar dari @jtahlborn dan @Ring

Martin
sumber
8
tolong jangan ikuti contoh ini, ini salah. Anda tidak boleh menyinkronkan pada CountDownLatch karena menangani keamanan thread secara internal.
jtahlborn
1
Itu adalah saran yang bagus sampai bagian yang disinkronkan, yang memakan waktu hampir 3-4 jam waktu debugging. stackoverflow.com/questions/11007551/…
Ring
2
Permintaan maaf atas kesalahan ini. Saya sudah mengedit jawaban dengan tepat.
Martin
7
Jika Anda memverifikasi bahwa onSuccess dipanggil, Anda harus menyatakan bahwa lock.await mengembalikan true.
Gilbert
1
@ Martin itu akan benar, tetapi itu berarti Anda memiliki masalah berbeda yang perlu diperbaiki.
George Aristy
75

Anda dapat mencoba menggunakan perpustakaan Awaitility . Mudah untuk menguji sistem yang sedang Anda bicarakan.

Johan
sumber
21
Penafian ramah: Johan adalah kontributor utama proyek ini.
dbm
1
Menderita masalah mendasar karena harus menunggu (unit test harus berjalan cepat ). Idealnya Anda benar-benar tidak ingin menunggu milidetik lebih lama dari yang dibutuhkan, jadi saya pikir menggunakan CountDownLatch(lihat jawaban oleh @Martin) lebih baik dalam hal ini.
George Aristy
Sangat mengagumkan.
R. Karlus
Ini adalah pustaka sempurna yang memenuhi persyaratan uji integrasi proses async saya. Sangat mengagumkan. Perpustakaan tampaknya terpelihara dengan baik dan memiliki fitur mulai dari dasar hingga lanjutan yang saya yakin cukup untuk memenuhi sebagian besar skenario. Terima kasih untuk referensi yang luar biasa!
Tanvir
Saran yang sangat mengagumkan. Terima kasih
RoyalTiger
68

Jika Anda menggunakan CompletableFuture (diperkenalkan di Java 8) atau SettableFuture (dari Google Guava ), Anda dapat menyelesaikan tes segera setelah selesai, daripada menunggu waktu yang telah ditentukan sebelumnya. Tes Anda akan terlihat seperti ini:

CompletableFuture<String> future = new CompletableFuture<>();
executorService.submit(new Runnable() {         
    @Override
    public void run() {
        future.complete("Hello World!");                
    }
});
assertEquals("Hello World!", future.get());
pengguna393274
sumber
4
... dan jika Anda terjebak dengan java-kurang-delapan coba jambu SettableFuture yang melakukan hal yang hampir sama
Markus T
23

Mulai proses dan tunggu hasilnya menggunakan a Future.

Tom Hawtin - tackline
sumber
18

Salah satu metode yang saya temukan sangat berguna untuk menguji metode asinkron adalah menyuntikkan Executor contoh dalam konstruktor objek-ke-tes. Dalam produksi, instance pelaksana dikonfigurasikan untuk berjalan secara tidak sinkron sementara dalam pengujian dapat diejek untuk dijalankan secara sinkron.

Jadi misalkan saya mencoba untuk menguji metode asinkron 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);
      }
    });
  }
}

Dalam produksi, saya akan membangun Foo dengan Executors.newSingleThreadExecutor()contoh Executor sementara dalam pengujian saya mungkin akan membangunnya dengan eksekutor sinkron yang melakukan hal berikut -

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

Sekarang tes JUnit saya tentang metode asinkron cukup bersih -

@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());
}
Mat
sumber
Tidak berfungsi jika saya ingin menguji integrasi sesuatu yang melibatkan panggilan perpustakaan asinkron seperti SpringWebClient
Stefan Haberl
8

Tidak ada yang salah dengan pengujian kode ulir / async, terutama jika threading adalah titik dari kode yang Anda uji. Pendekatan umum untuk menguji hal ini adalah dengan:

  • Blokir utas tes utama
  • Menangkap pernyataan gagal dari utas lainnya
  • Buka blokir utas tes utama
  • Rethrow semua kegagalan

Tapi itu banyak boilerplate untuk satu tes. Pendekatan yang lebih baik / sederhana adalah dengan hanya menggunakan ConcurrentUnit :

  final Waiter waiter = new Waiter();

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

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

Manfaat dari CountdownLatchpendekatan ini adalah bahwa itu kurang bertele-tele karena kegagalan pernyataan yang terjadi pada setiap utas dilaporkan dengan benar ke utas utama, artinya pengujian gagal ketika seharusnya. Lelang yang membandingkan CountdownLatchpendekatan ke ConcurrentUnit ada di sini .

Saya juga menulis posting blog tentang topik untuk mereka yang ingin belajar sedikit lebih detail.

Jonathan
sumber
solusi serupa yang saya gunakan di masa lalu adalah github.com/MichaelTamm/junit-toolbox , juga ditampilkan sebagai ekstensi pihak ketiga di junit.org/junit4
dschulten
4

Bagaimana dengan menelepon SomeObject.waitdan notifyAllseperti yang dijelaskan di sini ATAU menggunakan metode Robotium Solo.waitForCondition(...) ATAU menggunakan kelas yang saya tulis untuk melakukan ini (lihat komentar dan kelas uji untuk cara menggunakan)

Dori
sumber
1
Masalah dengan pendekatan wait / notify / interrupt adalah bahwa kode yang Anda uji berpotensi mengganggu jalinan tunggu (saya sudah melihatnya terjadi). Inilah sebabnya ConcurrentUnit menggunakan sirkuit pribadi yang dapat ditunggu oleh utas, yang tidak dapat secara tidak sengaja terganggu oleh interupsi pada utas uji utama.
Jonathan
3

Saya menemukan socket.io perpustakaan untuk menguji logika asinkron. Ini terlihat sederhana dan cara singkat menggunakan LinkedBlockingQueue . Berikut ini contohnya :

    @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();
}

Menggunakan LinkedBlockingQueue, ambil API untuk memblokir hingga mendapatkan hasil seperti cara sinkron. Dan atur batas waktu untuk menghindari asumsi terlalu banyak waktu untuk menunggu hasilnya.

Fantasi Fang
sumber
1
Pendekatan yang luar biasa!
aminografi
3

Perlu disebutkan bahwa ada bab yang sangat berguna Testing Concurrent Programsdalam Concurrency in Practice yang menjelaskan beberapa pendekatan pengujian unit dan memberikan solusi untuk masalah.

sebelas
sumber
1
Pendekatan apa itu? Bisakah Anda memberi contoh?
Bruno Ferreira
2

Inilah yang saya gunakan saat ini jika hasil tes diproduksi secara tidak sinkron.

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());
        }
    }
}

Menggunakan impor statis, tesnya terbilang bagus. (perhatikan, dalam contoh ini saya memulai utas untuk menggambarkan ide)

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

Jika f.completetidak dipanggil, tes akan gagal setelah batas waktu. Anda juga bisa menggunakan f.completeExceptionallyuntuk gagal lebih awal.

Jochen Bedersdorfer
sumber
2

Ada banyak jawaban di sini tetapi yang sederhana adalah dengan membuat CompletableFuture yang lengkap dan menggunakannya:

CompletableFuture.completedFuture("donzo")

Jadi dalam pengujian saya:

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

Saya hanya memastikan semua hal ini dipanggil. Teknik ini berfungsi jika Anda menggunakan kode ini:

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

Ini akan zip melalui itu karena semua CompletableFutures selesai!

markthegrea
sumber
2

Hindari pengujian dengan utas paralel kapan pun Anda bisa (yang sebagian besar waktu). Ini hanya akan membuat tes Anda terkelupas (terkadang lulus, kadang gagal).

Hanya ketika Anda perlu memanggil beberapa pustaka / sistem lain, Anda mungkin harus menunggu utas lainnya, dalam hal ini selalu gunakan pustaka Awaitility sebagai ganti Thread.sleep().

Jangan hanya menelepon get()atau join()dalam pengujian Anda, kalau tidak tes Anda akan berjalan selamanya di server CI Anda jika masa depan tidak pernah selesai. Selalu tegaskan isDone()dulu dalam tes Anda sebelum menelepon get(). Untuk CompletionStage, yaitu .toCompletableFuture().isDone().

Saat Anda menguji metode non-pemblokiran seperti ini:

public static CompletionStage<String> createGreeting(CompletableFuture<String> future) {
    return future.thenApply(result -> "Hello " + result);
}

maka Anda tidak hanya harus menguji hasilnya dengan melewati Masa Depan yang telah selesai dalam tes, Anda juga harus memastikan bahwa metode doSomething()Anda tidak memblokir dengan menelepon join()atauget() . Ini penting khususnya jika Anda menggunakan kerangka kerja non-blocking.

Untuk melakukannya, uji dengan masa depan yang belum selesai yang Anda setel untuk diselesaikan secara manual:

@Test
public void testDoSomething() throws Exception {
    CompletableFuture<String> innerFuture = new CompletableFuture<>();
    CompletableFuture<String> futureResult = createGreeting(innerFuture).toCompletableFuture();
    assertFalse(futureResult.isDone());

    // this triggers the future to complete
    innerFuture.complete("world");
    assertTrue(futureResult.isDone());

    // futher asserts about fooResult here
    assertEquals(futureResult.get(), "Hello world");
}

Dengan begitu, jika Anda menambahkan future.join() doSomething (), tes akan gagal.

Jika Layanan Anda menggunakan ExecutorService seperti di thenApplyAsync(..., executorService), maka dalam tes Anda menyuntikkan ExecutorService berulir tunggal, seperti yang dari jambu biji:

ExecutorService executorService = Executors.newSingleThreadExecutor();

Jika kode Anda menggunakan forkJoinPool seperti thenApplyAsync(...) , tulis ulang kode untuk menggunakan ExecutorService (ada banyak alasan bagus), atau gunakan Awaitility.

Untuk mempersingkat contoh, saya menjadikan BarService argumen metode yang diimplementasikan sebagai lambda Java8 dalam pengujian, biasanya itu akan menjadi referensi yang disuntikkan yang akan Anda tiru.

tkruse
sumber
Hai @tkruse, mungkin Anda punya repo git publik dengan tes menggunakan teknik ini?
Cristiano
@Christiano: itu akan bertentangan dengan filosofi SO. Sebagai gantinya, saya mengubah metode untuk mengkompilasi tanpa kode tambahan (semua impor java8 + atau junit) ketika Anda menempelkannya ke kelas uji junit kosong. Jangan ragu untuk memperbaiki.
tkruse
Saya mengerti sekarang. Terima kasih. Masalah saya sekarang adalah untuk menguji ketika metode mengembalikan CompletableFuture tetapi menerima objek lain sebagai parameter selain CompletableFuture.
Cristiano
Dalam kasus Anda, siapa yang membuat CompletableFuture yang dikembalikan metode ini? Jika ini adalah layanan lain, itu bisa diejek dan teknik saya masih berlaku. Jika metode itu sendiri menciptakan CompletableFuture situasinya berubah sangat banyak sehingga Anda dapat mengajukan pertanyaan baru tentangnya. Itu kemudian tergantung pada utas apa yang akan menyelesaikan masa depan metode Anda kembali.
tkruse
1

Saya lebih suka menggunakan tunggu dan beri tahu. Sederhana dan jelas.

@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];
    }
}

Pada dasarnya, kita perlu membuat referensi Array akhir, untuk digunakan di dalam kelas batin anonim. Saya lebih suka membuat boolean [], karena saya bisa memberi nilai pada kontrol jika kita perlu menunggu (). Ketika semuanya selesai, kami hanya merilis asyncExecuted.

Paulo
sumber
1
Jika pernyataan Anda gagal, utas tes utama tidak akan mengetahuinya.
Jonathan
Terima kasih atas solusinya, membantu saya men-debug kode dengan koneksi websocket.
Nazar Sakharenko
@ Jonathan, saya memperbarui kode untuk menangkap pernyataan dan pengecualian dan menginformasikannya ke utas uji utama.
Paulo
1

Untuk semua pengguna Spring di luar sana, inilah yang biasanya saya lakukan tes integrasi saya saat ini, di mana perilaku async terlibat:

Memecat peristiwa aplikasi dalam kode produksi, ketika tugas async (seperti panggilan I / O) telah selesai. Sebagian besar waktu acara ini diperlukan untuk menangani respons operasi async dalam produksi.

Dengan kejadian ini di tempat, Anda dapat menggunakan strategi berikut dalam kasus uji Anda:

  1. Jalankan sistem yang sedang diuji
  2. Dengarkan acara dan pastikan bahwa acara telah dipecat
  3. Lakukan pernyataan Anda

Untuk memecah ini, pertama-tama Anda perlu semacam acara domain untuk memecat. Saya menggunakan UUID di sini untuk mengidentifikasi tugas yang telah selesai, tetapi Anda tentu saja bebas menggunakan sesuatu yang lain asalkan unik.

(Perhatikan, bahwa cuplikan kode berikut juga menggunakan anotasi Lombok untuk menghilangkan kode pelat ketel)

@RequiredArgsConstructor
class TaskCompletedEvent() {
  private final UUID taskId;
  // add more fields containing the result of the task if required
}

Kode produksi itu sendiri biasanya terlihat seperti ini:

@Component
@RequiredArgsConstructor
class Production {

  private final ApplicationEventPublisher eventPublisher;

  void doSomeTask(UUID taskId) {
    // do something like calling a REST endpoint asynchronously
    eventPublisher.publishEvent(new TaskCompletedEvent(taskId));
  }

}

Saya kemudian dapat menggunakan Spring @EventListeneruntuk menangkap acara yang diterbitkan dalam kode uji. Pendengar acara sedikit lebih terlibat, karena harus menangani dua kasus dengan cara yang aman:

  1. Kode produksi lebih cepat daripada test case dan event sudah dipecat sebelum test case memeriksa event, atau
  2. Kasing uji lebih cepat dari kode produksi dan kasing harus menunggu acara.

A CountDownLatchdigunakan untuk kasus kedua seperti yang disebutkan dalam jawaban lain di sini. Juga perhatikan, bahwa @Orderanotasi pada metode event handler memastikan, bahwa metode handler event ini dipanggil setelah pendengar acara lain yang digunakan dalam produksi.

@Component
class TaskCompletionEventListener {

  private Map<UUID, CountDownLatch> waitLatches = new ConcurrentHashMap<>();
  private List<UUID> eventsReceived = new ArrayList<>();

  void waitForCompletion(UUID taskId) {
    synchronized (this) {
      if (eventAlreadyReceived(taskId)) {
        return;
      }
      checkNobodyIsWaiting(taskId);
      createLatch(taskId);
    }
    waitForEvent(taskId);
  }

  private void checkNobodyIsWaiting(UUID taskId) {
    if (waitLatches.containsKey(taskId)) {
      throw new IllegalArgumentException("Only one waiting test per task ID supported, but another test is already waiting for " + taskId + " to complete.");
    }
  }

  private boolean eventAlreadyReceived(UUID taskId) {
    return eventsReceived.remove(taskId);
  }

  private void createLatch(UUID taskId) {
    waitLatches.put(taskId, new CountDownLatch(1));
  }

  @SneakyThrows
  private void waitForEvent(UUID taskId) {
    var latch = waitLatches.get(taskId);
    latch.await();
  }

  @EventListener
  @Order
  void eventReceived(TaskCompletedEvent event) {
    var taskId = event.getTaskId();
    synchronized (this) {
      if (isSomebodyWaiting(taskId)) {
        notifyWaitingTest(taskId);
      } else {
        eventsReceived.add(taskId);
      }
    }
  }

  private boolean isSomebodyWaiting(UUID taskId) {
    return waitLatches.containsKey(taskId);
  }

  private void notifyWaitingTest(UUID taskId) {
    var latch = waitLatches.remove(taskId);
    latch.countDown();
  }

}

Langkah terakhir adalah menjalankan sistem yang sedang diuji dalam suatu uji kasus. Saya menggunakan uji SpringBoot dengan JUnit 5 di sini, tetapi ini harus bekerja sama untuk semua tes menggunakan konteks Spring.

@SpringBootTest
class ProductionIntegrationTest {

  @Autowired
  private Production sut;

  @Autowired
  private TaskCompletionEventListener listener;

  @Test
  void thatTaskCompletesSuccessfully() {
    var taskId = UUID.randomUUID();
    sut.doSomeTask(taskId);
    listener.waitForCompletion(taskId);
    // do some assertions like looking into the DB if value was stored successfully
  }

}

Perhatikan, bahwa berbeda dengan jawaban lain di sini, solusi ini juga akan berfungsi jika Anda menjalankan tes Anda secara paralel dan beberapa utas menggunakan kode async secara bersamaan.

Stefan Haberl
sumber
0

Jika Anda ingin menguji logika, jangan mengujinya secara tidak sinkron.

Misalnya untuk menguji kode ini yang berfungsi pada hasil metode asinkron.

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       
    }
}

Dalam tes mengejek ketergantungan dengan implementasi sinkron. Tes unit sepenuhnya sinkron dan berjalan dalam 150 ms.

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")));
    }
}

Anda tidak menguji perilaku async tetapi Anda dapat menguji apakah logikanya benar.

Nils El-Himoud
sumber