Skip to main content
โšก Calmops

Test Automation Best Practices

Test Automation Best Practices

Test automation requires careful planning and maintenance. This article covers best practices for sustainable test suites.

Introduction

Test automation provides:

  • Regression prevention
  • Faster feedback
  • Scalability
  • Cost reduction
  • Quality assurance

Understanding best practices helps you:

  • Write maintainable tests
  • Scale test suites
  • Reduce flakiness
  • Improve efficiency
  • Maintain quality

Test Design

Test Pyramid

        /\
       /  \  E2E Tests (10%)
      /____\
     /      \
    /  API   \ Integration Tests (30%)
   /  Tests  \
  /___________\
 /             \
/ Unit Tests    \ Unit Tests (60%)
/_______________\

Test Organization

// โœ… Good: Organized test structure
// tests/
// โ”œโ”€โ”€ unit/
// โ”‚   โ”œโ”€โ”€ utils.test.js
// โ”‚   โ”œโ”€โ”€ helpers.test.js
// โ”‚   โ””โ”€โ”€ validators.test.js
// โ”œโ”€โ”€ integration/
// โ”‚   โ”œโ”€โ”€ api.test.js
// โ”‚   โ”œโ”€โ”€ database.test.js
// โ”‚   โ””โ”€โ”€ auth.test.js
// โ””โ”€โ”€ e2e/
//     โ”œโ”€โ”€ login.cy.js
//     โ”œโ”€โ”€ registration.cy.js
//     โ””โ”€โ”€ checkout.cy.js

// โœ… Good: Descriptive test names
describe('UserService', () => {
  describe('createUser', () => {
    it('should create user with valid data', () => {});
    it('should throw error with invalid email', () => {});
    it('should hash password before saving', () => {});
  });

  describe('updateUser', () => {
    it('should update user name', () => {});
    it('should not update password without verification', () => {});
  });
});

Test Maintenance

Reducing Flakiness

// โœ… Good: Wait for elements
cy.get('.loading').should('not.exist');
cy.get('.content').should('be.visible');

// โŒ Bad: No waiting
cy.get('.content').click();

// โœ… Good: Use explicit waits
cy.get('.button', { timeout: 10000 }).click();

// โœ… Good: Retry logic
cy.get('.element').should('exist').and('be.visible');

// โŒ Bad: Race conditions
setTimeout(() => {
  cy.get('.element').click();
}, 1000);

Test Isolation

// โœ… Good: Isolated tests
describe('User API', () => {
  beforeEach(async () => {
    await db.clear();
    await db.seed('users', []);
  });

  it('creates user', async () => {
    const user = await api.createUser({ name: 'John' });
    expect(user.id).toBeDefined();
  });

  it('gets user', async () => {
    const created = await api.createUser({ name: 'John' });
    const user = await api.getUser(created.id);
    expect(user.name).toBe('John');
  });
});

// โŒ Bad: Test dependencies
describe('User API', () => {
  let userId;

  it('creates user', async () => {
    const user = await api.createUser({ name: 'John' });
    userId = user.id;
  });

  it('gets user', async () => {
    const user = await api.getUser(userId);
    expect(user.name).toBe('John');
  });
});

Test Scaling

Parallel Execution

# โœ… Good: Run tests in parallel
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        test-suite: [unit, integration, e2e]
    steps:
      - run: npm test -- --suite=${{ matrix.test-suite }}

# โœ… Good: Distribute tests
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - run: npm test -- --shard=${{ matrix.shard }}/4

Test Sharding

// โœ… Good: Shard tests
// jest.config.js
module.exports = {
  testMatch: ['**/__tests__/**/*.js'],
  // Shard configuration
  shard: {
    shardIndex: process.env.SHARD_INDEX || 0,
    shardCount: process.env.SHARD_COUNT || 1
  }
};

// Run with sharding
SHARD_INDEX=0 SHARD_COUNT=4 npm test
SHARD_INDEX=1 SHARD_COUNT=4 npm test
SHARD_INDEX=2 SHARD_COUNT=4 npm test
SHARD_INDEX=3 SHARD_COUNT=4 npm test

Best Practices

  1. Use page objects:

    // โœ… Good: Page object
    class LoginPage {
      login(email, password) {
        cy.get('input[type="email"]').type(email);
        cy.get('input[type="password"]').type(password);
        cy.get('button[type="submit"]').click();
      }
    }
    
    // โŒ Bad: Inline selectors
    cy.get('input[type="email"]').type(email);
    cy.get('input[type="password"]').type(password);
    cy.get('button[type="submit"]').click();
    
  2. Test behavior, not implementation:

    // โœ… Good: Test behavior
    expect(userService.createUser(data)).toHaveProperty('id');
    
    // โŒ Bad: Test implementation
    expect(userService.createUser).toHaveBeenCalled();
    
  3. Keep tests simple:

    // โœ… Good: Simple test
    it('adds numbers', () => {
      expect(add(2, 3)).toBe(5);
    });
    
    // โŒ Bad: Complex test
    it('does everything', () => {
      // Multiple assertions
      // Multiple setups
      // Hard to understand
    });
    

Summary

Test automation best practices are essential. Key takeaways:

  • Follow test pyramid
  • Organize tests logically
  • Reduce flakiness
  • Isolate tests
  • Scale with parallelization
  • Use page objects
  • Test behavior
  • Keep tests simple

Next Steps

Comments