Đố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:
- Thực thi hệ thống đang thử nghiệm
- Lắng nghe sự kiện và chắc chắn rằng sự kiện đã nổ ra
- 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:
- 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
- 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 @Order
chú 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.