Automated Testing - Frontend
This document serves as a guide for setting up, writing, and running automated tests for our frontend project. It explains the frameworks, tools, and conventions used to ensure consistency and efficiency.
Overview
Automated tests are crucial for ensuring our React application's reliability and maintainability. We implement three levels of testing.
- Unit Tests: Vitest for component and unit testing
- E2E Tests: Cypress for integration and end-to-end testing
- CI Pipeline: GitHub Actions for automated testing
Test Structure
Directory Layout
├── src/
│ ├── __tests__/ # Unit tests
│ │ ├── components/ # Component tests
│ │ └── unit/ # Utility function tests
├── cypress/
│ ├── e2e/ # Integration tests
│ ├── fixtures/ # Test data
│ └── support/ # Custom commands & utilities
Writing Tests
Unit Tests (Vitest)
Component Test Example
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import LoginForm from '../components/LoginForm'
describe('LoginForm', () => {
it('should handle successful login', async () => {
const onSubmit = vi.fn()
render(<LoginForm onSubmit={onSubmit} />)
// Fill form
await fireEvent.change(screen.getByLabelText('Username'), {
target: { value: 'testuser' }
})
await fireEvent.change(screen.getByLabelText('Password'), {
target: { value: 'password123' }
})
// Submit form
await fireEvent.click(screen.getByRole('button', { name: /login/i }))
// Assertions
expect(onSubmit).toHaveBeenCalledWith({
username: 'testuser',
password: 'password123'
})
})
it('should show validation errors', async () => {
render(<LoginForm />)
// Submit empty form
await fireEvent.click(screen.getByRole('button', { name: /login/i }))
// Check error messages
expect(screen.getByText('Username is required')).toBeInTheDocument()
})
})
Utility Test Example
describe('RealmService', () => {
let service;
beforeEach(() => {
service = new RealmService();
});
test('should construct URL correctly', () => {
expect(service.getUrl()).toBe(`${API_URL}/api/realm`);
});
});
E2E Tests (Cypress)
Login Flow Example
describe('Login Flow', () => {
beforeEach(() => {
cy.mockRealApiCalls();
})
it('should login successfully', () => {
cy.login('correctuser@qat.info', 'password');
cy.url().should('include', '/#/ApplicationDashboard');
})
it('should handle login errors', () => {
cy.intercept('POST', `${API_URL}/authenticate`, {
statusCode: 401,
body: { messageCode: 'static.message.login.invalidCredentials' }
}).as('loginRequest');
cy.login('wrong@email.com', 'wrongpass');
cy.contains('Incorrect login or password').should('be.visible');
cy.url().should('include', '/#/login');
})
})
Testing Standards
Unit Tests
-
Naming Convention
- Test files:
*.test.{js,jsx}
- Test suites: Describe the component/function
- Test cases: Start with "should" and describe expected behavior
- Test files:
-
Component Testing
- Test user interactions
- Test rendering logic
- Test prop changes
- Test error states
- Use React Testing Library queries in this order:
- getByRole
- getByLabelText
- getByText
- getByTestId (last resort)
-
Mocking
- Mock external dependencies
- Use
vi.mock()
for module mocks - Use
vi.fn()
for function mocks - Reset mocks between tests
- Keep mocks minimal:
- If your test requires more than 2-3 mocks, this is often a code smell
- Consider either:
- Refactoring the component to reduce dependencies
- Writing an integration test instead of a unit test
- Breaking down the component into smaller, more focused pieces
E2E Tests
-
Structure
- Group related tests in a single spec file
- Use descriptive test names
- Setup test data in
beforeEach
- Clean up after tests
-
Best Practices
- Mock API responses for consistent tests
- Use custom commands for common operations
- Add data-testid for stable selectors
- Test the happy path first, then edge cases
-
Custom Commands
// cypress/support/commands.js
Cypress.Commands.add('login', (email = 'test@example.com', password = 'password123') => {
cy.visit('/#/login', {
onBeforeLoad: (win) => {
win.WebSocket = undefined;
}
});
cy.window().then((win) => {
win.localStorage.setItem('loginOnline', 'true');
win.localStorage.setItem('sessionTimedOut', '0');
win.localStorage.setItem('sessionChanged', '0');
win.localStorage.setItem('lastLoggedInUsersLanguageChanged', 'false');
});
// Fill in the login form and submit
cy.get('#emailId').should('be.visible').type(email);
cy.get('#password').should('be.visible').type(password);
cy.get('button[type="submit"]').should('be.visible').click();
});
Running Tests
Unit Tests
npm test # Run tests in watch mode
npm run test:coverage # Run tests with coverage
npm run test:ui # Run tests with UI
E2E Tests
npm run cypress:open # Open Cypress UI
npm run cypress:run # Run Cypress headless
npm run test:e2e # Start dev server and run Cypress
All Tests
npm run test:all # Run both unit and E2E tests
CI/CD Pipeline
- Runs on every push (excluding documentation changes)
- Sequential execution: unit tests → E2E tests → coverage reporting
- Artifacts: test videos and screenshots available on failure
- Coverage reports sent to Code Climate
Coverage Requirements
- Minimum overall coverage: 80%
- Minimum function coverage: 80%
- Minimum branch coverage: 75%
Debugging Tests
Unit Tests
- Use
test.only()
to run specific tests - Use
console.log()
ordebug()
from Testing Library - Check test coverage with
npm run test:coverage
E2E Tests
- Use
cy.pause()
to pause test execution - Use
.debug()
to inspect elements - Check screenshots and videos in
cypress/screenshots
andcypress/videos
- Use Cypress UI for step-by-step debugging
Common Issues & Solutions
Unit Tests
-
Async Updates
// Wrong
fireEvent.click(button)
expect(result).toBe('updated')
// Correct
await fireEvent.click(button)
await waitFor(() => {
expect(result).toBe('updated')
}) -
Component Re-renders
// Wrong
const { rerender } = render(<Component prop={value} />)
rerender()
// Correct
const { rerender } = render(<Component prop={value} />)
rerender(<Component prop={newValue} />)
E2E Tests
- Race Conditions
// Wrong
cy.get('.loading').should('not.exist')
cy.get('.content')
// Correct
cy.get('.loading', { timeout: 10000 }).should('not.exist')
cy.get('.content').should('be.visible')
Contributing
- Write tests for new features
- Update tests for modified features
- Ensure all tests pass locally before pushing
- Follow the testing standards outlined above
- Include updated test coverage reports
References
Feel free to extend this documentation as our testing strategy evolves!