8. Testing¶
C-PAT has two test suites: Vitest specs for the client and Node.js test runner tests for the API. This page describes what exists, how to run it, and the infrastructure a new test should use. The last section walks through adding one.
8.1. What exists¶
Client |
API |
|
|---|---|---|
Runner |
Vitest with |
The Node.js built-in test runner ( |
Files |
|
|
Configuration |
|
|
Coverage |
V8 provider; thresholds 80% lines, 80% functions, 70% branches, 80% statements over |
None configured |
Needs a server or database |
No |
No |
The five API test files cover the collection access helpers (poamAccess.test.js), the POAM status gates (poamGates.test.js), the collection team sync service (collectionTeamSyncService.test.js), the base href rewriting in the client bootstrap (clientBaseHref.test.js), and the error serializer (serializeError.test.js). There is no end-to-end suite.
8.2. Run tests¶
Command (from the package directory) |
Effect |
|---|---|
|
Vitest in watch mode. |
|
One run of every client spec. |
|
One run with coverage; fails when a threshold is missed. |
|
The Vitest browser UI. Open the tokenized URL it prints; the UI rejects requests without the token. |
|
One spec file. |
|
The specs that import the given source files. CI uses this form. |
|
Every API test. |
|
One API test file. |
8.3. Client test infrastructure¶
src/test-setup.ts initializes the Angular test environment and installs a global CPAT.Env stub with apiBase: '/api', stigman.apiUrl: '/stigman/api', basePath: '', classification: 'U', the two inactivity timeouts, dodDeployment: false, and a features block. Services read CPAT.Env when their fields initialize, so the stub exists before any service is created. Override a field in a beforeAll only when a spec needs a different value.
src/testing/ holds what specs share:
mocks/service-mocks.ts:createMockPayloadService,createMockSharedService,createMockRouter,createMockActivatedRoute,createMockMessageService,createMockConfirmationService,createMockDialogService,createMockDynamicDialogRef,createMockDynamicDialogConfig, andcreateMockOidcSecurityService, each returning an object ofvi.fn()functions and RxJS subjects.fixtures/poam-fixtures.ts:mockPoam,mockPoamList,mockMilestone,mockApprover,mockPoamAsset,mockPoamLabel,mockPoamLog.fixtures/user-fixtures.ts:mockUser,mockAdminUser,mockUserList,mockCollection,mockStigManagerCollection,mockTenableCollection,mockCollectionList,mockAssignedTeam,mockCollectionPermissionDetail,mockAsset,mockLabel.test-utils.ts:getElement,getAllElements,clickElement,setInputValue,getTextContent,elementExists,waitForAsync.
A service spec uses the HTTP testing controller and verifies that no request is left unanswered. This is the opening of src/app/pages/poams/poams.service.spec.ts:
import { TestBed } from '@angular/core/testing';
import { HttpErrorResponse, provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
describe('PoamService', () => {
let service: PoamService;
let httpMock: HttpTestingController;
const apiBase = '/api';
beforeEach(() => {
TestBed.configureTestingModule({
providers: [PoamService, provideHttpClient(), provideHttpClientTesting()]
});
service = TestBed.inject(PoamService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
A component spec imports the standalone component, replaces its collaborators with mocks, and uses NO_ERRORS_SCHEMA so that PrimeNG templates need not be compiled. From src/app/pages/labels/labels.component.spec.ts:
await TestBed.configureTestingModule({
imports: [LabelsComponent],
providers: [
{ provide: LabelService, useValue: mockLabelService },
{ provide: PayloadService, useValue: mockPayloadService },
{ provide: SharedService, useValue: mockSharedService },
{ provide: MessageService, useValue: mockMessageService },
{ provide: DialogService, useValue: mockDialogService }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
Three habits keep component specs honest. Where a collaborator exposes a signal, the mock holds a real signal (payload: signal({ lastCollectionAccessedId: 1 })) so that computed values in the component work. Where the component reads a viewChild, the spec replaces the property with a getter that returns a function, because viewChild results are signals:
Object.defineProperty(component, 'labelTable', { get: () => () => mockTable, configurable: true });
And timers are controlled with vi.useFakeTimers() rather than waited on. Import everything from vitest; the test APIs are not global in the editor even though globals is enabled at runtime, and Jasmine is not available.
8.4. API tests¶
API tests use node:test and node:assert/strict and build their own fakes rather than a database. From api/test/poamAccess.test.js:
function fakeConnection(rows) {
const calls = [];
return {
calls,
query: async (sql, params) => {
calls.push({ sql, params });
return [rows];
},
};
}
function fakeReq({ userId = 7, isAdmin = false } = {}) {
return { userObject: { userId, isAdmin } };
}
The fake connection records every query and returns the rows the test supplies, so a test can assert both the decision a function made and the SQL it sent. Name the file after the module under test with a .test.js suffix, keep it free of network and database access, and prefer testing the service function over the controller, which contains no logic.
8.5. Continuous integration¶
.github/workflows/pr-tests.yml runs on pull requests to main and development and in the merge queue. It has three jobs:
changed-tests(client). It diffs the pull request against its base and picks a mode. If the test infrastructure changed (package.json, the lockfile,vitest.config.mts,tsconfig.json,tsconfig.spec.json,src/test-setup.ts, or anything undersrc/testing/), if any source file was deleted, or if more than 300 files changed, it runs the full suite withnpm run test:run. Otherwise it runsnpx vitest related --runon the changed.tsfiles (a changed.htmlmaps to its sibling.ts). With no client changes it skips. The merge queue always runs the full suite.api-tests. Installs withnpm ci --ignore-scripts, lints the contract withnpm run lint:spec, and runsnpm test.docs. Builds the documentation withsphinx-build -W --keep-goingwhen files underdocs/or.readthedocs.yamlchanged. Any Sphinx warning fails the job.
There is no coverage gate in CI. Run npm run test:coverage locally when a change touches a file with thin coverage, and keep the thresholds green.
8.6. Known flakes¶
Nothing pins the TZ environment variable, and several specs and the code they exercise use local time: the notification specs build dates with new Date().toISOString(), and the milestone and comply-date utilities use date-fns functions that operate in local time. A date test that fails in the evening, when UTC has already moved to the next day, is usually this. Re-run with UTC before investigating:
$env:TZ = 'UTC'; npx vitest run src/app/pages/poams/poam-details/components/poam-milestones/poam-milestones.component.spec.ts
TZ=UTC npx vitest run src/app/pages/poams/poam-details/components/poam-milestones/poam-milestones.component.spec.ts
If the test passes under UTC and fails otherwise, fix the assertion to be timezone-independent rather than the code.
8.7. Add a test¶
Put the spec next to the code:
<name>.component.spec.tsbeside<name>.component.ts,<name>.service.spec.tsbeside the service, orapi/test/<module>.test.jsfor the API.Start from the matching boilerplate above. Use the factories in
src/testing/mocks/service-mocks.tsfor collaborators and the fixtures for data instead of writing new literals.If the code reads a
CPAT.Envfield the stub lacks, add the field to the stub insrc/test-setup.tsrather than to the spec, so that every spec sees the same shape.Run the one file with
npx vitest run <file>(ornode --test test/<file>.test.js) until it passes, then run the related specs withnpx vitest related --run <source files>to catch shared state.Run
npm run test:coveragewhen the change touches authentication, route guards, collection access, migrations, or request and response handling. Those areas always carry tests.Commit the spec with the change it covers.
client/spec/support/jasmine.mjs and the karma.conf.js entry in client/eslint.config.mjs are remnants of the previous runner and are not used.