Tại sao kiểm thử E2E và tích hợp ổn định là cần thiết?
Giới thiệu
Kiểm thử end-to-end (E2E) và tích hợp là những thành phần thiết yếu giúp duy trì sự tự tin trong sản phẩm của bạn. Chúng xác thực các luồng người dùng và tương tác của hệ thống. Tuy nhiên, khi các bài kiểm thử không ổn định—có thể thất bại theo cách không liên quan đến mã—chúng cản trở việc phát hành, gây lãng phí thời gian gỡ lỗi và làm giảm lòng tin.
Bài viết này sẽ trình bày một cách tiếp cận thực tiễn để ổn định các bài kiểm thử bằng cách chạy chúng trong Docker với một hình ảnh xem trước của ứng dụng, kết nối với một bản sao cơ sở dữ liệu nhỏ gọn. Kết quả là các bài kiểm thử sẽ chạy nhanh, ổn định và có thể lặp lại.
Vấn đề với các bài kiểm thử không ổn định
Các nguồn gây ra sự không ổn định:
- Môi trường chia sẻ: Các bài kiểm thử truy cập vào một cơ sở dữ liệu chính/chia sẻ lớn và liên tục thay đổi.
- Phụ thuộc dữ liệu không kiểm soát: Các thực thể cần thiết có thể bị thiếu hoặc thay đổi theo thời gian.
- Cơ sở dữ liệu nặng nề: Các bản sao đầy đủ mất nhiều thời gian để chụp nhanh, khôi phục và di chuyển, đặc biệt trong CI.
- Sự thay đổi môi trường: Các phiên bản thư viện, dịch vụ hoặc cờ tính năng khác nhau giữa dev/CI/prod.
Giải pháp: Môi trường Docker tách biệt + Bản sao cơ sở dữ liệu nhỏ gọn
Chúng tôi cải thiện độ ổn định bằng cách tách biệt mọi thứ cho mỗi lần chạy kiểm thử:
- Hình ảnh Docker xem trước của ứng dụng (xây dựng từ nhánh hiện tại/PR).
- Container cơ sở dữ liệu riêng biệt (mới cho mỗi lần chạy) với một tập dữ liệu nhỏ, được biết đến là tốt.
- Gieo hạt có thể lặp lại: Một hạt gieo có thể lặp lại chỉ chứa các thực thể và mối quan hệ tối thiểu cần thiết.
Lợi ích:
- Dữ liệu ổn định, hành vi dự đoán được, thiết lập nhanh hơn và ít trường hợp sai sót hơn.
Cài đặt tham khảo (Docker Compose)
Dưới đây là một tệp Compose tối thiểu cho một ứng dụng web và một cơ sở dữ liệu Postgres. Hãy điều chỉnh tên dịch vụ, cổng và biến môi trường cho ngăn xếp của bạn.
yaml
version: "3.9"
services:
app:
image: ghcr.io/your-org/your-app:${GIT_SHA:-preview}
depends_on:
- db
environment:
NODE_ENV: test
DATABASE_URL: postgres://testuser:testpass@db:5432/testdb
ports:
- "8080:8080"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 5s
timeout: 2s
retries: 20
db:
image: postgres:16
environment:
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
POSTGRES_DB: testdb
ports:
- "5433:5432"
volumes:
- db_data_test:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U testuser -d testdb"]
interval: 5s
timeout: 2s
retries: 20
volumes:
db_data_test:
Xây dựng một hình ảnh xem trước
Xây dựng hình ảnh xem trước trong CI từ nhánh hiện tại và gán nó với SHA cam kết:
dockerfile
# Ví dụ ứng dụng Node
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app .
EXPOSE 8080
CMD ["node", "dist/server.js"]
Gieo hạt có thể lặp lại: Bản sao cơ sở dữ liệu nhỏ gọn
Một bản sao nhỏ gọn không phải là một bản sao đầy đủ của master/prod. Nó là một tập dữ liệu được tuyển chọn chỉ chứa các bảng/hàng cần thiết cho kịch bản kiểm thử của bạn—người dùng, vai trò, cờ tính năng, dữ liệu danh mục tối thiểu, v.v.
Ví dụ (Postgres) gieo hạt
sql
-- Vai trò tối thiểu
INSERT INTO roles (id, name) VALUES
('00000000-0000-0000-0000-000000000001', 'admin'),
('00000000-0000-0000-0000-000000000002', 'user')
ON CONFLICT (id) DO NOTHING;
-- Người dùng kiểm thử
INSERT INTO users (id, email, role_id, created_at)
VALUES ('10000000-0000-0000-0000-000000000001', 'testuser@example.com',
'00000000-0000-0000-0000-000000000002', NOW())
ON CONFLICT (id) DO NOTHING;
-- Cờ tính năng (chỉ những cờ cần thiết cho các luồng đang kiểm thử)
INSERT INTO feature_flags (key, enabled)
VALUES ('checkout_v2', true)
ON CONFLICT (key) DO UPDATE SET enabled = EXCLUDED.enabled;
-- Thực thể miền thiết yếu cho các luồng E2E
INSERT INTO products (id, name, price_cents)
VALUES (1, 'Sample Product', 1999)
ON CONFLICT (id) DO NOTHING;
Áp dụng các migration và gieo hạt trong CI
Sử dụng một Makefile đơn giản để tổ chức các bước vòng đời:
makefile
.PHONY: up migrate seed test down
up:
docker compose -f docker-compose.test.yml up -d --wait
migrate:
# Thay thế bằng công cụ migration của bạn, ví dụ: Prisma, Knex, Flyway, Liquibase
docker compose -f docker-compose.test.yml exec -T app npm run migrate:up
seed:
docker compose -f docker-compose.test.yml exec -T db \
psql -U testuser -d testdb < seed/slim-seed.sql
# Ví dụ: Playwright/Cypress/WDIO/v.v.
test:
docker compose -f docker-compose.test.yml exec -T app npm run test:e2e
down:
docker compose -f docker-compose.test.yml down -v
Sau đó trong CI (ví dụ GitHub Actions):
yaml
name: e2e
on: [pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Xây dựng hình ảnh xem trước
run: |
docker build -t ghcr.io/your-org/your-app:${{ github.sha }} .
- name: Bắt đầu ngăn xếp
run: |
GIT_SHA=${{ github.sha }} make up
- name: Di chuyển & gieo hạt
run: |
make migrate
make seed
- name: Chạy kiểm thử
run: |
make test
- name: Dọn dẹp
if: always()
run: |
make down
Tạo một bản sao nhỏ từ một cơ sở dữ liệu lớn (Tùy chọn)
Nếu bạn phải lấy tập dữ liệu nhỏ từ nguồn lớn, đừng sao chép tất cả. Chỉ lấy những hàng tối thiểu và duy trì tính toàn vẹn tham chiếu.
Chiến lược A: Gieo hạt thủ công (được khuyến nghị)
- Viết các câu lệnh
INSERTphi idempotent cho một số thực thể cần thiết. - Giữ nó trong kiểm soát phiên bản và xem xét các thay đổi cùng với các bài kiểm thử.
Chiến lược B: Xuất chọn lọc (ví dụ Postgres)
Sử dụng pg_dump với danh sách bao gồm và bộ lọc:
bash
# Xuất chỉ schema (migrations nhanh)
pg_dump \
--schema-only \
--no-owner --no-privileges \
"$DATABASE_URL" > schema.sql
# Xuất dữ liệu tối thiểu cho các bảng cụ thể
pg_dump \
--data-only \
--table=roles \
--table=feature_flags \
--table=products \
--column-inserts \
"$DATABASE_URL" > minimal-data.sql
cat schema.sql minimal-data.sql > seed/slim-seed.sql
Chiến lược C: Xuất theo chương trình
- Viết một script nhỏ (Node/TS) chỉ truy vấn các hàng/cột cần thiết và phát hành các dữ liệu SQL/JSON.
- Bắt buộc ID ổn định (UUID) cho các tham chiếu có thể xác định trong các lần chạy.
Mẹo: Giữ cho các hạt nhỏ (dưới vài trăm hàng). Nếu một bài kiểm thử cần dữ liệu mới, mở rộng dần hạt gieo và thêm một xác nhận để chứng minh điều đó.
Chạy kiểm thử trong ngăn xếp
Trình chạy kiểm thử của bạn (ví dụ: Playwright, Cypress, WebdriverIO, Jest-integration) nên:
- Chờ kiểm tra sức khỏe của ứng dụng và cơ sở dữ liệu.
- Sử dụng một
BASE_URLduy nhất đã biết (ví dụ:http://app:8080). - Tạo dữ liệu kiểm thử tạm thời trên cơ sở khi cần thiết, hoặc tái sử dụng các hạt gieo.
Ví dụ đoạn cấu hình Playwright:
javascript
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: process.env.BASE_URL || 'http://localhost:8080',
trace: 'retain-on-failure',
video: 'retain-on-failure',
},
});
Chạy lệnh:
bash
BASE_URL=http://localhost:8080 npx playwright test
Khắc phục sự không ổn định: Danh sách kiểm tra ngắn
- Thời gian: Thay thế việc sử dụng
waittùy ý bằng cácwaitrõ ràng cho trạng thái mạng/DOM. - Dữ liệu: Đảm bảo các hạt được áp dụng và idempotent; đặt lại DB giữa các kịch bản nếu cần.
- Tách biệt: Không bài kiểm thử nào nên phụ thuộc vào trạng thái từ các bài kiểm thử trước đó.
- Đồng hồ: Giả mạo thời gian nếu các luồng phụ thuộc vào thời gian (mã thông báo, hết hạn, cron).
- API bên ngoài: Stub/mirror các cuộc gọi của bên thứ ba; không để chúng làm hỏng CI của bạn.
- Retry: Sử dụng retry cấp độ kiểm thử một cách tiết kiệm và chỉ cho các điểm đã biết không ổn định.
Kết luận
Các bài kiểm thử ổn định là nền tảng của việc phát hành đáng tin cậy. Bằng cách chạy các bài kiểm thử E2E/tích hợp trong Docker với một hình ảnh ứng dụng xem trước và một bản sao cơ sở dữ liệu nhỏ gọn, bạn loại bỏ sự trôi dạt dữ liệu và kết nối môi trường. Các bộ kiểm thử của bạn trở nên nhanh hơn, có thể xác định và dễ tin tưởng hơn.
Trong thực tiễn, các đội ngũ áp dụng cách tiếp cận này đã thấy giảm mạnh tỉ lệ thất bại trong kiểm thử do các vấn đề về dữ liệu hoặc môi trường, đồng thời đạt được tăng tốc độ thực thi kiểm thử đáng kể. Các chu kỳ phản hồi nhanh hơn và ít báo động sai hơn trực tiếp dẫn đến năng suất của lập trình viên và tự tin hơn trong các bản phát hành.
Nếu đội ngũ của bạn đang phải đấu tranh với các bài kiểm thử không ổn định, hãy thử cách tiếp cận này. Bắt đầu với một hạt gieo nhỏ, thủ công; tự động hóa các migration; và giữ mọi thứ trong Compose. Nỗ lực ban đầu sẽ nhanh chóng mang lại lợi ích trong việc tiết kiệm thời gian kỹ thuật và bản phát hành an toàn hơn.
Tác động thực tế (Ví dụ số liệu)
Dưới đây là một cái nhìn trước/sau đơn giản mà nhiều đội ngũ quan sát khi chuyển sang Docker + bản sao cơ sở dữ liệu nhỏ:
| Chỉ số | Trước (DB chia sẻ) | Sau (DB nhỏ trong Docker) |
|---|---|---|
| Thời gian chạy E2E trung bình | ~45 phút | ~18 phút |
| Tỉ lệ thất bại bài kiểm thử không ổn định | 20–30% các lần chạy | <5% các lần chạy |
| Sự tự tin của lập trình viên | Thấp | Cao |
Ngay cả những số liệu gần đúng như thế này cũng tạo ra một lập luận mạnh mẽ trong các bài viết và bài thuyết trình nội bộ.
Phụ lục: Biến thể MongoDB (Bonus)
Đối với MongoDB, thay thế các dịch vụ/lệnh Postgres bằng các hình ảnh Mongo và mongorestore:
yaml
version: "3.9"
services:
app:
image: ghcr.io/your-org/your-app:${GIT_SHA:-preview}
environment:
MONGODB_URI: mongodb://testuser:testpass@mongo:27017/testdb?authSource=admin
depends_on:
- mongo
mongo:
image: mongo:7
ports:
- "27018:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: testuser
MONGO_INITDB_ROOT_PASSWORD: testpass
Ví dụ gieo hạt:
bash
# Dữ liệu JSON hoặc bsondump giữ nhỏ và được kiểm soát phiên bản
mongorestore --uri "mongodb://testuser:testpass@localhost:27018/testdb?authSource=admin" \
--drop ./seed/mongo/slim