name: readable-tests
description: Apply test-naming conventions, AAA structure, and readability rules when writing or reviewing any *.spec.ts file. Use whenever naming describe/it blocks, structuring test bodies, deciding what to assert, or reviewing tests for clarity. Triggers on: "write a test for", "add a spec", "name this test", "review my test", "is this test readable", editing any *.spec.ts, or any question about test structure, naming, or what to assert.
Writing readable Angular unit tests
This skill covers how tests should read and be structured. It is about naming, layout, and what to assert — not which testing framework APIs to use. For ng-mocks / MockBuilder / MockRender specifics, see the ng-mocks skill.
Naming
describe blocks
Name after the class under test using .name:
describe(UserBannerComponent.name, () => { … });
describe(OffersApiService.name, () => { … });
describe(PriceFormatPipe.name, () => { … });
Use nested describe blocks to group tests by scenario or feature — not by method name:
describe(UserBannerComponent.name, () => {
describe('when user is logged in', () => { … });
describe('when user is logged out', () => { … });
describe('logout button', () => { … });
});
it blocks
Pattern: it('should <outcome> when <condition>', …)
it('should display the user name when a user is returned from the store', …);
it('should hide the banner when selectUser returns null', …);
it('should dispatch logoutRequested when the logout button is clicked', …);
it('should work', …);
it('should render', …);
it('renders correctly', …);
Rules:
- Always start with
should
- State the outcome first, then the condition (
when …)
- Be specific enough that a failing test name tells you what broke without reading the body
- No abbreviations or internal jargon in test names
AAA — Arrange, Act, Assert
Every test body follows three phases in order. Never mix them.
it('should return formatted price for a valid number', () => {
const value = 9.5;
const result = pipe.transform(value);
expect(result).toBe('$9.50');
});
Rules:
- One logical assertion focus per test — a test can have multiple
expect calls if they all verify the same behavior
- If "Act" takes more than two lines, extract a helper
- Never put assertions inside
beforeEach — they belong in it blocks
- Avoid logic (
if, for) inside test bodies; extract to helpers or parametrize with it.each
One behavior per test
Each it block should verify one behavior, not a sequence of states:
it('should display name and dispatch on click', () => {
expect(nativeElement.textContent).toContain('Alice');
ngMocks.click('button');
expect(store.dispatch).toHaveBeenCalledWith(logoutRequested());
});
it('should display the user name', () => {
expect(nativeElement.textContent).toContain('Alice');
});
it('should dispatch logoutRequested when logout is clicked', () => {
ngMocks.click('button');
expect(store.dispatch).toHaveBeenCalledWith(logoutRequested());
});
What to assert
Assert behavior, not implementation:
expect(component.formatLabel).toHaveBeenCalledWith('sale');
expect(nativeElement.querySelector('.badge')?.textContent).toBe('SALE');
Guidelines:
- Prefer DOM assertions (
nativeElement.textContent, querySelector) over component property assertions
- Use semantic selectors (
button[type=submit], [data-testid=logout]) over fragile CSS class selectors
- Assert on outputs (emitted values, dispatched actions, DOM state) not on internal method calls
- Do not assert on analytics/telemetry calls unless the test is specifically for analytics logic
- Avoid
toBeTruthy() when a more specific matcher exists (toBe(true), toContain(…), toEqual(…))
Parametrized tests
Use it.each for table-driven data instead of copy-pasting it blocks:
it.each([
[null, '—'],
[undefined, '—'],
[0, '$0.00'],
[9.5, '$9.50'],
])('should format %s as %s', (input, expected) => {
expect(pipe.transform(input)).toBe(expected);
});
Helper functions
Extract repeated setup or DOM queries into named helpers in the same file:
function getLogoutButton(): HTMLButtonElement {
return nativeElement.querySelector('button[data-testid=logout]')!;
}
Keep helpers in the same describe block they serve. Do not export them.
Anti-patterns — do not do these
- ❌
it('should work') — tells you nothing when it fails
- ❌ Multiple unrelated behaviors in one
it block
- ❌ Assertions inside
beforeEach
- ❌
expect(x).toBeTruthy() when expect(x).toBe(true) or expect(x).toEqual(…) is available
- ❌ Asserting on private methods or internal state instead of observable outputs
- ❌ Copy-pasted
it blocks differing only in data — use it.each
- ❌
// Arrange / Act / Assert comments in the body — the structure should be self-evident from spacing and naming
- ❌ Logic (
if, loops) inside it blocks
Where to look for more
Reference files in this skill:
references/naming.md — extended naming examples across components, services, effects, pipes, guards
references/patterns.md — full test bodies for common readable-test patterns (parametrized, DOM-focused, event-driven)