Testing Practices
This document outlines the testing practices and procedures for the Digibit ERP system.
Testing Strategy
Test Pyramid
Our testing strategy follows the test pyramid principle: - Unit Tests: 70% of tests - Test individual units of code in isolation - Integration Tests: 20% of tests - Test interactions between components/modules - End-to-End Tests: 10% of tests - Test complete user workflows
Unit Testing
Framework
- Use Jasmine as the testing framework
- Use Karma as the test runner
- Leverage Angular Testing Utilities
Writing Unit Tests
Component Tests
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CompanyDetailComponent } from './company-detail.component';
import { CompanyService } from '../services/company.service';
import { of } from 'rxjs';
describe('CompanyDetailComponent', () => {
let component: CompanyDetailComponent;
let fixture: ComponentFixture<CompanyDetailComponent>;
let companyService: jasmine.SpyObj<CompanyService>;
beforeEach(async () => {
const spy = jasmine.createSpyObj('CompanyService', ['getCompany']);
await TestBed.configureTestingModule({
declarations: [CompanyDetailComponent],
providers: [
{ provide: CompanyService, useValue: spy }
]
}).compileComponents();
companyService = TestBed.inject(CompanyService) as jasmine.SpyObj<CompanyService>;
});
beforeEach(() => {
fixture = TestBed.createComponent(CompanyDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should load company on initialization', () => {
const mockCompany = { id: '1', name: 'Test Company' };
companyService.getCompany.and.returnValue(of(mockCompany));
component.companyId = '1';
component.ngOnInit();
expect(companyService.getCompany).toHaveBeenCalledWith('1');
expect(component.company).toEqual(mockCompany);
});
});
Service Tests
import { TestBed } from '@angular/core/testing';
import { CompanyService } from './company.service';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
describe('CompanyService', () => {
let service: CompanyService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [CompanyService]
});
service = TestBed.inject(CompanyService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should retrieve companies', () => {
const mockCompanies = [
{ id: '1', name: 'Company 1' },
{ id: '2', name: 'Company 2' }
];
service.getCompanies().subscribe(companies => {
expect(companies.length).toBe(2);
expect(companies).toEqual(mockCompanies);
});
const req = httpMock.expectOne('api/companies');
expect(req.request.method).toBe('GET');
req.flush(mockCompanies);
});
});
Test Coverage
- Aim for at least 80% code coverage
- Focus on testing business logic rather than simple getter/setter methods
- Use Istanbul for coverage reports
- Monitor coverage trends over time
Integration Testing
Component Integration
- Test how components interact with each other
- Verify data flow between parent and child components
- Test directive and pipe functionality with real data
Service Integration
- Test services with actual HTTP requests (when appropriate)
- Verify data transformations and business logic
- Test error handling in real scenarios
End-to-End Testing
Framework
- Use Cypress for end-to-end testing
- Test critical user journeys
- Simulate real user interactions
Example E2E Test
describe('Company Management', () => {
beforeEach(() => {
cy.visit('/companies');
cy.login(); // Custom command for authentication
});
it('should allow creating a new company', () => {
cy.get('[data-testid="add-company-btn"]').click();
cy.get('[data-testid="company-name-input"]').type('New Company');
cy.get('[data-testid="company-description-input"]').type('Description for new company');
cy.get('[data-testid="save-company-btn"]').click();
cy.get('[data-testid="company-list"]')
.contains('New Company')
.should('be.visible');
});
it('should display companies in the list', () => {
cy.get('[data-testid="company-list-item"]').should('have.length.greaterThan', 0);
});
});
E2E Best Practices
- Test user workflows rather than individual components
- Use data-testid attributes for element selection
- Keep tests independent and deterministic
- Use fixtures for test data
- Mock external services that are not under test
Testing Guidelines
Test Naming Convention
- Use the format:
[Scenario] should [Expected Result] when [Condition] - Example:
should display error message when username is empty
Arrange-Act-Assert Pattern
Structure tests using the AAA pattern: 1. Arrange: Set up test data and conditions 2. Act: Execute the functionality being tested 3. Assert: Verify the expected outcome
Mocking Dependencies
- Use mocks for external services and APIs
- Mock complex dependencies to isolate the unit under test
- Use fake data that represents realistic scenarios
Async Testing
- Handle asynchronous operations properly
- Use async/await or promises as appropriate
- Wait for operations to complete before assertions
Continuous Testing
Pre-commit Hooks
- Run unit tests before committing code
- Fail fast if tests don't pass
- Use lint-staged to run tests only on changed files
CI Pipeline
- Execute tests on every push to the repository
- Run different test suites in parallel when possible
- Generate coverage reports and publish them
- Fail the build if tests fail or coverage drops below threshold
Quality Metrics
Test Effectiveness
- Track test pass/fail rates
- Monitor flaky tests and address them promptly
- Measure mean time to detect failures
- Evaluate test execution time and optimize slow tests
Code Quality
- Integrate static analysis tools
- Check for code complexity and maintainability
- Identify and eliminate duplicate code
- Ensure consistent code style across the project