Testing Best Practices
This guide outlines best practices for testing in LightNap, covering strategies, mocking, coverage, and integration with development workflows.
Testing Strategy
Testing Pyramid
LightNap follows a balanced testing pyramid:
- Unit Tests (Base): Test individual functions and components
- Integration Tests: Test component interactions and services
- E2E Tests (Top): Test complete user workflows
When to Write Tests
- New Features: Write tests alongside implementation
- Bug Fixes: Add regression tests
- Refactoring: Ensure tests pass after changes
- Critical Paths: Test authentication, data operations, user flows
Unit Testing Best Practices
Test Structure
describe('ComponentName', () => {
describe('when initialized', () => {
it('should display default state', () => {
// Test implementation
});
});
describe('when user interacts', () => {
it('should update state accordingly', () => {
// Test implementation
});
});
});
Mocking Strategy
Service Mocking
const mockService = jasmine.createSpyObj('MyService', ['method1', 'method2']);
mockService.method1.and.returnValue(of(testData));
TestBed.configureTestingModule({
providers: [
{ provide: MyService, useValue: mockService }
]
});
HTTP Mocking
import { HttpTestingController } from '@angular/common/http/testing';
it('should fetch data', () => {
service.getData().subscribe();
const req = httpMock.expectOne('/api/data');
expect(req.request.method).toBe('GET');
req.flush(mockData);
httpMock.verify();
});
Using Test Builders
LightNap provides builders for consistent test data:
import { UserDtoBuilder } from '@testing/builders';
const testUser = new UserDtoBuilder()
.withName('John Doe')
.withEmail('john@example.com')
.build();
E2E Testing Best Practices
Page Object Pattern
// cypress/support/page-objects/login-page.ts
export class LoginPage {
visit() {
cy.visit('/identity/login');
return this;
}
fillCredentials(username: string, password: string) {
cy.get('[data-cy="login-username"]').type(username);
cy.get('[data-cy="login-password"]').type(password);
return this;
}
submit() {
cy.get('[data-cy="login-submit"]').click();
return this;
}
}
// Usage
const loginPage = new LoginPage();
loginPage.visit().fillCredentials('user', 'pass').submit();
Custom Commands
// cypress/support/commands.ts
Cypress.Commands.add('login', (username: string, password: string) => {
cy.session([username, password], () => {
cy.visit('/identity/login');
cy.get('[data-cy="login-username"]').type(username);
cy.get('[data-cy="login-password"]').type(password);
cy.get('[data-cy="login-submit"]').click();
cy.url().should('not.include', '/identity/login');
});
});
// Usage
cy.login('admin', 'password');
Coverage Goals
Target Coverage Levels
- Statements: 45% (current threshold)
- Branches: 25%
- Functions: 40%
- Lines: 42%
Coverage Analysis
Focus coverage on:
- Business Logic: Service methods, complex calculations
- User Interactions: Component event handlers
- Error Handling: Exception paths, validation
- Edge Cases: Boundary conditions, error states
Accept lower coverage for:
- UI Boilerplate: Simple getters/setters, template logic
- Generated Code: Auto-generated files
- Trivial Code: Simple property bindings
CI/CD Integration
Running Tests in Pipeline
# .github/workflows/ci.yml
- name: Run Unit Tests
run: npm run test:ci
- name: Run E2E Tests
run: npm run e2e:ci
- name: Upload Coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
Parallel Execution
# Run E2E tests in parallel
npm run e2e:ci
# Configured with --record --parallel in package.json
Test Reporting
- Coverage Reports: Upload to Codecov or similar
- Test Results: Store JUnit XML for CI dashboards
- Screenshots: Capture on E2E failures
Debugging Tests
Unit Test Debugging
it('should debug component behavior', () => {
spyOn(console, 'log'); // Capture console output
component.debugMethod();
expect(console.log).toHaveBeenCalledWith('Debug info');
});
E2E Debugging
it('should debug user flow', () => {
cy.visit('/page');
// Pause execution
cy.pause();
// Debug element
cy.get('[data-cy="element"]').debug();
// Log information
cy.get('[data-cy="element"]').then($el => {
cy.log('Element text:', $el.text());
});
});
Test Maintenance
Keeping Tests Green
- Regular Execution: Run tests frequently during development
- CI Monitoring: Fix broken tests immediately
- Refactor Tests: Update tests when code changes
- Remove Flaky Tests: Replace unreliable tests with better implementations
Test Code Quality
- DRY Principle: Extract common test setup
- Descriptive Names: Clear test and describe blocks
- Documentation: Comment complex test scenarios
- Performance: Keep tests fast and focused
Common Pitfalls
Avoid These Patterns
Testing Implementation Details
// Bad: Testing private methods
expect(component['privateMethod']()).toBe(true);
// Good: Test public behavior
component.publicAction();
expect(component.publicProperty).toBe(expectedValue);
Over-Mocking
// Bad: Mocking everything
const mockEverything = jasmine.createSpyObj('Service', ['method1', 'method2', ...]);
// Good: Mock only what's needed
const mockService = jasmine.createSpyObj('Service', ['neededMethod']);
Brittle Selectors
// Bad: CSS-dependent
cy.get('.btn-primary').click();
// Good: Semantic
cy.get('[data-cy="submit-button"]').click();
Race Conditions
// Bad: Fixed waits
cy.wait(3000);
// Good: Assertions
cy.get('[data-cy="result"]').should('be.visible');
Tools and Resources
Recommended Tools
- Wallaby.js: Real-time test execution in IDE
- Cypress Dashboard: Cloud-based test management
- TestCafe: Alternative E2E framework
- Jest: Alternative unit testing framework