Skip to main content

Test Automation Best Practices

Created: May 8, 2026 Larry Qu 3 min read

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();
    ```javascript
    
  2. Test behavior, not implementation:
    // ✅ Good: Test behavior
    expect(userService.createUser(data)).toHaveProperty('id');
    
    // ❌ Bad: Test implementation
    expect(userService.createUser).toHaveBeenCalled();
    ```javascript
    
  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

Resources

Comments

Share this article

Scan to read on mobile