Stop Writing Flaky Tests: Xây dựng chiến lược Unit & E2E Test 'chuẩn Big Tech' cho Node.js

Chào các bạn, nếu bạn đang làm Backend với Node.js, chắc hẳn bạn đã trải qua cảm giác: "Code chạy ngon trên máy mình, nhưng đẩy lên CI/CD thì lúc pass, lúc fail".
Hiện tượng này được gọi là Flaky Tests (Test chập chờn). Nó thường xảy ra khi chúng ta viết E2E test nhưng lại để "lọt" state của Database từ bài test trước sang bài test sau, hoặc do kết nối hạ tầng (Redis, Kafka) chưa ổn định khi test bắt đầu.
Hôm nay, mình sẽ chia sẻ lại toàn bộ kinh nghiệm và kiến trúc Testing mà mình đã đúc kết được sau khi hoàn thiện framework tự động hóa nodejs-quickstart-structure. Chúng ta sẽ giải quyết bài toán cốt lõi: Làm sao để Unit Test siêu tốc và E2E Test cực kỳ chuẩn xác (Deterministic)?
1. Vấn nạn của 90% các dự án hiện nay
Rất nhiều team viết test theo kiểu "nửa vời":
- Unit Test: Không mock Database/Redis, dẫn đến Unit Test chạy chậm như rùa vì phải chờ I/O mạng.
- E2E Test: Dùng chung DB với môi trường Dev. File test A tạo ra một User, file test B lại check tổng số User, thế là khi đổi thứ tự chạy, test sẽ Tịt! Thậm chí có nơi còn dùng
if-elsetrong test:nếu statusCode == 404 thì expect(404) else expect(201).
Đây rành rành là một Anti-pattern! Test phải luôn cho ra một kết quả duy nhất dựa trên input cố định.
2. Chiến lược "Big Tech": Tách bạch biên giới rõ ràng
Để giải quyết, chúng ta cần chia ranh giới rõ ràng:
Unit Tests (Nhanh, Cô lập)
- Mục tiêu: Kiểm tra Business Logic (Use cases, Services, Utils).
- Quy tắc: MOCK TOÀN BỘ. Không kết nối DB thực, không gọi API ngoài, không chạm vào Redis hay Kafka.
- Tốc độ: Chạy hàng nghìn test cases trong chưa tới 1-2 giây.
E2E Tests (Hộp đen, Tự động hóa Hạ tầng)
- Mục tiêu: Kiểm tra toàn bộ luồng request thực tế (Route -> Controller -> Usecase -> Repo -> DB -> Response).
- Quy tắc: Sử dụng Database, Redis, Kafka thật (thông qua Docker Container riêng biệt hoặc Testcontainers).
- Đặc trưng: Dữ liệu phải được DỌN SẠCH (Teardown/Truncate) trước mỗi bài test để đảm bảo môi trường "sạch". Tốc độ chậm hơn nhưng độ tin cậy là tuyệt đối.
3. Bí kíp Setup E2E Test hoàn hảo
Để E2E test không bị đụng chạm với môi trường Dev hiện tại của đồng nghiệp, hãy áp dụng quy trình sau và xem thử source này nodejs-service-redis-kafka:
Bước 1: Tách biệt hoàn toàn jest.config.js
Đừng dùng chung config. Hãy tạo một file jest.e2e.config.js riêng với testTimeout cao hơn (như 30 giây để chờ DB boot).
/* eslint-disable @typescript-eslint/no-require-imports */
module.exports = {
...require('./jest.config'),
testMatch: ['<rootDir>/tests/e2e/**/*.test.ts'],
testPathIgnorePatterns: ['/node_modules/'],
testTimeout: 30000,
clearMocks: true
};
Bước 2: Dùng Node.js Script để quản lý vòng đời Docker (Testcontainers Alternative)
Thay vì bắt Developer phải gõ docker-compose up bằng tay rồi mới chạy test, hãy viết một script tự động:
- Gán cổng riêng (
PORT=3001thay vì3000) để không đụng Dev Server. execSync('docker-compose up -d db redis kafka').- Dùng package
wait-onđể poll healthcheck cho đến khi server ready. - Chạy
npm run test:e2e:run. - Thu dọn tàn cuộc:
execSync('docker-compose down').
// Dịch vụ chờ sẵn sàng trước khi test chặn Flaky connections
execute(`npx wait-on http-get://127.0.0.1:${TEST_PORT}/health -t 120000`);
execute('jest --config ./jest.e2e.config.js');
Bước 3: Fix triệt để "read ECONNRESET" của Kafka khi test Local
Lỗi điển hình nhất khi tích hợp Kafka vào E2E Local là Test chạy ngoài Host nhưng Kafka bị nhốt trong Docker.
Giải pháp: Ánh xạ tường minh PLAINTEXT_HOST qua cổng khác (VD: 9093)
# docker-compose.yml
kafka:
ports:
- "9093:9093"
environment:
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:9093
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,PLAINTEXT_HOST://:9093,CONTROLLER://:9094
Sau đó, trong file .env.test, bạn trỏ KAFKA_BROKER=localhost:9093.
Bước 4: Viết Assertion "Thép" (Deterministic)
Loại bỏ ngay các assertion lỏng lẻo. Database của bạn đã được dọn sạch (hoặc bạn dùng random Seed như Date.now()), vì vậy kết quả trả về phải cứng nhắc!
it('should create a user successfully via REST', async () => {
const uniqueEmail = `test_${Date.now()}@example.com`;
const response = await request(SERVER_URL)
.post('/api/users')
.send({ name: 'Test User', email: uniqueEmail });
// Cương quyết bắt buộc trả về 201 Created
expect(response.statusCode).toBe(201);
});
4. Kết luận
Chuyển dịch sang phương pháp Testing tách bạch Unit và E2E (Isolated Dockerized Testing) sẽ lấy đi của bạn thêm 1-2 ngày setup hạ tầng ban đầu. Nhưng đổi lại, nó mang lại một Sự bình yên tuyệt đối cho team khi code lớn lên.
Nếu bạn thấy quá trình setup này quá rườm rà, bạn có thể tham khảo trực tiếp cấu trúc thư mục và toàn bộ script Docker tự động mà mình đã cấu hình sẵn trong công cụ CLI mã nguồn mở của mình tại:
Hãy để lại sao (⭐) nếu nó hữu ích với bạn nhé! Chúc các bạn code sạch mượt mà và không bao giờ phải thấy dòng chữ Test Failed randomly trên Jenkins / GitHub Actions nữa!
All rights reserved