วิธีใช้ JUnit เพื่อทดสอบกระบวนการแบบอะซิงโครนัส


204

คุณจะทดสอบวิธีที่ใช้กระบวนการแบบอะซิงโครนัสกับ JUnit ได้อย่างไร

ฉันไม่ทราบวิธีที่จะทำให้การทดสอบของฉันรอให้กระบวนการสิ้นสุด (ไม่ใช่การทดสอบหน่วยอย่างแน่นอนมันเป็นเหมือนการทดสอบการรวมเข้าด้วยกันเนื่องจากมันเกี่ยวข้องกับหลายคลาสและไม่ใช่แค่หนึ่งคลาส)


คุณสามารถลอง JAT (ทดสอบ Java Asynchronous Test): bitbucket.org/csolar/jat
cs0lar

2
JAT มีผู้เฝ้าดู 1 คนและยังไม่ได้อัปเดตใน 1.5 ปี Awaitility ได้รับการอัปเดตเมื่อ 1 เดือนก่อนและเป็นรุ่น 1.6 ในขณะที่เขียนนี้ ฉันไม่ได้มีส่วนเกี่ยวข้องกับโครงการใด แต่ถ้าฉันจะลงทุนเพิ่มเติมจากโครงการของฉันฉันจะให้ความเชื่อถือกับ Awaitility มากขึ้นในเวลานี้
Les Hazlewood

JAT ยังไม่มีการอัปเดต: "อัปเดตล่าสุด 2013-01-19" เพียงประหยัดเวลาในการติดตามลิงก์
deamon

@ LesHazlewood หนึ่งผู้เฝ้าดูไม่ดีสำหรับ JAT แต่ไม่มีการปรับปรุงสำหรับปี ... เพียงแค่ตัวอย่างเดียว คุณอัพเดต TCP สแต็กระดับต่ำของระบบปฏิบัติการของคุณบ่อยแค่ไหนถ้ามันใช้งานได้ ทางเลือกในการ JAT เป็นคำตอบด้านล่างstackoverflow.com/questions/631598/...
user1742529

คำตอบ:


46

IMHO เป็นการดีที่การทดสอบหน่วยสร้างหรือรอเธรด ฯลฯ คุณต้องการให้การทดสอบเหล่านี้ทำงานในไม่กี่วินาที นั่นเป็นเหตุผลที่ฉันต้องการเสนอวิธีการสองขั้นตอนในการทดสอบกระบวนการอะซิงก์

  1. ทดสอบว่ากระบวนการ async ของคุณถูกส่งอย่างถูกต้อง คุณสามารถจำลองวัตถุที่ยอมรับคำขอ async ของคุณและตรวจสอบให้แน่ใจว่างานที่ส่งมีคุณสมบัติที่ถูกต้อง ฯลฯ
  2. ทดสอบว่าการโทรกลับ async ของคุณกำลังทำสิ่งที่ถูกต้อง ที่นี่คุณสามารถเยาะเย้ยงานที่ส่งมา แต่เดิมและคิดว่ามันถูกเริ่มต้นอย่างถูกต้องและตรวจสอบว่าการเรียกกลับของคุณถูกต้อง

148
แน่ใจ แต่บางครั้งคุณจำเป็นต้องทดสอบโค้ดที่ควรจัดการเธรดโดยเฉพาะ
ขาด

77
สำหรับพวกเราที่ใช้ Junit หรือ TestNG เพื่อทำการทดสอบการรวม (และไม่ใช่แค่การทดสอบหน่วย) หรือการทดสอบการยอมรับของผู้ใช้ (เช่น w / Cucumber) การรอให้การทำ async เสร็จสิ้นและยืนยันผลนั้นเป็นสิ่งที่จำเป็นอย่างยิ่ง
Les Hazlewood

38
กระบวนการอะซิงโครนัสเป็นโค้ดที่ซับซ้อนที่สุดที่จะทำให้ถูกต้องและคุณบอกว่าคุณไม่ควรใช้การทดสอบหน่วยสำหรับพวกเขาและทดสอบด้วยเธรดเดียวเท่านั้น? นั่นเป็นความคิดที่แย่มาก
ชาร์ลส์

18
การทดสอบการเยาะเย้ยมักจะล้มเหลวในการพิสูจน์ว่าฟังก์ชั่นการทำงานจนจบ ฟังก์ชันการทำงานของ Async จำเป็นต้องได้รับการทดสอบในลักษณะอะซิงโครนัสเพื่อให้แน่ใจว่าสามารถใช้งานได้ เรียกว่าเป็นการทดสอบการรวมระบบหากคุณต้องการ แต่เป็นการทดสอบที่ยังต้องการ
Scott Boring

4
นี่ไม่ควรเป็นคำตอบที่ยอมรับได้ การทดสอบมีมากกว่าการทดสอบหน่วย OP เรียกมันว่าเป็นการทดสอบการรวมมากกว่าการทดสอบหน่วย
Jeremiah Adams

190

อีกทางเลือกหนึ่งคือใช้คลาส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());
    }
}

หมายเหตุคุณไม่สามารถใช้ซิงโครไนซ์กับวัตถุปกติเป็นล็อคได้เนื่องจากการโทรกลับอย่างรวดเร็วสามารถปลดล็อคก่อนที่จะเรียกวิธีการรอของล็อค ดูโพสต์บล็อกนี้โดยโจวอลเนส

EDITลบบล็อกที่ทำข้อมูลให้ตรงกันรอบ ๆ CountDownLatch ด้วยความเห็นจาก @jtahlborn และ @Ring


8
โปรดอย่าทำตามตัวอย่างนี้มันไม่ถูกต้อง คุณไม่ควรซิงโครไนซ์กับ CountDownLatch เนื่องจากจัดการกับความปลอดภัยของเธรดภายใน
jtahlborn

1
มันเป็นคำแนะนำที่ดีจนถึงส่วนที่ซิงโครไนซ์ซึ่งกินได้ใกล้เคียงกับเวลาดีบัก 3-4 ชั่วโมง stackoverflow.com/questions/11007551/…
แหวน

2
ขออภัยในความผิดพลาด ฉันได้แก้ไขคำตอบอย่างเหมาะสมแล้ว
Martin

7
หากคุณกำลังตรวจสอบว่ามีการเรียกใช้ onSuccess คุณควรยืนยันว่า lock.await ให้ผลตอบแทนจริง
Gilbert

1
@ มาร์ตินที่จะถูกต้อง แต่ก็หมายความว่าคุณมีปัญหาอื่นที่ต้องแก้ไข
George Aristy

75

คุณสามารถลองใช้ห้องสมุดAwaitility ทำให้ง่ายต่อการทดสอบระบบที่คุณกำลังพูดถึง


21
ข้อความปฏิเสธความรับผิดชอบที่เป็นมิตร: Johan เป็นผู้สนับสนุนหลักให้กับโครงการ
dbm

1
ทนทุกข์ทรมานจากปัญหาพื้นฐานของการต้องรอ (การทดสอบหน่วยต้องทำงานเร็ว ) ในทางกลับกันคุณคงไม่ต้องการรอนานกว่ามิลลิวินาทีเป็นเวลานานดังนั้นฉันคิดว่าการใช้CountDownLatch(ดูคำตอบจาก @Martin) นั้นดีกว่าในเรื่องนี้
George Aristy

ยอดเยี่ยมจริงๆ
R. Karlus

นี่คือห้องสมุดที่สมบูรณ์แบบที่ตอบสนองความต้องการการทดสอบการรวมกระบวนการของฉัน ยอดเยี่ยมจริงๆ ดูเหมือนว่าห้องสมุดจะได้รับการดูแลอย่างดีและมีฟีเจอร์ที่ขยายตั้งแต่ขั้นพื้นฐานไปจนถึงขั้นสูงที่ฉันเชื่อว่าเพียงพอที่จะรองรับสถานการณ์ส่วนใหญ่ ขอบคุณสำหรับการอ้างอิงที่ยอดเยี่ยม!
Tanvir

ข้อเสนอแนะที่ยอดเยี่ยมจริงๆ ขอบคุณ
RoyalTiger

68

หากคุณใช้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());

4
... และถ้าคุณติดอยู่กับ java-less-than-eight ลองใช้ guavas SettableFutureซึ่งทำสิ่งเดียวกันได้สวยมาก
Markus T


18

วิธีการหนึ่งที่ฉันพบว่าค่อนข้างมีประโยชน์สำหรับการทดสอบวิธีการแบบอะซิงโครนัสคือการฉีดExecutorอินสแตนซ์ในตัวสร้างของ object-to-test ในการผลิตอินสแตนซ์ตัวเรียกใช้งานถูกกำหนดค่าให้เรียกใช้แบบอะซิงโครนัสในขณะที่ทดสอบว่าสามารถจำลองให้ทำงานแบบซิงโครนัสได้

ดังนั้นคิดว่าฉันกำลังพยายามที่จะทดสอบวิธีการไม่ตรงกัน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());
}

ไม่ทำงานหากฉันต้องการรวมการทดสอบสิ่งที่เกี่ยวข้องกับการเรียกไลบรารีแบบอะซิงโครนัสอย่าง Spring'sWebClient
Stefan Haberl

8

ไม่มีอะไรผิดปกติกับการทดสอบโค้ดเธรด / async โดยเฉพาะอย่างยิ่งถ้าเธรดเป็นจุดของโค้ดที่คุณกำลังทดสอบ วิธีการทั่วไปในการทดสอบสิ่งนี้คือ:

  • บล็อกเธรดการทดสอบหลัก
  • การดักจับการยืนยันที่ล้มเหลวจากเธรดอื่น
  • เลิกบล็อกเธรดการทดสอบหลัก
  • 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 เป็นที่นี่

ฉันยังเขียนโพสต์บล็อกในหัวข้อสำหรับผู้ที่ต้องการเรียนรู้รายละเอียดเพิ่มเติมอีกเล็กน้อย


โซลูชันที่คล้ายกันที่ฉันเคยใช้ในอดีตคือgithub.com/MichaelTamm/junit-toolboxซึ่งเป็นส่วนเสริมของบุคคลที่สามในjunit.org/junit4
dschulten

4

วิธีการเกี่ยวกับการโทรSomeObject.waitและnotifyAllตามที่อธิบายไว้ที่นี่หรือการใช้วิธีRobotiums Solo.waitForCondition(...)หรือใช้คลาสที่ฉันเขียนเพื่อทำสิ่งนี้ (ดูความคิดเห็นและคลาสทดสอบสำหรับวิธีใช้)


1
ปัญหาเกี่ยวกับวิธีการรอ / แจ้งเตือน / ขัดจังหวะคือรหัสที่คุณทดสอบอาจรบกวนการรอเธรด (ฉันเคยเห็นมันเกิดขึ้น) นี่คือเหตุผลที่ConcurrentUnitใช้วงจรส่วนตัวที่เธรดสามารถรอได้ซึ่งไม่สามารถแทรกแซงโดยไม่ตั้งใจโดยอินเทอร์รัปต์ไปยังเธรดการทดสอบหลัก
Jonathan

3

ฉันค้นหา library socket.ioเพื่อทดสอบตรรกะอะซิงโครนัส มันดูเรียบง่ายและสั้นวิธีใช้LinkedBlockingQueue นี่คือตัวอย่าง :

    @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
วิธีการที่ยอดเยี่ยม!
อะ

3

เป็นมูลค่าการกล่าวขวัญว่ามีบทที่มีประโยชน์มากTesting Concurrent Programsในการใช้งานพร้อมกันในทางปฏิบัติซึ่งจะอธิบายวิธีการทดสอบหน่วยและให้คำแนะนำในการแก้ไขปัญหา


1
วิธีการใดที่ คุณยกตัวอย่างได้ไหม
Bruno Ferreira

2

นี่คือสิ่งที่ฉันใช้อยู่ทุกวันนี้หากผลการทดสอบถูกสร้างแบบอะซิงโครนัส

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เพื่อล้มเหลวก่อน


2

มีคำตอบมากมายที่นี่ แต่คำตอบง่ายๆคือเพียงสร้างเสร็จสมบูรณ์ในอนาคตและใช้มัน:

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!


2

หลีกเลี่ยงการทดสอบด้วยเธรดแบบขนานเมื่อใดก็ตามที่คุณทำได้ (ซึ่งเป็นส่วนใหญ่) วิธีนี้จะทำให้การทดสอบของคุณไม่สม่ำเสมอ (บางครั้งผ่านบางครั้งก็ล้มเหลว)

เฉพาะเมื่อคุณจำเป็นต้องเรียกบางคนอื่น ๆ ในห้องสมุด / ระบบคุณอาจจะต้องรอในหัวข้ออื่น ๆ ในกรณีที่มักใช้AwaitilityThread.sleep()ห้องสมุดแทน

ไม่เพียงแค่โทรget()หรือjoin()ในการทดสอบของคุณมิฉะนั้นการทดสอบของคุณอาจทำงานตลอดไปบนเซิร์ฟเวอร์ CI ของคุณในกรณีที่อนาคตไม่เสร็จสมบูรณ์ เสมอยืนยันเป็นครั้งแรกในการทดสอบของคุณก่อนที่จะเรียกisDone() get()สำหรับ CompletionStage .toCompletableFuture().isDone()ที่เป็น

เมื่อคุณทดสอบวิธีที่ไม่บล็อกเช่นนี้:

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

จากนั้นคุณไม่ควรเพียงแค่ทดสอบผลลัพธ์โดยผ่านอนาคตที่สมบูรณ์ในการทดสอบคุณควรตรวจสอบให้แน่ใจว่าวิธีการของคุณdoSomething()ไม่ได้บล็อกโดยการโทรjoin()หรือget()หรือสิ่งนี้มีความสำคัญเป็นพิเศษหากคุณใช้กรอบงานที่ไม่บล็อก

เมื่อต้องการทำเช่นนั้นให้ทดสอบกับอนาคตที่ไม่เสร็จซึ่งคุณตั้งค่าไว้ให้เสร็จสมบูรณ์ด้วยตนเอง:

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

ด้วยวิธีนี้หากคุณเพิ่มfuture.join()ใน doSomething () การทดสอบจะล้มเหลว

หากบริการของคุณใช้ ExecutorService เช่นในthenApplyAsync(..., executorService)จากนั้นในการทดสอบของคุณฉีด ExecutorService แบบเธรดเดียวเช่นหนึ่งจากฝรั่ง

ExecutorService executorService = Executors.newSingleThreadExecutor();

หากรหัสของคุณใช้ forkJoinPool เช่น thenApplyAsync(...)เขียนรหัสใหม่เพื่อใช้ ExecutorService (มีเหตุผลที่ดีมากมาย) หรือใช้ Awaitility

เพื่อทำให้ตัวอย่างสั้นลงฉันได้กำหนดให้ BarService เป็นอาร์กิวเมนต์เมธอดที่นำมาใช้เป็นแลมบ์ดา Java8 ในการทดสอบโดยทั่วไปจะเป็นการอ้างอิงแบบฉีดที่คุณจะจำลอง


สวัสดี @tkruse คุณอาจมี repo คอมไพล์สาธารณะด้วยการทดสอบโดยใช้เทคนิคนี้หรือไม่?
Cristiano

@Christiano: ที่จะขัดต่อปรัชญาดังนั้น แต่ฉันเปลี่ยนวิธีการคอมไพล์โดยไม่มีรหัสเพิ่มเติมใด ๆ (การนำเข้าทั้งหมดคือ java8 + หรือ junit) เมื่อคุณวางลงในคลาสทดสอบ junit ที่ว่างเปล่า รู้สึกอิสระที่จะ upvote
tkruse

ฉันเข้าใจแล้ว ขอบคุณ ปัญหาของฉันตอนนี้คือการทดสอบเมื่อเมธอดส่งคืน CompletableFuture แต่ยอมรับวัตถุอื่นเป็นพารามิเตอร์อื่นที่ไม่ใช่ CompletableFuture
Cristiano

ในกรณีของคุณใครเป็นคนสร้างกระบวนการที่สมบูรณ์แบบ หากเป็นบริการอื่น ๆ ที่สามารถเยาะเย้ยและเทคนิคของฉันยังคงใช้ หากวิธีการนั้นสร้างเสร็จสมบูรณ์ในอนาคตสถานการณ์จะเปลี่ยนไปมากดังนั้นคุณสามารถถามคำถามใหม่เกี่ยวกับมันได้ จากนั้นขึ้นอยู่กับว่าเธรดใดจะเสร็จสิ้นในอนาคตซึ่งวิธีการของคุณจะส่งคืน
tkruse

1

ฉันชอบใช้การรอและแจ้งเตือน มันง่ายและชัดเจน

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

โดยพื้นฐานแล้วเราจำเป็นต้องสร้างการอ้างอิง Array ขั้นสุดท้ายเพื่อใช้ภายในคลาสภายในแบบไม่ระบุชื่อ ฉันอยากจะสร้างบูลีน [] เพราะฉันสามารถใส่ค่าในการควบคุมถ้าเราต้องรอ () เมื่อทุกอย่างเสร็จสิ้นเราเพิ่งเปิดตัว asyncExecuted


1
หากการยืนยันของคุณล้มเหลวเธรดการทดสอบหลักจะไม่ทราบ
Jonathan

ขอบคุณสำหรับการแก้ปัญหาช่วยให้ฉันแก้จุดบกพร่องรหัสด้วยการเชื่อมต่อ websocket
Nazar Sakharenko

@ โจนาธานฉันอัปเดตรหัสเพื่อตรวจสอบการยืนยันและข้อยกเว้นและแจ้งให้หัวข้อการทดสอบหลัก
เปาโล

1

สำหรับผู้ใช้สปริงทุกคนนี่คือวิธีที่ฉันมักจะทำการทดสอบการรวมระบบของฉันทุกวันนี้ที่มีการใช้งาน async:

เริ่มเหตุการณ์แอปพลิเคชันในรหัสการผลิตเมื่องาน async (เช่นการโทร I / O) เสร็จสิ้น ส่วนใหญ่เหตุการณ์นี้มีความจำเป็นต่อการตอบสนองของการดำเนินการ async ในการผลิต

เมื่อมีเหตุการณ์นี้เกิดขึ้นคุณสามารถใช้กลยุทธ์ต่อไปนี้ในกรณีทดสอบของคุณ:

  1. ดำเนินการระบบภายใต้การทดสอบ
  2. ฟังเหตุการณ์และตรวจสอบให้แน่ใจว่าเหตุการณ์นั้นเริ่มต้นขึ้น
  3. ทำการยืนยันของคุณ

ในการแยกแยะสิ่งนี้ก่อนอื่นคุณต้องมีกิจกรรมโดเมนก่อน ฉันใช้ UUID ที่นี่เพื่อระบุงานที่เสร็จ แต่คุณสามารถใช้งานได้อย่างอิสระตราบใดที่มันไม่เหมือนใคร

(โปรดทราบว่าข้อมูลโค้ดต่อไปนี้ใช้หมายเหตุประกอบลอมบอกเพื่อกำจัดรหัสแผ่นบอยเลอร์)

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

โดยทั่วไปแล้วรหัสการผลิตจะมีลักษณะดังนี้:

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

}

จากนั้นฉันสามารถใช้ Spring @EventListenerเพื่อตรวจจับเหตุการณ์ที่เผยแพร่ในรหัสทดสอบ ผู้ฟังเหตุการณ์มีส่วนเกี่ยวข้องเพิ่มขึ้นเล็กน้อยเนื่องจากมีการจัดการสองกรณีในลักษณะที่ปลอดภัยของเธรด:

  1. รหัสการผลิตเร็วกว่ากรณีทดสอบและเหตุการณ์ได้เริ่มต้นแล้วก่อนที่กรณีทดสอบจะตรวจสอบเหตุการณ์หรือ
  2. กรณีทดสอบเร็วกว่ารหัสการผลิตและกรณีทดสอบต้องรอเหตุการณ์

CountDownLatchใช้สำหรับกรณีที่สองตามที่กล่าวไว้ในคำตอบอื่น ๆ ที่นี่ นอกจากนี้โปรดทราบว่า@Orderคำอธิบายประกอบบนเมธอดตัวจัดการเหตุการณ์ทำให้แน่ใจว่าเมธอดตัวจัดการเหตุการณ์นี้ถูกเรียกหลังจากผู้ฟังเหตุการณ์อื่น ๆ ที่ใช้ในการผลิต

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

}

ขั้นตอนสุดท้ายคือการดำเนินการระบบภายใต้การทดสอบในกรณีทดสอบ ฉันใช้การทดสอบ SpringBoot กับ JUnit 5 ที่นี่ แต่สิ่งนี้ควรจะเหมือนกันสำหรับการทดสอบทั้งหมดโดยใช้บริบท 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
  }

}

โปรดทราบว่าในทางตรงกันข้ามกับคำตอบอื่น ๆ ที่นี่การแก้ปัญหานี้จะใช้งานได้หากคุณดำเนินการทดสอบในแบบคู่ขนานและหลายเธรดใช้รหัส async พร้อมกัน


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

คุณไม่ทดสอบพฤติกรรมการซิงค์ แต่คุณสามารถทดสอบว่าตรรกะนั้นถูกต้องหรือไม่

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.