Test Automation Best Practices
Learn the essential best practices for building maintainable and scalable test automation frameworks
Introduction
Test automation is crucial for maintaining software quality while moving fast. However, poorly designed automation can become a maintenance nightmare. This guide covers essential best practices for building robust test automation.
Core Principles
1. The Test Pyramid
Follow the test pyramid approach:
- Unit Tests (70%): Fast, isolated, testing individual components
- Integration Tests (20%): Testing component interactions
- E2E Tests (10%): Full user journey testing
// Good: Unit test
describe('UserValidator', () => {
it('should validate email format', () => {
const validator = new UserValidator();
expect(validator.isValidEmail('test@example.com')).toBe(true);
expect(validator.isValidEmail('invalid-email')).toBe(false);
});
});
// Good: Integration test
describe('User Registration', () => {
it('should create user and send welcome email', async () => {
const user = await createUser({ email: 'test@example.com' });
const emailSent = await checkEmailQueue('test@example.com');
expect(emailSent).toBe(true);
});
});2. Keep Tests Independent
Each test should be able to run independently without relying on others.
// Bad: Tests depend on execution order
test('create user', async () => {
userId = await createUser(userData);
});
test('update user', async () => {
await updateUser(userId, newData); // Depends on previous test
});
// Good: Each test is independent
test('create user', async () => {
const userId = await createUser(userData);
expect(userId).toBeDefined();
});
test('update user', async () => {
const userId = await createUser(userData); // Set up own data
const updated = await updateUser(userId, newData);
expect(updated).toBe(true);
});3. Use Page Object Model
Separate test logic from page implementation details.
// page-objects/LoginPage.js
class LoginPage {
constructor(page) {
this.page = page;
this.emailInput = '#email';
this.passwordInput = '#password';
this.submitButton = '[type="submit"]';
}
async login(email, password) {
await this.page.fill(this.emailInput, email);
await this.page.fill(this.passwordInput, password);
await this.page.click(this.submitButton);
}
async getErrorMessage() {
return await this.page.textContent('.error-message');
}
}
// tests/login.spec.js
test('login with invalid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.login('invalid@example.com', 'wrongpassword');
const error = await loginPage.getErrorMessage();
expect(error).toContain('Invalid credentials');
});Test Data Management
1. Use Test Data Builders
Create reusable test data builders:
class UserBuilder {
constructor() {
this.user = {
email: `test${Date.now()}@example.com`,
name: 'Test User',
role: 'user',
};
}
withEmail(email) {
this.user.email = email;
return this;
}
withRole(role) {
this.user.role = role;
return this;
}
build() {
return this.user;
}
}
// Usage
const adminUser = new UserBuilder()
.withEmail('admin@example.com')
.withRole('admin')
.build();2. Clean Up Test Data
Always clean up after tests:
describe('User Management', () => {
let testUser;
beforeEach(async () => {
testUser = await createTestUser();
});
afterEach(async () => {
await deleteTestUser(testUser.id);
});
test('should update user profile', async () => {
// Test code
});
});Waiting Strategies
1. Avoid Hard Waits
// Bad: Hard wait
await page.waitForTimeout(3000);
// Good: Wait for specific condition
await page.waitForSelector('.data-loaded');
await page.waitForLoadState('networkidle');2. Use Smart Waiting
// Wait for element to be visible and enabled
await page.waitForSelector('button.submit', {
state: 'visible',
timeout: 5000
});
// Wait for API response
await page.waitForResponse(
response => response.url().includes('/api/users') && response.status() === 200
);Assertions
1. Use Descriptive Assertions
// Bad: Generic assertion
expect(result).toBe(true);
// Good: Descriptive assertion
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
expect(result.data).toMatchObject({
id: expect.any(String),
email: 'test@example.com'
});2. Test Both Positive and Negative Cases
describe('Email Validation', () => {
test('accepts valid email formats', () => {
expect(validateEmail('user@example.com')).toBe(true);
expect(validateEmail('user.name@example.co.uk')).toBe(true);
});
test('rejects invalid email formats', () => {
expect(validateEmail('invalid')).toBe(false);
expect(validateEmail('@example.com')).toBe(false);
expect(validateEmail('user@')).toBe(false);
});
});CI/CD Integration
1. Parallel Execution
Run tests in parallel to speed up execution:
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v3
- name: Run tests
run: npm test -- --shard=${{ matrix.shard }}/42. Fail Fast
Configure tests to fail fast on critical issues:
// jest.config.js
module.exports = {
bail: 1, // Stop after first test failure
maxWorkers: '50%', // Use 50% of CPU cores
testTimeout: 10000, // 10 second timeout
};Reporting and Debugging
1. Capture Screenshots on Failure
test('complex user flow', async ({ page }) => {
try {
// Test steps
} catch (error) {
await page.screenshot({ path: `failure-${Date.now()}.png` });
throw error;
}
});2. Use Meaningful Test Names
// Bad
test('test1', () => {});
// Good
test('should display error message when login fails with invalid credentials', () => {});Common Pitfalls to Avoid
- Over-reliance on UI tests: Keep most tests at unit/integration level
- Flaky tests: Use proper waits and avoid race conditions
- Tight coupling: Don't tie tests to implementation details
- Poor test organization: Group related tests and use descriptive names
- Ignoring test failures: Fix or remove failing tests immediately
- Not reviewing test code: Test code needs reviews too
Conclusion
Effective test automation requires:
- Following the test pyramid
- Writing independent, maintainable tests
- Using proper patterns like Page Object Model
- Smart waiting and assertion strategies
- Good CI/CD integration
- Continuous improvement
Remember: The goal is to increase confidence in your codebase, not just to increase test count.
Further Reading
Comments (0)
Loading comments...