คุณจะทดสอบวิธีที่ใช้กระบวนการแบบอะซิงโครนัสกับ JUnit ได้อย่างไร
ฉันไม่ทราบวิธีที่จะทำให้การทดสอบของฉันรอให้กระบวนการสิ้นสุด (ไม่ใช่การทดสอบหน่วยอย่างแน่นอนมันเป็นเหมือนการทดสอบการรวมเข้าด้วยกันเนื่องจากมันเกี่ยวข้องกับหลายคลาสและไม่ใช่แค่หนึ่งคลาส)
คุณจะทดสอบวิธีที่ใช้กระบวนการแบบอะซิงโครนัสกับ JUnit ได้อย่างไร
ฉันไม่ทราบวิธีที่จะทำให้การทดสอบของฉันรอให้กระบวนการสิ้นสุด (ไม่ใช่การทดสอบหน่วยอย่างแน่นอนมันเป็นเหมือนการทดสอบการรวมเข้าด้วยกันเนื่องจากมันเกี่ยวข้องกับหลายคลาสและไม่ใช่แค่หนึ่งคลาส)
คำตอบ:
IMHO เป็นการดีที่การทดสอบหน่วยสร้างหรือรอเธรด ฯลฯ คุณต้องการให้การทดสอบเหล่านี้ทำงานในไม่กี่วินาที นั่นเป็นเหตุผลที่ฉันต้องการเสนอวิธีการสองขั้นตอนในการทดสอบกระบวนการอะซิงก์
อีกทางเลือกหนึ่งคือใช้คลาส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
คุณสามารถลองใช้ห้องสมุดAwaitility ทำให้ง่ายต่อการทดสอบระบบที่คุณกำลังพูดถึง
CountDownLatch
(ดูคำตอบจาก @Martin) นั้นดีกว่าในเรื่องนี้
หากคุณใช้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());
Future
เริ่มขั้นตอนการออกและรอผลการใช้
วิธีการหนึ่งที่ฉันพบว่าค่อนข้างมีประโยชน์สำหรับการทดสอบวิธีการแบบอะซิงโครนัสคือการฉีด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());
}
WebClient
ไม่มีอะไรผิดปกติกับการทดสอบโค้ดเธรด / async โดยเฉพาะอย่างยิ่งถ้าเธรดเป็นจุดของโค้ดที่คุณกำลังทดสอบ วิธีการทั่วไปในการทดสอบสิ่งนี้คือ:
แต่นั่นเป็นจำนวนมากสำหรับการทดสอบหนึ่งครั้ง วิธีที่ดีกว่า / ง่ายกว่าคือการใช้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 เป็นที่นี่
ฉันยังเขียนโพสต์บล็อกในหัวข้อสำหรับผู้ที่ต้องการเรียนรู้รายละเอียดเพิ่มเติมอีกเล็กน้อย
วิธีการเกี่ยวกับการโทรSomeObject.wait
และnotifyAll
ตามที่อธิบายไว้ที่นี่หรือการใช้วิธีRobotiums Solo.waitForCondition(...)
หรือใช้คลาสที่ฉันเขียนเพื่อทำสิ่งนี้ (ดูความคิดเห็นและคลาสทดสอบสำหรับวิธีใช้)
ฉันค้นหา 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 เพื่อบล็อกจนกว่าจะได้ผลลัพธ์เช่นเดียวกับวิธีการซิงโครนัส และตั้งค่าการหมดเวลาเพื่อหลีกเลี่ยงการสมมติเวลามากเกินไปที่จะรอผล
เป็นมูลค่าการกล่าวขวัญว่ามีบทที่มีประโยชน์มากTesting Concurrent Programs
ในการใช้งานพร้อมกันในทางปฏิบัติซึ่งจะอธิบายวิธีการทดสอบหน่วยและให้คำแนะนำในการแก้ไขปัญหา
นี่คือสิ่งที่ฉันใช้อยู่ทุกวันนี้หากผลการทดสอบถูกสร้างแบบอะซิงโครนัส
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
เพื่อล้มเหลวก่อน
มีคำตอบมากมายที่นี่ แต่คำตอบง่ายๆคือเพียงสร้างเสร็จสมบูรณ์ในอนาคตและใช้มัน:
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!
หลีกเลี่ยงการทดสอบด้วยเธรดแบบขนานเมื่อใดก็ตามที่คุณทำได้ (ซึ่งเป็นส่วนใหญ่) วิธีนี้จะทำให้การทดสอบของคุณไม่สม่ำเสมอ (บางครั้งผ่านบางครั้งก็ล้มเหลว)
เฉพาะเมื่อคุณจำเป็นต้องเรียกบางคนอื่น ๆ ในห้องสมุด / ระบบคุณอาจจะต้องรอในหัวข้ออื่น ๆ ในกรณีที่มักใช้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 ในการทดสอบโดยทั่วไปจะเป็นการอ้างอิงแบบฉีดที่คุณจะจำลอง
ฉันชอบใช้การรอและแจ้งเตือน มันง่ายและชัดเจน
@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
สำหรับผู้ใช้สปริงทุกคนนี่คือวิธีที่ฉันมักจะทำการทดสอบการรวมระบบของฉันทุกวันนี้ที่มีการใช้งาน async:
เริ่มเหตุการณ์แอปพลิเคชันในรหัสการผลิตเมื่องาน async (เช่นการโทร I / O) เสร็จสิ้น ส่วนใหญ่เหตุการณ์นี้มีความจำเป็นต่อการตอบสนองของการดำเนินการ async ในการผลิต
เมื่อมีเหตุการณ์นี้เกิดขึ้นคุณสามารถใช้กลยุทธ์ต่อไปนี้ในกรณีทดสอบของคุณ:
ในการแยกแยะสิ่งนี้ก่อนอื่นคุณต้องมีกิจกรรมโดเมนก่อน ฉันใช้ 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
เพื่อตรวจจับเหตุการณ์ที่เผยแพร่ในรหัสทดสอบ ผู้ฟังเหตุการณ์มีส่วนเกี่ยวข้องเพิ่มขึ้นเล็กน้อยเนื่องจากมีการจัดการสองกรณีในลักษณะที่ปลอดภัยของเธรด:
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 พร้อมกัน
หากคุณต้องการทดสอบตรรกะเพียงอย่าทดสอบแบบอะซิงโครนัส
ตัวอย่างเช่นการทดสอบรหัสนี้ซึ่งทำงานกับผลลัพธ์ของวิธีการแบบอะซิงโครนัส
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")));
}
}
คุณไม่ทดสอบพฤติกรรมการซิงค์ แต่คุณสามารถทดสอบว่าตรรกะนั้นถูกต้องหรือไม่