Cách sử dụng JUnit để kiểm tra các quy trình không đồng bộ


204

Làm thế nào để bạn kiểm tra các phương thức kích hoạt các quy trình không đồng bộ với JUnit?

Tôi không biết làm thế nào để thử nghiệm của mình chờ quá trình kết thúc (nó không chính xác là một thử nghiệm đơn vị, nó giống như một thử nghiệm tích hợp vì nó liên quan đến một số lớp chứ không chỉ một).


Bạn có thể thử JAT (Kiểm tra không đồng bộ Java): bitbucket.org/csolar/jat
cs0lar

2
JAT có 1 người theo dõi và đã không được cập nhật trong 1,5 năm. Awaitility đã được cập nhật chỉ 1 tháng trước và trên phiên bản 1.6 tại thời điểm viết bài này. Tôi không liên kết với một trong hai dự án, nhưng nếu tôi định đầu tư bổ sung vào dự án của mình, tôi sẽ tin tưởng hơn vào Awaitility tại thời điểm này.
Les Hazlewood

JAT vẫn chưa có bản cập nhật: "Cập nhật lần cuối 2013-01-19". Chỉ cần tiết kiệm thời gian để theo liên kết.
deamon

@LesHazlewood, một người theo dõi rất tệ cho JAT, nhưng về việc không có bản cập nhật nào trong nhiều năm ... Chỉ là một ví dụ. Tần suất bạn cập nhật ngăn xếp TCP cấp thấp của HĐH, nếu nó chỉ hoạt động? Thay thế cho JAT được trả lời bên dưới stackoverflow.com/questions/631598/ ,.
dùng1742529

Câu trả lời:


46

IMHO thực tế không tốt khi có các bài kiểm tra đơn vị tạo hoặc chờ đợi trên các luồng, v.v. Bạn muốn các bài kiểm tra này chạy trong vài giây. Đó là lý do tại sao tôi muốn đề xuất cách tiếp cận 2 bước để kiểm tra các quy trình không đồng bộ.

  1. Kiểm tra rằng quá trình không đồng bộ của bạn được gửi đúng. Bạn có thể giả định đối tượng chấp nhận các yêu cầu không đồng bộ của bạn và đảm bảo rằng công việc đã gửi có thuộc tính chính xác, v.v.
  2. Kiểm tra rằng các cuộc gọi lại async của bạn đang làm những điều đúng đắn. Tại đây, bạn có thể giả định công việc được gửi ban đầu và cho rằng nó được khởi tạo đúng cách và xác minh rằng các cuộc gọi lại của bạn là chính xác.

148
Chắc chắn rồi. Nhưng đôi khi bạn cần kiểm tra mã được cho là đặc biệt để quản lý các luồng.
thiếu

77
Đối với những người trong chúng tôi sử dụng Junit hoặc TestNG để thực hiện kiểm tra tích hợp (và không chỉ kiểm tra đơn vị) hoặc kiểm tra chấp nhận của người dùng (ví dụ: w / Cucumber), chờ đợi hoàn thành không đồng bộ và xác minh kết quả là hoàn toàn cần thiết.
Les Hazlewood

38
Các quy trình không đồng bộ là một số mã phức tạp nhất để có được quyền và bạn nói rằng bạn không nên sử dụng thử nghiệm đơn vị cho chúng và chỉ kiểm tra với một luồng duy nhất? Đó là một ý tưởng rất tồi.
Charles

18
Các thử nghiệm giả thường không chứng minh được rằng chức năng hoạt động từ đầu đến cuối. Chức năng Async cần được kiểm tra theo cách không đồng bộ để đảm bảo nó hoạt động. Gọi nó là một bài kiểm tra tích hợp nếu bạn thích, nhưng đó là một bài kiểm tra vẫn cần thiết.
Scott Bored

4
Đây không phải là câu trả lời được chấp nhận. Kiểm tra vượt ra ngoài kiểm tra đơn vị. OP gọi nó là một bài kiểm tra tích hợp nhiều hơn bài kiểm tra đơn vị.
Jeremiah Adams

190

Một cách khác là sử dụng lớp 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());
    }
}

LƯU Ý, bạn không thể chỉ sử dụng đồng bộ hóa với một đối tượng thông thường như một khóa, vì các cuộc gọi lại nhanh có thể giải phóng khóa trước khi phương thức chờ của khóa được gọi. Xem bài đăng blog này của Joe Walnes.

EDIT Đã xóa các khối được đồng bộ hóa xung quanh CountDownLatch nhờ nhận xét từ @jtahlborn và @Ring


8
xin vui lòng không làm theo ví dụ này, nó không chính xác. bạn không nên đồng bộ hóa trên CountDownLatch vì nó xử lý an toàn luồng trong nội bộ.
jtahlborn

1
Đó là lời khuyên tốt cho đến khi phần được đồng bộ hóa, có thể đã ăn gần 3-4 giờ thời gian gỡ lỗi. stackoverflow.com/questions/11007551/ trộm
Đổ chuông

2
Xin lỗi về lỗi này. Tôi đã chỉnh sửa câu trả lời một cách thích hợp.
Martin

7
Nếu bạn đang xác minh rằng onSuccess đã được gọi, bạn nên xác nhận rằng lock.await trả về đúng.
Gilbert

1
@Martin điều đó sẽ đúng, nhưng điều đó có nghĩa là bạn có một vấn đề khác cần được khắc phục.
George Aristy

75

Bạn có thể thử sử dụng thư viện Awaitility . Nó giúp bạn dễ dàng kiểm tra các hệ thống mà bạn đang nói đến.


20
Từ chối trách nhiệm thân thiện: Johan là người đóng góp chính cho dự án.
dbm

1
Bị vấn đề cơ bản là phải chờ đợi (bài kiểm tra đơn vị cần phải chạy nhanh ). Lý tưởng nhất là bạn thực sự không muốn đợi một phần nghìn giây lâu hơn mức cần thiết, vì vậy tôi nghĩ rằng việc sử dụng CountDownLatch(xem câu trả lời của @Martin) là tốt hơn trong vấn đề này.
George Aristy

Thực sự tuyệt vời.
R. Karlus

Đây là thư viện hoàn hảo đáp ứng các yêu cầu kiểm tra tích hợp quy trình không đồng bộ của tôi. Thực sự tuyệt vời. Thư viện dường như được duy trì tốt và có các tính năng mở rộng từ cơ bản đến nâng cao mà tôi tin là đủ để phục vụ cho hầu hết các kịch bản. Cảm ơn đã tham khảo tuyệt vời!
Tanvir

Đề nghị thực sự tuyệt vời. Cảm ơn
RoyalTiger

68

Nếu bạn sử dụng CompleteableFuture (được giới thiệu trong Java 8) hoặc SfortFuture (từ Google Guava ), bạn có thể hoàn thành bài kiểm tra của mình ngay khi hoàn thành, thay vì chờ một khoảng thời gian được đặt trước. Bài kiểm tra của bạn sẽ trông giống như thế này:

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

4
... và nếu bạn bị mắc kẹt với java ít hơn tám lần, hãy thử guavas SfortFuture , thứ tương tự như vậy
Markus T


18

Một phương pháp tôi thấy khá hữu ích để kiểm tra các phương thức không đồng bộ là đưa một Executorthể hiện vào hàm tạo của đối tượng để kiểm tra. Trong sản xuất, phiên bản thực thi được cấu hình để chạy không đồng bộ trong khi kiểm tra, nó có thể được mô phỏng để chạy đồng bộ.

Vì vậy, giả sử tôi đang thử nghiệm phương pháp không đồng bộ 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);
      }
    });
  }
}

Trong sản xuất, tôi sẽ xây dựng Foovới một Executors.newSingleThreadExecutor()cá thể Executor trong khi thử nghiệm, tôi có thể sẽ xây dựng nó với một bộ thực thi đồng bộ thực hiện như sau -

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

Bây giờ bài kiểm tra JUnit của tôi về phương pháp không đồng bộ khá sạch sẽ -

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

Không hoạt động nếu tôi muốn kiểm tra tích hợp một cái gì đó liên quan đến một cuộc gọi thư viện không đồng bộ như Spring'sWebClient
Stefan Haberl

8

Không có gì sai khi thử nghiệm mã luồng / async, đặc biệt nếu luồng là điểm của mã bạn đang kiểm tra. Cách tiếp cận chung để kiểm tra công cụ này là:

  • Chặn luồng thử nghiệm chính
  • Nắm bắt các xác nhận thất bại từ các chủ đề khác
  • Bỏ chặn luồng kiểm tra chính
  • Lấy lại bất kỳ thất bại

Nhưng đó là rất nhiều nồi hơi cho một thử nghiệm. Một cách tiếp cận tốt hơn / đơn giản hơn là chỉ sử dụng ConcurrencyUnit :

  final Waiter waiter = new Waiter();

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

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

Lợi ích của CountdownLatchcách tiếp cận này là nó ít dài dòng hơn vì các lỗi xác nhận xảy ra trong bất kỳ luồng nào đều được báo cáo đúng cho luồng chính, nghĩa là thử nghiệm thất bại khi cần. Một bài viết so sánh CountdownLatchcách tiếp cận với ConcurrencyUnit có ở đây .

Tôi cũng đã viết một bài đăng trên blog về chủ đề cho những người muốn tìm hiểu chi tiết hơn một chút.


một giải pháp tương tự tôi đã sử dụng trong quá khứ là github.com/MichaelTamm/junit-toolbox , cũng được giới thiệu như một phần mở rộng của bên thứ ba trên Junit.org/junit4
dschulten

4

Cách gọi SomeObject.waitnotifyAllnhư được mô tả ở đây HOẶC sử dụng phương pháp Robotium Solo.waitForCondition(...) HOẶC sử dụng lớp tôi đã viết để thực hiện việc này (xem nhận xét và lớp kiểm tra để biết cách sử dụng)


1
Vấn đề với cách tiếp cận chờ / thông báo / ngắt là mã mà bạn đang kiểm tra có khả năng can thiệp vào các chuỗi chờ (tôi đã thấy điều đó xảy ra). Đây là lý do tại sao ConcurrencyUnit sử dụng một mạch riêng mà các luồng có thể chờ, điều này không thể vô tình bị can thiệp bởi các ngắt đến luồng thử nghiệm chính.
Jonathan

3

Tôi tìm thấy một thư viện socket.io để kiểm tra logic không đồng bộ. Nó trông đơn giản và ngắn gọn bằng cách sử dụng LinkedBlockingQueue . Đây là ví dụ :

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

Sử dụng LinkedBlockingQueue, hãy dùng API để chặn cho đến khi nhận được kết quả giống như cách đồng bộ. Và đặt thời gian chờ để tránh giả sử quá nhiều thời gian để chờ kết quả.


1
Cách tiếp cận tuyệt vời!
aminography


2

Đây là những gì tôi đang sử dụng hiện nay nếu kết quả kiểm tra được sản xuất không đồng bộ.

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

Sử dụng nhập tĩnh, kiểm tra đọc kinda tốt đẹp. (lưu ý, trong ví dụ này tôi đang bắt đầu một chủ đề để minh họa ý tưởng)

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

Nếu f.completekhông được gọi, bài kiểm tra sẽ thất bại sau khi hết thời gian. Bạn cũng có thể sử dụng f.completeExceptionallyđể thất bại sớm.


2

Có rất nhiều câu trả lời ở đây nhưng một câu hỏi đơn giản là chỉ cần tạo một CompleteableFuture đã hoàn thành và sử dụng nó:

CompletableFuture.completedFuture("donzo")

Vì vậy, trong thử nghiệm của tôi:

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

Tôi chỉ đảm bảo rằng tất cả những thứ này sẽ được gọi bằng mọi cách. Kỹ thuật này hoạt động nếu bạn đang sử dụng mã này:

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

Nó sẽ nén ngay qua nó khi tất cả các CompleteableFutures đã hoàn thành!


2

Tránh thử nghiệm với các luồng song song bất cứ khi nào bạn có thể (hầu hết thời gian). Điều này sẽ chỉ làm cho các bài kiểm tra của bạn không ổn định (đôi khi vượt qua, đôi khi thất bại).

Chỉ khi bạn cần gọi một số thư viện / hệ thống khác, bạn có thể phải chờ các luồng khác, trong trường hợp đó luôn luôn sử dụng thư viện Awaitility thay vì Thread.sleep().

Không bao giờ chỉ gọi get()hoặc join()trong các thử nghiệm của bạn, các thử nghiệm khác của bạn có thể chạy mãi mãi trên máy chủ CI của bạn trong trường hợp tương lai không bao giờ hoàn thành. Luôn luôn khẳng định isDone()đầu tiên trong các bài kiểm tra của bạn trước khi gọi get(). Đối với CompleteionStage, đó là .toCompletableFuture().isDone().

Khi bạn kiểm tra một phương pháp không chặn như thế này:

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

sau đó, bạn không nên chỉ kiểm tra kết quả bằng cách vượt qua một Tương lai đã hoàn thành trong bài kiểm tra, bạn cũng nên đảm bảo rằng phương thức của bạn doSomething()không chặn bằng cách gọi join()hoặc get(). Điều này đặc biệt quan trọng nếu bạn sử dụng khung không chặn.

Để làm điều đó, hãy kiểm tra với một tương lai chưa hoàn thành mà bạn đặt thành hoàn thành thủ công:

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

Bằng cách đó, nếu bạn thêm future.join()vào doS Something (), bài kiểm tra sẽ thất bại.

Nếu Dịch vụ của bạn sử dụng ExecutorService như trong thenApplyAsync(..., executorService), thì trong các thử nghiệm của bạn sẽ tiêm một ExecutorService đơn luồng, chẳng hạn như dịch vụ từ ổi:

ExecutorService executorService = Executors.newSingleThreadExecutor();

Nếu mã của bạn sử dụng forkJoinPool, chẳng hạn như thenApplyAsync(...), hãy viết lại mã để sử dụng ExecutorService (có nhiều lý do chính đáng) hoặc sử dụng Awaitility.

Để rút ngắn ví dụ, tôi đã tạo cho BarService một đối số phương thức được triển khai dưới dạng lambda Java8 trong thử nghiệm, thông thường nó sẽ là một tham chiếu được tiêm mà bạn sẽ chế giễu.


Xin chào @tkruse, có lẽ bạn có một repo git công khai với một bài kiểm tra sử dụng kỹ thuật này?
Cristiano

@Christiano: điều đó sẽ chống lại triết lý SO. Thay vào đó, tôi đã thay đổi các phương thức để biên dịch mà không cần bất kỳ mã bổ sung nào (tất cả các lần nhập là java8 + hoặc Junit) khi bạn dán chúng vào một lớp kiểm tra Junit trống. Hãy upvote.
tkruse

Tôi hiểu ngay bây giờ. cảm ơn. Vấn đề của tôi bây giờ là kiểm tra khi các phương thức trả về CompleteableFuture nhưng chấp nhận các đối tượng khác làm tham số khác với CompleteableFuture.
Cristiano

Trong trường hợp của bạn, ai tạo ra CompleteableFuture mà phương thức trả về? Nếu đó là một dịch vụ khác, điều đó có thể bị chế giễu và kỹ thuật của tôi vẫn được áp dụng. Nếu phương thức tự tạo một CompleteableFuture, tình huống sẽ thay đổi rất nhiều để bạn có thể hỏi một câu hỏi mới về nó. Sau đó, nó phụ thuộc vào luồng nào sẽ hoàn thành trong tương lai mà phương thức của bạn trả về.
tkruse

1

Tôi thích sử dụng chờ đợi và thông báo. Nó đơn giản và rõ ràng.

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

Về cơ bản, chúng ta cần tạo một tham chiếu Array cuối cùng, để được sử dụng bên trong lớp bên trong ẩn danh. Tôi thà tạo một boolean [], bởi vì tôi có thể đặt một giá trị để kiểm soát nếu chúng ta cần đợi (). Khi mọi thứ đã xong, chúng tôi chỉ phát hành asyncExecuted.


1
Nếu xác nhận của bạn không thành công, luồng kiểm tra chính sẽ không biết về nó.
Jonathan

Cảm ơn giải pháp, giúp tôi gỡ lỗi mã với kết nối websocket.
Nazar Sakharenko

@Jonathan, tôi đã cập nhật mã để nắm bắt bất kỳ khẳng định và ngoại lệ nào và thông báo nó cho luồng thử nghiệm chính.
Paulo

1

Đối với tất cả người dùng Spring ngoài kia, đây là cách tôi thường thực hiện các bài kiểm tra tích hợp của mình hiện nay, nơi có hành vi không đồng bộ:

Bắt đầu một sự kiện ứng dụng trong mã sản xuất, khi một tác vụ không đồng bộ (như cuộc gọi I / O) đã kết thúc. Hầu hết thời gian sự kiện này là cần thiết để xử lý phản hồi của hoạt động không đồng bộ trong sản xuất.

Với sự kiện này, bạn có thể sử dụng chiến lược sau trong trường hợp thử nghiệm của mình:

  1. Thực thi hệ thống đang thử nghiệm
  2. Lắng nghe sự kiện và chắc chắn rằng sự kiện đã nổ ra
  3. Xác nhận của bạn

Để phá vỡ điều này, trước tiên bạn sẽ cần một số loại sự kiện tên miền để kích hoạt. Tôi đang sử dụng UUID ở đây để xác định nhiệm vụ đã hoàn thành, nhưng tất nhiên bạn có thể tự do sử dụng một thứ khác miễn là nó là duy nhất.

(Lưu ý rằng các đoạn mã sau đây cũng sử dụng các chú thích Lombok để loại bỏ mã tấm nồi hơi)

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

Mã sản xuất sau đó thường trông như thế này:

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

}

Sau đó tôi có thể sử dụng Spring @EventListenerđể bắt sự kiện được công bố trong mã kiểm tra. Trình lắng nghe sự kiện có liên quan nhiều hơn một chút, bởi vì nó phải xử lý hai trường hợp theo cách an toàn của luồng:

  1. Mã sản xuất nhanh hơn trường hợp thử nghiệm và sự kiện đã được kích hoạt trước khi trường hợp thử nghiệm kiểm tra sự kiện, hoặc
  2. Trường hợp thử nghiệm nhanh hơn mã sản xuất và trường hợp thử nghiệm phải chờ sự kiện.

A CountDownLatchđược sử dụng cho trường hợp thứ hai như được đề cập trong các câu trả lời khác ở đây. Cũng lưu ý rằng @Orderchú thích về phương thức xử lý sự kiện đảm bảo rằng phương thức xử lý sự kiện này được gọi sau bất kỳ trình lắng nghe sự kiện nào khác được sử dụng trong sản xuất.

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

}

Bước cuối cùng là thực thi hệ thống đang thử nghiệm trong trường hợp thử nghiệm. Tôi đang sử dụng thử nghiệm SpringBoot với JUnit 5 tại đây, nhưng điều này sẽ hoạt động tương tự cho tất cả các thử nghiệm sử dụng bối cảnh 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
  }

}

Lưu ý, trái ngược với các câu trả lời khác ở đây, giải pháp này cũng sẽ hoạt động nếu bạn thực hiện các bài kiểm tra của mình song song và nhiều luồng thực hiện mã async cùng một lúc.


0

Nếu bạn muốn kiểm tra logic, đừng don kiểm tra nó không đồng bộ.

Ví dụ để kiểm tra mã này hoạt động dựa trên kết quả của phương pháp không đồng bộ.

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

Trong thử nghiệm giả định sự phụ thuộc với thực hiện đồng bộ. Bài kiểm tra đơn vị hoàn toàn đồng bộ và chạy trong 150ms.

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

Bạn không kiểm tra hành vi không đồng bộ nhưng bạn có thể kiểm tra xem logic có đúng không.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.