- Authors
- Name
- Nguyễn Đức Xinh
- Published on
- Published on
Pyramid testing (Kim Tự Tháp Kiểm Thử): Hướng Dẫn Về Chiến Lược Kiểm Thử Phần Mềm
Giới Thiệu
Pyramid testing là một khái niệm cơ bản trong kiểm thử phần mềm giúp các nhóm tổ chức chiến lược kiểm thử một cách hiệu quả. Nó được phổ biến bởi Mike Cohn trong cuốn sách "Succeeding with Agile" và đã trở thành nền tảng của các phương pháp kiểm thử phần mềm hiện đại.
Pyramid testing là gì?
Pyramid testing là một phép ẩn dụ trực quan thể hiện sự phân bố lý tưởng của các loại kiểm thử khác nhau trong một dự án phần mềm. Nó gợi ý rằng bạn nên có:
- Một số lượng lớn các unit test (kiểm thử đơn vị) ở đáy
- Một số lượng nhỏ hơn các integration test (kiểm thử tích hợp) ở giữa
- Một số lượng nhỏ nhất các end-to-end test (kiểm thử end-to-end) ở đỉnh
Các Thành Phần Của Pyramid testing
1. Kiểm Thử Đơn Vị (Lớp Đáy)
- Định nghĩa: Kiểm thử các thành phần hoặc hàm riêng lẻ một cách độc lập
- Đặc điểm:
- Thực thi nhanh - Fast execution
- Dễ bảo trì - Easy to maintain
- Độ phủ cao - High coverage
- Tách biệt với các phụ thuộc bên ngoài - Isolated from external dependencies
- Ví dụ: Kiểm thử các hàm, lớp, hoặc phương thức riêng lẻ
- Ai thực hiện Unit Tests?: Các nhà phát triển phần mềm thực hiện unit testing. Họ chịu trách nhiệm xác định xem mã của họ có hoạt động hay không.
- Khi nào Unit Testing được thực hiện?: Unit tests thường được thực hiện trong giai đoạn phát triển.
Unit testing là nền tảng của test pyramid Nó giúp kiểm tra các thành phần riêng lẻ của một codebase lớn. Chúng rất quan trọng vì chúng xác định các vấn đề ở cấp độ mã có thể gây ra vấn đề sau này Unit testing giúp các nhà phát triển đảm bảo rằng mỗi đơn vị mã, chẳng hạn như hàm hoặc phương thức, hoạt động như mong đợi, từ đó giúp họ phát hiện lỗi trong giai đoạn đầu của quá trình phát triển phần mềm. Nhìn chung, unit testing giúp cải thiện hiệu quả mã, từ đó giúp các nhà phát triển xây dựng một sản phẩm mạnh mẽ.
// Example of a unit test using Jest
describe('Calculator', () => {
test('should calculate total price correctly', () => {
// Arrange
const items = [
{ name: 'item1', price: 10 },
{ name: 'item2', price: 20 }
];
// Act
const total = calculateTotal(items);
// Assert
expect(total).toBe(30);
});
test('should format date correctly', () => {
const date = new Date('2024-01-01');
const formattedDate = formatDate(date);
expect(formattedDate).toBe('01/01/2024');
});
});
// Example of mocking external dependencies
describe('UserService', () => {
test('should create user with hashed password', () => {
// Arrange
const mockHash = jest.fn().mockReturnValue('hashed_password');
const userService = new UserService({ hashPassword: mockHash });
// Act
const user = userService.createUser('john', 'password123');
// Assert
expect(mockHash).toHaveBeenCalledWith('password123');
expect(user.password).toBe('hashed_password');
});
});
2. Integration Test (Kiểm Thử Tích Hợp) - Lớp Giữa
- Định nghĩa: Kiểm thử sự tương tác giữa nhiều thành phần
- Đặc điểm:
- Tốc độ thực thi trung bình
- Kiểm thử tương tác giữa các thành phần
- Có thể liên quan đến các dịch vụ bên ngoài
- Ví dụ: Kiểm thử API, kiểm thử tích hợp cơ sở dữ liệu
- Ai thực hiện Integration Tests?: Integration testing thường được thực hiện bởi các tester.
- Khi nào Integration Testing được thực hiện?: Integration testing thường được thực hiện sau khi phát triển hoàn thành nhưng trước khi phần mềm được phát hành. Nó thường được thực hiện sau unit testing, được thực hiện trong quá trình phát triển để test các đơn vị riêng lẻ một cách cô lập. Sau khi các đơn vị riêng lẻ đã được test, chúng được tích hợp lại với nhau để tạo thành các thành phần lớn hơn, và sau đó các thành phần lớn hơn này được test để đảm bảo chúng hoạt động cùng nhau một cách chính xác.
// Example of an API integration test using Jest and Supertest
const request = require('supertest');
const app = require('../app');
const db = require('../db');
describe('User API Integration', () => {
beforeEach(async () => {
await db.clear();
});
test('should create and retrieve user', async () => {
// Arrange
const userData = {
username: 'testuser',
email: 'test@example.com'
};
// Act
const createResponse = await request(app)
.post('/api/users')
.send(userData);
// Assert
expect(createResponse.status).toBe(201);
expect(createResponse.body.username).toBe(userData.username);
// Verify database state
const user = await db.getUserByEmail(userData.email);
expect(user).toBeDefined();
expect(user.username).toBe(userData.username);
});
});
// Example of a database integration test
describe('UserRepository', () => {
test('should save and retrieve user from database', async () => {
// Arrange
const repo = new UserRepository();
const user = new User('John', 'john@example.com');
// Act
const savedUser = await repo.save(user);
// Assert
expect(savedUser.id).toBeDefined();
const retrievedUser = await repo.findById(savedUser.id);
expect(retrievedUser).toEqual(savedUser);
});
});
3. End-to-End Test (Kiểm Thử End-to-End) - Lớp Đỉnh
- Định nghĩa: Kiểm thử toàn bộ luồng ứng dụng từ đầu đến cuối
- Đặc điểm:
- Thực thi chậm
- Phức tạp trong bảo trì
- Mô phỏng các kịch bản người dùng thực tế
- Ví dụ: Kiểm thử giao diện người dùng, kiểm thử quy trình hoàn chỉnh
- Ai thực hiện End-to-End Tests?: End-to-End testing được thực hiện bởi các nhóm QA.
- Khi nào End-to-End Testing được thực hiện?: End-to-End testing thường được thực hiện sau integration testing.
End-to-End testing, hay còn gọi là E2E testing, giúp test toàn bộ chức năng của sản phẩm từ đầu đến cuối. Nó liên quan đến việc test toàn bộ luồng của sản phẩm, từ giao diện người dùng đến backend. Ở đây, các QA sẽ phải test ứng dụng từ góc độ người dùng cuối bằng cách sử dụng các kịch bản thực tế. Hãy hiểu điều này bằng một ví dụ. Giả sử tester phải test trang đăng nhập của ứng dụng.
QA sẽ test gì trong E2E testing - Họ sẽ test tất cả các kịch bản người dùng. Đó là, các tester sẽ phải thực hiện cả test tích cực và tiêu cực để kiểm tra xem ứng dụng có thể xử lý các dữ liệu khác nhau hay không. Ví dụ, người dùng có thể thực hiện bất kỳ hành động nào sau đây trên biểu mẫu đăng nhập:
- Nhập tên người dùng và mật khẩu hợp lệ
- Nhập tên người dùng và mật khẩu trống
- Nhấp vào nút đăng nhập bằng Google/Facebook
- Nhập tên người dùng và mật khẩu không hợp lệ
- Nhấp vào nút đăng ký
Các test này thường được tự động hóa, cung cấp một cách đáng tin cậy để đảm bảo rằng sản phẩm hoạt động tốt sau bất kỳ thay đổi hoặc cập nhật nào.
// Example of an E2E test using Playwright
const { test, expect } = require('@playwright/test');
test.describe('E-commerce Purchase Flow', () => {
test('should complete purchase successfully', async ({ page }) => {
// Visit the website
await page.goto('https://example.com');
// Add item to cart
await page.locator('[data-testid="product-card"]').first().click();
await page.locator('[data-testid="add-to-cart"]').click();
// Go to cart
await page.locator('[data-testid="cart-icon"]').click();
// Proceed to checkout
await page.locator('[data-testid="checkout-button"]').click();
// Fill shipping information
await page.locator('[data-testid="shipping-address"]').fill('123 Main St');
await page.locator('[data-testid="city"]').fill('New York');
await page.locator('[data-testid="zip-code"]').fill('10001');
// Fill payment information
await page.locator('[data-testid="card-number"]').fill('4111111111111111');
await page.locator('[data-testid="expiry-date"]').fill('12/25');
await page.locator('[data-testid="cvv"]').fill('123');
// Complete purchase
await page.locator('[data-testid="place-order"]').click();
// Verify success
await expect(page).toHaveURL(/.*order-confirmation/);
await expect(page.locator('[data-testid="success-message"]'))
.toContainText('Order placed successfully');
});
});
// Example of an E2E test for user registration with multiple browsers
test.describe('User Registration', () => {
test('should register new user and redirect to dashboard', async ({ page }) => {
await page.goto('/register');
// Fill registration form
await page.locator('[data-testid="username"]').fill('newuser');
await page.locator('[data-testid="email"]').fill('newuser@example.com');
await page.locator('[data-testid="password"]').fill('password123');
await page.locator('[data-testid="confirm-password"]').fill('password123');
// Submit form
await page.locator('[data-testid="register-button"]').click();
// Verify redirect and welcome message
await expect(page).toHaveURL(/.*dashboard/);
await expect(page.locator('[data-testid="welcome-message"]'))
.toContainText('Welcome, newuser');
});
// Example of testing with different viewport sizes
test('should handle registration on mobile devices', async ({ page }) => {
// Set viewport to mobile size
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/register');
// Test mobile-specific UI elements
await expect(page.locator('.mobile-menu-button')).toBeVisible();
// Fill form and submit
await page.locator('[data-testid="username"]').fill('mobileuser');
await page.locator('[data-testid="email"]').fill('mobile@example.com');
await page.locator('[data-testid="password"]').fill('password123');
await page.locator('[data-testid="register-button"]').click();
await expect(page).toHaveURL(/.*dashboard/);
});
});
// Example of testing with authentication
test.describe('Authenticated User Flow', () => {
// Use test.beforeEach to set up authentication
test.beforeEach(async ({ page }) => {
await page.goto('/login');
await page.locator('[data-testid="email"]').fill('test@example.com');
await page.locator('[data-testid="password"]').fill('password123');
await page.locator('[data-testid="login-button"]').click();
await expect(page).toHaveURL(/.*dashboard/);
});
test('should access protected profile page', async ({ page }) => {
await page.locator('[data-testid="profile-link"]').click();
await expect(page).toHaveURL(/.*profile/);
await expect(page.locator('[data-testid="user-email"]'))
.toContainText('test@example.com');
});
});
Tại sao Software Testing Pyramid có lợi hơn cho các nhóm Agile?
- Testing pyramid giúp các nhóm agile đưa sản phẩm ra thị trường nhanh hơn bằng cách thúc đẩy phản hồi nhanh hơn, chất lượng được cải thiện và chi phí giảm.
- Sử dụng các phương pháp agile như Test-Driven Development (TDD) cho unit testing giúp đảm bảo mã đơn giản, đáng tin cậy và không có lỗi. Cách tiếp cận này liên quan đến việc viết test trước khi viết bất kỳ mã sản xuất nào.
- Cách tiếp cận này giúp các nhóm agile quản lý thời gian tốt hơn và đạt được kết quả tuyệt vời vì pyramid được xây dựng theo cách để chạy các test dễ tiếp cận nhất trước, bắt đầu với unit testing.
- Nó giúp các nhóm agile ưu tiên testing dựa trên rủi ro liên quan đến mỗi loại test.
Thực hiện các test này theo cách thủ công sẽ tiêu tốn nhiều thời gian, chi phí và công sức hơn. Nhưng, tự động hóa các test cho phép testing hiệu quả và phản hồi nhanh hơn ở tất cả các cấp độ của pyramid.
Lợi Ích Của Pyramid testing
Pyramid testing là một chiến lược kiểm thử hiệu quả giúp các nhóm agile đảm bảo phần mềm của họ được kiểm thử kỹ lưỡng trong khi tối thiểu hóa chi phí và nỗ lực cần thiết cho việc kiểm thử. Nó cũng mang lại nhiều lợi ích khác.
- Kiểm Thử Hiệu Quả: Tập trung nguồn lực vào nơi chúng hiệu quả nhất
- Phản Hồi Nhanh: Các kiểm thử đơn vị nhanh cung cấp phản hồi tức thì
- Tiết Kiệm Chi Phí: Giảm chi phí bảo trì bằng cách tối thiểu hóa các kiểm thử E2E đắt đỏ
- Độ Phủ Tốt Hơn: Đảm bảo độ phủ kiểm thử toàn diện
- Gỡ Lỗi Dễ Dàng Hơn: Các kiểm thử độc lập giúp dễ dàng xác định vấn đề
Các Mẫu Phản Tác Dụng Thường Gặp
- Hình Nón Kem: Quá nhiều kiểm thử E2E và không đủ unit test
- Hình Đồng Hồ Cát: Nặng về cả unit test và E2E, nhẹ về kiểm thử tích hợp
- Kim Tự Tháp Ngược: Nhiều kiểm thử E2E hơn unit test
Best Practice
- Duy Trì Tỷ Lệ: Giữ hình dạng kim tự tháp trong tâm trí khi lập kế hoạch kiểm thử
- Tự Động Hóa Mọi Thứ: Tất cả các kiểm thử nên được tự động hóa
- Tích Hợp Liên Tục: Chạy kiểm thử như một phần của pipeline CI/CD
- Tách Biệt Kiểm Thử: Giữ các kiểm thử độc lập với nhau
- Bảo Trì Thường Xuyên: Xem xét và cập nhật kiểm thử thường xuyên
Mẹo Triển Khai
- Bắt đầu với unit test cho các tính năng mới
- Thêm kiểm thử tích hợp cho các quy trình quan trọng
- Sử dụng kiểm thử E2E một cách tiết kiệm cho các flow người dùng chính
- Mock các phụ thuộc bên ngoài trong unit test
- Sử dụng test doubles một cách phù hợp
Kết Luận
Pyramid testing cung cấp nền tảng vững chắc để xây dựng chiến lược kiểm thử mạnh mẽ. Bằng cách tuân theo các nguyên tắc của nó, các nhóm có thể đạt được độ phủ kiểm thử tốt hơn, chu kỳ phản hồi nhanh hơn và bộ kiểm thử dễ bảo trì hơn. Hãy nhớ rằng Pyramid testing là một hướng dẫn, không phải một quy tắc cứng nhắc - hãy điều chỉnh nó cho phù hợp với nhu cầu cụ thể của dự án của bạn trong khi vẫn duy trì các nguyên tắc cốt lõi.
Đọc Thêm
- "Succeeding with Agile" của Mike Cohn
- "Test-Driven Development" của Kent Beck
- "Working Effectively with Legacy Code" của Michael Feathers