Testing this workspace — zoneless Vitest
SkillDev toolsUse whenever writing, editing, running, or fixing a Vitest spec (`*.spec.ts`) in this workspace — signal stores, `rxMethod`s, `computed`s, presentational and container components, mocks/spies, or pure utils. ESPECIALLY reach for this the moment a test involves anything async (`rxMethod`, HTTP, a timer) or a signal-driven effect, because this app is zoneless and the usual `fakeAsync`/`tick` recipe silently does not work here. If you find yourself about to import `fakeAsync`, `tick`, or reach for `zone.js`, stop and read this first. Also reach for this when you're chasing 100% Vitest coverage.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Testing this workspace — zoneless Vitest skill
What this skill tells your AI
The instructions your AI receives, as published by fabiangosebrink/dog-rate-app in .claude/skills/angular-testing/SKILL.md and read by ahel’s review.
Tests run on Vitest (via @nx/vitest + AnalogJS), not Karma or Jest, and the app is
fully zoneless — there is no zone.js dependency and there never will be (deliberate
architectural choice). That single fact changes how every async and effect-driven test is
written, so internalize it before writing assertions.
The one rule that trips everyone up
Never use Angular's fakeAsync / tick, and never add zone.js. They depend on Zone
being patched in; here it is not installed, so tick() does nothing and the test either
hangs, times out, or passes vacuously. Adding zone.js to make them work fights the whole
architecture and will be reverted. src/test-setup.ts stays the zoneless sibling version
(setupTestBed() only) — do not swap in @analogjs/vitest-angular/setup-zone.
Two zoneless tools replace fakeAsync/tick:
| You need to flush… | Use |
|---|---|
a timer — setTimeout, an rxjs delay/asyncScheduler delay | Vitest fake timers: vi.useFakeTimers() / vi.advanceTimersByTime(ms) |
a signal-driven effect — an rxMethod wired to a signal, a component/store effect() | TestBed.tick() (synchronously runs pending effects, zonelessly) |
You sometimes need both in the same test: TestBed.tick() to push the signal's value into the
pipeline, then vi.advanceTimersByTime() to flush a timer inside it.
Arrange-Act-Assert, always
Every test is structured as Arrange → Act → Assert, each phase preceded by a bare comment —
// Arrange, // Act, // Assert — with no explanation text in the comment. This is a
narrow, deliberate exception to the workspace's "avoid comments" rule: these three words are
structural labels, not prose explaining what the code does.
Every test name always starts with "should " — it('should rate the selected dog', ...),
not it('rates the selected dog', ...). This applies to every it(...) description in the
workspace, including one-off it('should create', ...) smoke tests.
Testing pure utils
Calculation or mapping logic belongs in a pure function in the lib's utils/ (e.g. a rating or
grouping helper), not in a component. Those get plain unit tests with no TestBed — call the
function, assert the return. This is the cheapest, fastest layer, so push logic here and cover it
directly rather than through the DOM.
import { getAverageRating } from './rating.utils';
it('should average the rating sum over the count', () => {
// Act
const average = getAverageRating({ ratingSum: 40, ratingCount: 10 } as Dog);
// Assert
expect(average).toBe(4);
});
Recipes for specific test shapes
Read the matching reference file before writing the test — each covers one shape in full, with a worked example:
references/mocking.md— mocking services inline inMockProvider(notngMocks.defaultMock), and capturing spies into named variables and asserting on the variable, not the mock's property (MockProvider/vi.spyOnalike).references/testing-stores.md— testing asignalStore: providing it inTestBed, driving it through its methods, whyTestBed.tick()is needed to flush anonIniteffect, and using a small factory plusTestBed.resetTestingModule()when a test needs a different initial module.references/testing-components.md— the base fixture/whenStable()shape, presentational (dumb) components, asserting onoutput()emissions, the one sanctioned exception for reaching into a component's class, and container components (mock the store and the children).
Running tests
Project names follow the lib path (libs/dogs/feature → dogs-feature).
npx nx test dogs-feature # one project
npx nx test dogs-feature -- -t "MyDogsStore" # one describe/it by name
npx nx test dogs-feature -- --coverage # per-file coverage table (v8)
npx nx run-many -t test # everything
If Nx can't find a newly created/renamed project, run npx nx reset once (clears the daemon
cache) and retry. Keep test output pristine — a stray warning is a finding, not noise.
When asked for full coverage on a lib, run with --coverage and iterate until statements,
branches, functions and lines are all 100%. Treat every gap as one of exactly two things: a
missing DOM-reachable test for a real branch, or the unreachable-computed exception in
references/testing-components.md — never leave an uncovered line unexplained.
Two config rules keep that 100% honest — hold them when creating or editing a lib's test setup:
- Every lib's
vite.config.mtssetscoverage.include: ['src/**/*.ts']. Without it, the v8 provider only sees files the specs import, so an untested file is invisible and the number lies.excludeis only for files with genuinely nothing to instrument (src/test-setup.ts, the public-APIsrc/index.ts,**/*.spec.ts, and documented cases like interface-only models) — never exclude a file just because covering it is inconvenient. - The
testtarget'soutputsinproject.jsonuses the{workspaceRoot}prefix ({workspaceRoot}/coverage/libs/<path>), not{options.reportsDirectory}— that option holds a project-relative../../../path Nx resolves from the workspace root, landing outside the repo. With the wrong output, cache hits replay green tests but silently drop the lib from the combined coverage report.
Where to put the logic you're testing (so it's testable)
Testing pain is usually a design smell. Before writing an elaborate DOM/async test, check the
angular-components, angular-signals, and state-management skills — the patterns they
enforce are what make tests small:
- Logic in the store, not the component. A
computedview-model in the store is tested by reading a signal; the same mapping in a component template needs a rendered fixture. - Pure functions for mapping/calculation (in
utils/) are tested without Angular at all. - Dumb components take finished data via
input()and render — assert the markup or theoutput(), don't reconstruct logic in the test. - Coordinate stores with events, test each side on its own. A store that dispatches an event
and a store that reacts to it (
withReducer(on(...))) are each tested in isolation.
Signals
- GitHub stars
- 99
- Forks
- 11
- Last commit
- Jul 2026
Advanced
- Catalog kind
- skill
- Gateway key
angular-testing- Source
- github.com/fabiangosebrink/dog-rate-app