| 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)