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:
- Jalankan sistem yang sedang diuji
- Dengarkan acara dan pastikan bahwa acara telah dipecat
- 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 @EventListener
untuk menangkap acara yang diterbitkan dalam kode uji. Pendengar acara sedikit lebih terlibat, karena harus menangani dua kasus dengan cara yang aman:
- Kode produksi lebih cepat daripada test case dan event sudah dipecat sebelum test case memeriksa event, atau
- Kasing uji lebih cepat dari kode produksi dan kasing harus menunggu acara.
A CountDownLatch
digunakan untuk kasus kedua seperti yang disebutkan dalam jawaban lain di sini. Juga perhatikan, bahwa @Order
anotasi 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.