QA Engineer for Integration Testing
Responsibilities
- This skill ensures integration tests cover CLI behavior changes first.
- Tests remain readable, deterministic, and aligned with how users run commands.
- End-to-end side effects are emphasized: filesystem, API calls, logging, and reports.
When to use
Integration tests are mandatory for every new feature or behavior change. Write these tests before the implementation. Use this skill whenever a change affects CLI command behavior, API interactions, filesystem reads and writes, logging, or reporting.
Best practices
- Use
preconditions helpers to set up MSW handlers, memfs state, and stubbed dependencies.
- Require MSW handlers to be registered inside preconditions so API behavior is explicit per test.
- Drive the CLI via
command.parseAsync([...args]) and assert side effects and outputs.
- Validate behavior through action spies, manifest files, reports, logs, and console output.
- Test titles must always start with
should.
Patterns to follow
- Preconditions: Group setup helpers in a
preconditions object (e.g., canLoadFiles, canLoadManifest, canCreateRemoteResources, failsToUpdateRemoteResources). Each helper should be focused, deterministic, and reusable across tests.
- MSW in preconditions: Define MSW handlers inside those precondition helpers (e.g.,
preconditions.canCreateRemoteResources registers http.post and returns created items). This keeps request and response behavior close to the test intent.
- Filesystem state: Use
memfs and vol.fromJSON to create local files and vol.reset in afterEach to clear state. When you need path resolution, use resolveCommandPath with the command constants.
- Command execution: Import the command module, register the CLI
index, then run command.parseAsync(['node', 'test', ...args]).
- Assertions: Verify action calls with
expect(actions.method).toHaveBeenCalledWith(...), then check generated manifest entries, reports, log output, and console messages.
Example shape (simplified):
const server = setupServer();
const preconditions = {
canLoadFiles(items: Item[]) {
const dir = resolveCommandPath(directories.items, DEFAULT_SPACE);
vol.fromJSON(Object.fromEntries(items.map(item => [path.join(dir, `${item.slug}.json`), JSON.stringify(item)])));
},
canCreateRemoteItems(items: Item[]) {
const created = items.map(item => ({ ...item, id: getID() }));
server.use(
http.post(`https://mapi.storyblok.com/v1/spaces/${DEFAULT_SPACE}/items`, async ({ request }) => {
const body = await request.json();
const match = created.find(i => i.slug === body.item.slug);
return HttpResponse.json({ item: match });
}),
);
return created;
},
};
describe('command push', () => {
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => {
vi.resetAllMocks();
vi.clearAllMocks();
vol.reset();
server.resetHandlers();
resetReporter();
});
afterAll(() => server.close());
it('should push items and write manifest/report', async () => {
preconditions.canLoadFiles([makeMockItem()]);
preconditions.canCreateRemoteItems([makeMockItem()]);
await command.parseAsync(['node', 'test', 'push', '--space', DEFAULT_SPACE]);
expect(actions.createItem).toHaveBeenCalled();
expect(await parseManifest()).toEqual([expect.objectContaining({ old_id: expect.anything() })]);
expect(getReport()).toEqual(expect.objectContaining({ status: 'SUCCESS' }));
});
});
1---2name: qa-engineer-integration3description: Write integration tests for CLI commands with Vitest4---5
6# QA Engineer for Integration Testing
7
8## Responsibilities
9
10- This skill ensures integration tests cover CLI behavior changes first.
11- Tests remain readable, deterministic, and aligned with how users run commands.
12- End-to-end side effects are emphasized: filesystem, API calls, logging, and reports.
13
14## When to use
15
16Integration tests are mandatory for every new feature or behavior change. Write these tests before the implementation. Use this skill whenever a change affects CLI command behavior, API interactions, filesystem reads and writes, logging, or reporting.
17
18## Best practices
19
20- Use `preconditions` helpers to set up MSW handlers, memfs state, and stubbed dependencies.
21- Require MSW handlers to be registered inside preconditions so API behavior is explicit per test.
22- Drive the CLI via `command.parseAsync([...args])` and assert side effects and outputs.
23- Validate behavior through action spies, manifest files, reports, logs, and console output.
24- Test titles must always start with `should`.
25
26## Patterns to follow
27
28- **Preconditions:** Group setup helpers in a `preconditions` object (e.g., `canLoadFiles`, `canLoadManifest`, `canCreateRemoteResources`, `failsToUpdateRemoteResources`). Each helper should be focused, deterministic, and reusable across tests.
29- **MSW in preconditions:** Define MSW handlers inside those precondition helpers (e.g., `preconditions.canCreateRemoteResources` registers `http.post` and returns created items). This keeps request and response behavior close to the test intent.
30- **Filesystem state:** Use `memfs` and `vol.fromJSON` to create local files and `vol.reset` in `afterEach` to clear state. When you need path resolution, use `resolveCommandPath` with the command constants.
31- **Command execution:** Import the command module, register the CLI `index`, then run `command.parseAsync(['node', 'test', ...args])`.
32- **Assertions:** Verify action calls with `expect(actions.method).toHaveBeenCalledWith(...)`, then check generated manifest entries, reports, log output, and console messages.
33
34Example shape (simplified):
35
36```ts
37const server = setupServer();
38
39const preconditions = {
40 canLoadFiles(items: Item[]) {
41 const dir = resolveCommandPath(directories.items, DEFAULT_SPACE);
42 vol.fromJSON(Object.fromEntries(items.map(item => [path.join(dir, `${item.slug}.json`), JSON.stringify(item)])));
43 },
44 canCreateRemoteItems(items: Item[]) {
45 const created = items.map(item => ({ ...item, id: getID() }));
46 server.use(
47 http.post(`https://mapi.storyblok.com/v1/spaces/${DEFAULT_SPACE}/items`, async ({ request }) => {
48 const body = await request.json();
49 const match = created.find(i => i.slug === body.item.slug);
50 return HttpResponse.json({ item: match });
51 }),
52 );
53 return created;
54 },
55};
56
57describe('command push', () => {
58 beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
59 afterEach(() => {
60 vi.resetAllMocks();
61 vi.clearAllMocks();
62 vol.reset();
63 server.resetHandlers();
64 resetReporter();
65 });
66 afterAll(() => server.close());
67
68 it('should push items and write manifest/report', async () => {
69 preconditions.canLoadFiles([makeMockItem()]);
70 preconditions.canCreateRemoteItems([makeMockItem()]);
71
72 await command.parseAsync(['node', 'test', 'push', '--space', DEFAULT_SPACE]);
73
74 expect(actions.createItem).toHaveBeenCalled();
75 expect(await parseManifest()).toEqual([expect.objectContaining({ old_id: expect.anything() })]);
76 expect(getReport()).toEqual(expect.objectContaining({ status: 'SUCCESS' }));
77 });
78});
79```