This document describes the unit testing approach and patterns used for testing Angular components and services in the eform-client/src/app/modules directory.
- Jest: Modern JavaScript testing framework
- jest-preset-angular: Angular-specific Jest preset
- Angular Testing Utilities:
TestBed,ComponentFixture, etc.
jest.config.js: Jest configuration with Angular-specific settingssrc/setup-jest.ts: Jest setup file with Angular test environment initializationsrc/tsconfig.spec.json: TypeScript configuration for tests
# Run all tests
npm run test:unit
# Run tests in watch mode (for development)
npm run test:watch
# or
npm run test:local_unit
# Run tests with coverage
npm run test:unit
# Run tests for CI/CD
npm run test:ciEvery component test file should follow this structure:
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ComponentName } from './component-name.component';
// Import dependencies...
describe('ComponentName', () => {
let component: ComponentName;
let fixture: ComponentFixture<ComponentName>;
let mockService: jasmine.SpyObj<ServiceName>;
beforeEach(waitForAsync(() => {
// Create spy objects for dependencies
mockService = jasmine.createSpyObj('ServiceName', ['method1', 'method2']);
TestBed.configureTestingModule({
declarations: [ComponentName],
providers: [
{ provide: ServiceName, useValue: mockService },
// ... other providers
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ComponentName);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
});
// Additional tests...
});Always mock external dependencies using Jasmine spies:
// Create a spy object
mockService = jasmine.createSpyObj('ServiceName', ['getAllItems', 'createItem']);
// Configure return values
mockService.getAllItems.and.returnValue(of({
success: true,
message: '',
model: []
}));
// Verify method calls
expect(mockService.getAllItems).toHaveBeenCalled();
expect(mockService.createItem).toHaveBeenCalledWith(expectedData);let mockDialog: jasmine.SpyObj<MatDialog>;
beforeEach(() => {
mockDialog = jasmine.createSpyObj('MatDialog', ['open']);
});
// In tests
const mockDialogRef = {
afterClosed: () => of(true) // or of(false) for cancellation
};
mockDialog.open.and.returnValue(mockDialogRef as any);let mockStore: jasmine.SpyObj<Store>;
beforeEach(() => {
mockStore = jasmine.createSpyObj('Store', ['select', 'dispatch']);
mockStore.select.and.returnValue(of(true)); // or appropriate selector value
});let mockTranslateService: jasmine.SpyObj<TranslateService>;
beforeEach(() => {
mockTranslateService = jasmine.createSpyObj('TranslateService', ['stream', 'instant']);
mockTranslateService.stream.and.returnValue(of('Translated Text'));
});describe('loadAllItems', () => {
it('should load items successfully', () => {
const mockItems: ItemDto[] = [
{ id: 1, name: 'Item 1' } as ItemDto,
{ id: 2, name: 'Item 2' } as ItemDto
];
const mockResult: OperationDataResult<Array<ItemDto>> = {
success: true,
message: '',
model: mockItems
};
mockService.getAllItems.and.returnValue(of(mockResult));
component.loadAllItems();
expect(mockService.getAllItems).toHaveBeenCalled();
expect(component.items).toEqual(mockItems);
});
it('should handle unsuccessful response', () => {
const mockResult: OperationDataResult<Array<ItemDto>> = {
success: false,
message: 'Error message',
model: null
};
mockService.getAllItems.and.returnValue(of(mockResult));
component.loadAllItems();
expect(mockService.getAllItems).toHaveBeenCalled();
expect(component.items).toEqual([]);
});
});describe('openCreateModal', () => {
it('should open modal and reload data on success', () => {
const mockDialogRef = {
afterClosed: () => of(true)
};
mockDialog.open.and.returnValue(mockDialogRef as any);
spyOn(component, 'loadAllItems');
component.openCreateModal();
expect(mockDialog.open).toHaveBeenCalled();
expect(component.loadAllItems).toHaveBeenCalled();
});
it('should not reload data when modal is cancelled', () => {
const mockDialogRef = {
afterClosed: () => of(false)
};
mockDialog.open.and.returnValue(mockDialogRef as any);
spyOn(component, 'loadAllItems');
component.openCreateModal();
expect(mockDialog.open).toHaveBeenCalled();
expect(component.loadAllItems).not.toHaveBeenCalled();
});
});For components that use MatDialogRef:
let mockDialogRef: jasmine.SpyObj<MatDialogRef<ComponentName>>;
beforeEach(() => {
mockDialogRef = jasmine.createSpyObj('MatDialogRef', ['close']);
});
it('should close dialog with true when successful', () => {
component.hide(true);
expect(mockDialogRef.close).toHaveBeenCalledWith(true);
});
it('should close dialog with false by default', () => {
component.hide();
expect(mockDialogRef.close).toHaveBeenCalledWith(false);
});let mockDialogData: DataModel;
beforeEach(() => {
mockDialogData = { id: 1, name: 'Test' } as DataModel;
TestBed.configureTestingModule({
declarations: [ComponentName],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: mockDialogData },
// ... other providers
]
});
});
it('should initialize with dialog data', () => {
expect(component.data).toBeDefined();
expect(component.data.id).toBe(1);
});All API responses use these models, which must include the message property:
// OperationResult
const mockResult: OperationResult = {
success: true,
message: ''
};
// OperationDataResult
const mockResult: OperationDataResult<T> = {
success: true,
message: '',
model: data
};Reference implementations can be found in:
src/app/modules/advanced/components/units/units.component.spec.tssrc/app/modules/advanced/components/units/unit-create/unit-create.component.spec.tssrc/app/modules/advanced/components/units/units-otp-code/units-otp-code.component.spec.tssrc/app/modules/advanced/components/workers/workers/workers.component.spec.tssrc/app/modules/advanced/components/workers/worker-edit-create/worker-edit-create.component.spec.tssrc/app/modules/advanced/components/folders/folders/folders.component.spec.ts
- Methods: All public methods should have test coverage
- Scenarios: Test both success and failure cases
- Edge Cases: Test null/undefined values, empty arrays, etc.
- Interactions: Test component interactions with services and dialogs
- Use descriptive test names:
'should load items successfully' - Group related tests using
describeblocks - Start with
'should'for behavior descriptions - Be specific about what is being tested
- Missing message property: Ensure all OperationResult/OperationDataResult objects include
message: '' - Zone.js imports: Use
import 'zone.js'andimport 'zone.js/testing'in test.ts - Karma timeouts: Increase
browserNoActivityTimeoutin karma.conf.js for slow tests - Spy not configured: Always configure spy return values with
.and.returnValue()
# Run tests with source maps for debugging
ng test --source-map
# Run a single test file (may not work with all configurations)
ng test --include='**/component-name.component.spec.ts'
# Increase Karma logging
# Edit karma.conf.js and set logLevel: config.LOG_DEBUGTests are automatically run in GitHub Actions workflows using Jest:
# Run tests in CI/CD mode
npm run test:ciJest is configured with:
- Code coverage reporting
- CI-specific optimizations (--ci flag)
- Maximum of 2 workers for parallel execution
When adding new components:
- Create a
.spec.tsfile alongside the component - Follow the patterns documented here
- Ensure all public methods are tested
- Test both success and error scenarios
- Run tests locally before committing