@venizia/ignis-docs 0.0.2 → 0.0.4-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +4 -2
- package/wiki/best-practices/api-usage-examples.md +591 -0
- package/wiki/best-practices/architectural-patterns.md +415 -0
- package/wiki/best-practices/architecture-decisions.md +488 -0
- package/wiki/{get-started/best-practices → best-practices}/code-style-standards.md +647 -182
- package/wiki/{get-started/best-practices → best-practices}/common-pitfalls.md +109 -4
- package/wiki/{get-started/best-practices → best-practices}/contribution-workflow.md +34 -7
- package/wiki/best-practices/data-modeling.md +376 -0
- package/wiki/best-practices/deployment-strategies.md +698 -0
- package/wiki/best-practices/index.md +27 -0
- package/wiki/best-practices/performance-optimization.md +196 -0
- package/wiki/best-practices/security-guidelines.md +218 -0
- package/wiki/{get-started/best-practices → best-practices}/troubleshooting-tips.md +97 -1
- package/wiki/changelogs/2025-12-16-initial-architecture.md +1 -1
- package/wiki/changelogs/2025-12-16-model-repo-datasource-refactor.md +1 -1
- package/wiki/changelogs/2025-12-17-refactor.md +1 -1
- package/wiki/changelogs/2025-12-18-performance-optimizations.md +5 -5
- package/wiki/changelogs/2025-12-18-repository-validation-security.md +13 -7
- package/wiki/changelogs/2025-12-26-nested-relations-and-generics.md +86 -0
- package/wiki/changelogs/2025-12-26-transaction-support.md +57 -0
- package/wiki/changelogs/2025-12-29-dynamic-binding-registration.md +104 -0
- package/wiki/changelogs/2025-12-29-snowflake-uid-helper.md +100 -0
- package/wiki/changelogs/2025-12-30-repository-enhancements.md +214 -0
- package/wiki/changelogs/2025-12-31-json-path-filtering-array-operators.md +214 -0
- package/wiki/changelogs/2025-12-31-string-id-custom-generator.md +137 -0
- package/wiki/changelogs/2026-01-02-default-filter-and-repository-mixins.md +418 -0
- package/wiki/changelogs/index.md +8 -1
- package/wiki/changelogs/planned-schema-migrator.md +2 -10
- package/wiki/{get-started/core-concepts → guides/core-concepts/application}/bootstrapping.md +18 -5
- package/wiki/{get-started/core-concepts/application.md → guides/core-concepts/application/index.md} +47 -104
- package/wiki/guides/core-concepts/components-guide.md +509 -0
- package/wiki/guides/core-concepts/components.md +122 -0
- package/wiki/{get-started → guides}/core-concepts/controllers.md +30 -13
- package/wiki/{get-started → guides}/core-concepts/dependency-injection.md +97 -0
- package/wiki/guides/core-concepts/persistent/datasources.md +179 -0
- package/wiki/guides/core-concepts/persistent/index.md +119 -0
- package/wiki/guides/core-concepts/persistent/models.md +241 -0
- package/wiki/guides/core-concepts/persistent/repositories.md +219 -0
- package/wiki/guides/core-concepts/persistent/transactions.md +170 -0
- package/wiki/{get-started → guides}/core-concepts/services.md +26 -3
- package/wiki/{get-started → guides/get-started}/5-minute-quickstart.md +59 -14
- package/wiki/guides/get-started/philosophy.md +682 -0
- package/wiki/guides/get-started/setup.md +157 -0
- package/wiki/guides/index.md +89 -0
- package/wiki/guides/reference/glossary.md +243 -0
- package/wiki/{get-started → guides/reference}/mcp-docs-server.md +0 -10
- package/wiki/{get-started → guides/tutorials}/building-a-crud-api.md +134 -132
- package/wiki/{get-started/quickstart.md → guides/tutorials/complete-installation.md} +107 -71
- package/wiki/guides/tutorials/ecommerce-api.md +1399 -0
- package/wiki/guides/tutorials/realtime-chat.md +1261 -0
- package/wiki/guides/tutorials/testing.md +723 -0
- package/wiki/index.md +176 -37
- package/wiki/references/base/application.md +27 -0
- package/wiki/references/base/bootstrapping.md +30 -26
- package/wiki/references/base/components.md +532 -31
- package/wiki/references/base/controllers.md +136 -38
- package/wiki/references/base/datasources.md +108 -5
- package/wiki/references/base/dependency-injection.md +39 -3
- package/wiki/references/base/filter-system/application-usage.md +224 -0
- package/wiki/references/base/filter-system/array-operators.md +132 -0
- package/wiki/references/base/filter-system/comparison-operators.md +109 -0
- package/wiki/references/base/filter-system/default-filter.md +428 -0
- package/wiki/references/base/filter-system/fields-order-pagination.md +155 -0
- package/wiki/references/base/filter-system/index.md +127 -0
- package/wiki/references/base/filter-system/json-filtering.md +197 -0
- package/wiki/references/base/filter-system/list-operators.md +71 -0
- package/wiki/references/base/filter-system/logical-operators.md +156 -0
- package/wiki/references/base/filter-system/null-operators.md +58 -0
- package/wiki/references/base/filter-system/pattern-matching.md +108 -0
- package/wiki/references/base/filter-system/quick-reference.md +431 -0
- package/wiki/references/base/filter-system/range-operators.md +63 -0
- package/wiki/references/base/filter-system/tips.md +190 -0
- package/wiki/references/base/filter-system/use-cases.md +452 -0
- package/wiki/references/base/index.md +90 -0
- package/wiki/references/base/middlewares.md +602 -0
- package/wiki/references/base/models.md +215 -23
- package/wiki/references/base/providers.md +732 -0
- package/wiki/references/base/repositories/advanced.md +555 -0
- package/wiki/references/base/repositories/index.md +228 -0
- package/wiki/references/base/repositories/mixins.md +331 -0
- package/wiki/references/base/repositories/relations.md +486 -0
- package/wiki/references/base/repositories.md +40 -549
- package/wiki/references/base/services.md +28 -4
- package/wiki/references/components/authentication.md +22 -2
- package/wiki/references/components/health-check.md +12 -0
- package/wiki/references/components/index.md +23 -0
- package/wiki/references/components/mail.md +687 -0
- package/wiki/references/components/request-tracker.md +16 -0
- package/wiki/references/components/socket-io.md +18 -0
- package/wiki/references/components/static-asset.md +14 -26
- package/wiki/references/components/swagger.md +17 -0
- package/wiki/references/configuration/environment-variables.md +427 -0
- package/wiki/references/configuration/index.md +73 -0
- package/wiki/references/helpers/cron.md +14 -0
- package/wiki/references/helpers/crypto.md +15 -0
- package/wiki/references/helpers/env.md +16 -0
- package/wiki/references/helpers/error.md +17 -0
- package/wiki/references/helpers/index.md +15 -0
- package/wiki/references/helpers/inversion.md +24 -4
- package/wiki/references/helpers/logger.md +19 -0
- package/wiki/references/helpers/network.md +11 -0
- package/wiki/references/helpers/queue.md +19 -0
- package/wiki/references/helpers/redis.md +21 -0
- package/wiki/references/helpers/socket-io.md +24 -5
- package/wiki/references/helpers/storage.md +18 -10
- package/wiki/references/helpers/testing.md +18 -0
- package/wiki/references/helpers/types.md +167 -0
- package/wiki/references/helpers/uid.md +167 -0
- package/wiki/references/helpers/worker-thread.md +16 -0
- package/wiki/references/index.md +177 -0
- package/wiki/references/quick-reference.md +634 -0
- package/wiki/references/src-details/boot.md +3 -3
- package/wiki/references/src-details/dev-configs.md +0 -4
- package/wiki/references/src-details/docs.md +2 -2
- package/wiki/references/src-details/index.md +86 -0
- package/wiki/references/src-details/inversion.md +1 -6
- package/wiki/references/src-details/mcp-server.md +3 -15
- package/wiki/references/utilities/index.md +86 -10
- package/wiki/references/utilities/jsx.md +577 -0
- package/wiki/references/utilities/request.md +0 -2
- package/wiki/references/utilities/statuses.md +740 -0
- package/wiki/changelogs/planned-transaction-support.md +0 -216
- package/wiki/get-started/best-practices/api-usage-examples.md +0 -266
- package/wiki/get-started/best-practices/architectural-patterns.md +0 -170
- package/wiki/get-started/best-practices/data-modeling.md +0 -177
- package/wiki/get-started/best-practices/deployment-strategies.md +0 -121
- package/wiki/get-started/best-practices/performance-optimization.md +0 -88
- package/wiki/get-started/best-practices/security-guidelines.md +0 -99
- package/wiki/get-started/core-concepts/components.md +0 -98
- package/wiki/get-started/core-concepts/persistent.md +0 -543
- package/wiki/get-started/index.md +0 -65
- package/wiki/get-started/philosophy.md +0 -296
- package/wiki/get-started/prerequisites.md +0 -113
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
# Testing Your Ignis Application
|
|
2
|
+
|
|
3
|
+
This guide shows you how to write tests for your Ignis application.
|
|
4
|
+
|
|
5
|
+
**⏱️ Time to Complete:** ~30 minutes
|
|
6
|
+
|
|
7
|
+
## Choose Your Test Framework
|
|
8
|
+
|
|
9
|
+
**Ignis works with any test framework.** You can use whichever testing tool you prefer:
|
|
10
|
+
|
|
11
|
+
| Framework | Description |
|
|
12
|
+
|-----------|-------------|
|
|
13
|
+
| **Jest** | Popular, feature-rich testing framework |
|
|
14
|
+
| **Vitest** | Fast, Vite-native testing framework |
|
|
15
|
+
| **Bun Test** | Built-in test runner for Bun |
|
|
16
|
+
| **Playwright** | End-to-end testing for web applications |
|
|
17
|
+
| **node:test** | Node.js native test module |
|
|
18
|
+
| **Mocha** | Flexible testing framework |
|
|
19
|
+
| **Any other** | All test frameworks work with Ignis |
|
|
20
|
+
|
|
21
|
+
Since Ignis is just a TypeScript/JavaScript application framework, you can test it with any tool that supports TypeScript.
|
|
22
|
+
|
|
23
|
+
::: tip IGNIS Testing Extension
|
|
24
|
+
IGNIS provides its own testing utilities built on `node:test`. These utilities (`TestPlan`, `TestCase`, `TestCaseHandler`) offer a structured approach for organizing tests with lifecycle hooks and shared context. This is optional — use it if you prefer this pattern, or use your favorite test framework directly.
|
|
25
|
+
:::
|
|
26
|
+
|
|
27
|
+
## Prerequisites
|
|
28
|
+
|
|
29
|
+
Before starting, ensure you have:
|
|
30
|
+
- A working Ignis application (see [Building a CRUD API](./building-a-crud-api.md))
|
|
31
|
+
- Basic understanding of [Controllers](../core-concepts/controllers.md) and [Repositories](../core-concepts/persistent/)
|
|
32
|
+
|
|
33
|
+
## Quick Examples with Popular Frameworks
|
|
34
|
+
|
|
35
|
+
### Using Vitest
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
// __tests__/todo.test.ts
|
|
39
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
40
|
+
import { app } from '../src/application';
|
|
41
|
+
|
|
42
|
+
describe('Todo API', () => {
|
|
43
|
+
beforeAll(async () => {
|
|
44
|
+
// Setup: start server, seed database, etc.
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterAll(async () => {
|
|
48
|
+
// Cleanup: close connections
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should return list of todos', async () => {
|
|
52
|
+
const response = await app.request('/api/todos', { method: 'GET' });
|
|
53
|
+
|
|
54
|
+
expect(response.status).toBe(200);
|
|
55
|
+
const body = await response.json();
|
|
56
|
+
expect(Array.isArray(body)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should create a new todo', async () => {
|
|
60
|
+
const response = await app.request('/api/todos', {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
body: JSON.stringify({ title: 'Test Todo' }),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(response.status).toBe(201);
|
|
67
|
+
const body = await response.json();
|
|
68
|
+
expect(body.title).toBe('Test Todo');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Using Jest
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// __tests__/todo.test.ts
|
|
77
|
+
import { app } from '../src/application';
|
|
78
|
+
|
|
79
|
+
describe('Todo API', () => {
|
|
80
|
+
it('should return list of todos', async () => {
|
|
81
|
+
const response = await app.request('/api/todos', { method: 'GET' });
|
|
82
|
+
|
|
83
|
+
expect(response.status).toBe(200);
|
|
84
|
+
const body = await response.json();
|
|
85
|
+
expect(Array.isArray(body)).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Using Bun Test
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// __tests__/todo.test.ts
|
|
94
|
+
import { describe, it, expect } from 'bun:test';
|
|
95
|
+
import { app } from '../src/application';
|
|
96
|
+
|
|
97
|
+
describe('Todo API', () => {
|
|
98
|
+
it('should return list of todos', async () => {
|
|
99
|
+
const response = await app.request('/api/todos', { method: 'GET' });
|
|
100
|
+
|
|
101
|
+
expect(response.status).toBe(200);
|
|
102
|
+
const body = await response.json();
|
|
103
|
+
expect(Array.isArray(body)).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Using Playwright (E2E)
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// e2e/todo.spec.ts
|
|
112
|
+
import { test, expect } from '@playwright/test';
|
|
113
|
+
|
|
114
|
+
test.describe('Todo Application', () => {
|
|
115
|
+
test('should display todo list', async ({ request }) => {
|
|
116
|
+
const response = await request.get('http://localhost:3000/api/todos');
|
|
117
|
+
|
|
118
|
+
expect(response.ok()).toBeTruthy();
|
|
119
|
+
const todos = await response.json();
|
|
120
|
+
expect(Array.isArray(todos)).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Using IGNIS Testing Extension
|
|
126
|
+
|
|
127
|
+
IGNIS provides its own testing utilities built on `node:test` for a more structured approach.
|
|
128
|
+
|
|
129
|
+
### 1. Create Your First Test
|
|
130
|
+
|
|
131
|
+
Create a test file in your project:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// __tests__/hello.test.ts
|
|
135
|
+
import {
|
|
136
|
+
TestPlan,
|
|
137
|
+
TestDescribe,
|
|
138
|
+
TestCase,
|
|
139
|
+
TestCaseHandler,
|
|
140
|
+
TestCaseDecisions,
|
|
141
|
+
} from '@venizia/ignis-helpers';
|
|
142
|
+
|
|
143
|
+
// Step 1: Define a Test Handler
|
|
144
|
+
class HelloHandler extends TestCaseHandler {
|
|
145
|
+
async execute() {
|
|
146
|
+
// The action to test
|
|
147
|
+
const message = 'Hello, Ignis!';
|
|
148
|
+
return { message };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
getValidator() {
|
|
152
|
+
// Validate the result
|
|
153
|
+
return (result: { message: string }) => {
|
|
154
|
+
if (result.message === 'Hello, Ignis!') {
|
|
155
|
+
return TestCaseDecisions.SUCCESS;
|
|
156
|
+
}
|
|
157
|
+
return TestCaseDecisions.FAIL;
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Step 2: Create a Test Plan
|
|
163
|
+
const helloTestPlan = TestPlan.newInstance({
|
|
164
|
+
scope: 'Hello World Tests',
|
|
165
|
+
testCases: [
|
|
166
|
+
TestCase.withOptions({
|
|
167
|
+
code: 'HELLO-001',
|
|
168
|
+
description: 'Should return greeting message',
|
|
169
|
+
expectation: 'Message equals "Hello, Ignis!"',
|
|
170
|
+
handler: new HelloHandler({ context: {} as any }),
|
|
171
|
+
}),
|
|
172
|
+
],
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Step 3: Run the Test
|
|
176
|
+
TestDescribe.withTestPlan({ testPlan: helloTestPlan }).run();
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 2. Run Tests
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# Using Bun
|
|
183
|
+
bun test
|
|
184
|
+
|
|
185
|
+
# Using Node.js
|
|
186
|
+
node --test __tests__/*.test.ts
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Core Concepts
|
|
190
|
+
|
|
191
|
+
### Test Framework Components
|
|
192
|
+
|
|
193
|
+
| Component | Purpose |
|
|
194
|
+
|-----------|---------|
|
|
195
|
+
| **TestPlan** | Organizes a test suite with lifecycle hooks and shared context |
|
|
196
|
+
| **TestCase** | A single test unit with code, description, and handler |
|
|
197
|
+
| **TestCaseHandler** | Encapsulates test execution and validation logic |
|
|
198
|
+
| **TestDescribe** | Runs test plans using `node:test` |
|
|
199
|
+
|
|
200
|
+
### Test Case Decisions
|
|
201
|
+
|
|
202
|
+
| Decision | Meaning |
|
|
203
|
+
|----------|---------|
|
|
204
|
+
| `TestCaseDecisions.SUCCESS` | Test passed |
|
|
205
|
+
| `TestCaseDecisions.FAIL` | Test failed |
|
|
206
|
+
| `TestCaseDecisions.UNKNOWN` | Result undetermined |
|
|
207
|
+
|
|
208
|
+
### Lifecycle Hooks
|
|
209
|
+
|
|
210
|
+
| Hook | When | Use Case |
|
|
211
|
+
|------|------|----------|
|
|
212
|
+
| `before` | Before all tests | Start server, seed database |
|
|
213
|
+
| `after` | After all tests | Close connections, cleanup |
|
|
214
|
+
| `beforeEach` | Before each test | Reset state |
|
|
215
|
+
| `afterEach` | After each test | Clear test data |
|
|
216
|
+
|
|
217
|
+
## Testing Controllers
|
|
218
|
+
|
|
219
|
+
Here's how to test an HTTP controller:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// __tests__/todo.controller.test.ts
|
|
223
|
+
import {
|
|
224
|
+
TestPlan,
|
|
225
|
+
TestDescribe,
|
|
226
|
+
TestCase,
|
|
227
|
+
TestCaseHandler,
|
|
228
|
+
TestCaseDecisions,
|
|
229
|
+
} from '@venizia/ignis-helpers';
|
|
230
|
+
import { app } from '../src/application'; // Your Ignis app
|
|
231
|
+
|
|
232
|
+
// Handler for testing GET /todos
|
|
233
|
+
class GetTodosHandler extends TestCaseHandler {
|
|
234
|
+
async execute() {
|
|
235
|
+
// Make HTTP request to your app
|
|
236
|
+
const response = await app.request('/api/todos', {
|
|
237
|
+
method: 'GET',
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
status: response.status,
|
|
242
|
+
body: await response.json(),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
getValidator() {
|
|
247
|
+
return (result: { status: number; body: any }) => {
|
|
248
|
+
// Validate status code
|
|
249
|
+
if (result.status !== 200) {
|
|
250
|
+
return TestCaseDecisions.FAIL;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Validate response is an array
|
|
254
|
+
if (!Array.isArray(result.body)) {
|
|
255
|
+
return TestCaseDecisions.FAIL;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return TestCaseDecisions.SUCCESS;
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Handler for testing POST /todos
|
|
264
|
+
class CreateTodoHandler extends TestCaseHandler {
|
|
265
|
+
async execute() {
|
|
266
|
+
const response = await app.request('/api/todos', {
|
|
267
|
+
method: 'POST',
|
|
268
|
+
headers: { 'Content-Type': 'application/json' },
|
|
269
|
+
body: JSON.stringify({
|
|
270
|
+
title: 'Test Todo',
|
|
271
|
+
description: 'Created by test',
|
|
272
|
+
}),
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
status: response.status,
|
|
277
|
+
body: await response.json(),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
getValidator() {
|
|
282
|
+
return (result: { status: number; body: any }) => {
|
|
283
|
+
if (result.status !== 201) {
|
|
284
|
+
return TestCaseDecisions.FAIL;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (result.body.title !== 'Test Todo') {
|
|
288
|
+
return TestCaseDecisions.FAIL;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return TestCaseDecisions.SUCCESS;
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Create test plan
|
|
297
|
+
const todoControllerTests = TestPlan.newInstance({
|
|
298
|
+
scope: 'Todo Controller',
|
|
299
|
+
hooks: {
|
|
300
|
+
before: async () => {
|
|
301
|
+
console.log('Setting up Todo controller tests...');
|
|
302
|
+
// Start server or setup test database
|
|
303
|
+
},
|
|
304
|
+
after: async () => {
|
|
305
|
+
console.log('Cleaning up...');
|
|
306
|
+
// Cleanup resources
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
testCases: [
|
|
310
|
+
TestCase.withOptions({
|
|
311
|
+
code: 'TODO-001',
|
|
312
|
+
description: 'GET /todos returns list of todos',
|
|
313
|
+
expectation: 'Status 200 with array response',
|
|
314
|
+
handler: new GetTodosHandler({ context: {} as any }),
|
|
315
|
+
}),
|
|
316
|
+
TestCase.withOptions({
|
|
317
|
+
code: 'TODO-002',
|
|
318
|
+
description: 'POST /todos creates a new todo',
|
|
319
|
+
expectation: 'Status 201 with created todo',
|
|
320
|
+
handler: new CreateTodoHandler({ context: {} as any }),
|
|
321
|
+
}),
|
|
322
|
+
],
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
TestDescribe.withTestPlan({ testPlan: todoControllerTests }).run();
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Testing with Shared Context
|
|
329
|
+
|
|
330
|
+
Use the test plan's context to share data between tests (like authentication tokens):
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// __tests__/auth.test.ts
|
|
334
|
+
import {
|
|
335
|
+
TestPlan,
|
|
336
|
+
TestDescribe,
|
|
337
|
+
TestCase,
|
|
338
|
+
TestCaseHandler,
|
|
339
|
+
TestCaseDecisions,
|
|
340
|
+
ITestContext,
|
|
341
|
+
} from '@venizia/ignis-helpers';
|
|
342
|
+
|
|
343
|
+
// Define context shape
|
|
344
|
+
interface AuthContext {
|
|
345
|
+
token: string;
|
|
346
|
+
userId: string;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Handler that uses shared context
|
|
350
|
+
class SecureEndpointHandler extends TestCaseHandler<AuthContext> {
|
|
351
|
+
async execute() {
|
|
352
|
+
// Get token from context (set in before hook)
|
|
353
|
+
const token = this.context.getSync<string>({ key: 'token' });
|
|
354
|
+
|
|
355
|
+
const response = await app.request('/api/profile', {
|
|
356
|
+
method: 'GET',
|
|
357
|
+
headers: {
|
|
358
|
+
Authorization: `Bearer ${token}`,
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
status: response.status,
|
|
364
|
+
body: await response.json(),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
getValidator() {
|
|
369
|
+
return (result: { status: number; body: any }) => {
|
|
370
|
+
if (result.status === 200 && result.body.id) {
|
|
371
|
+
return TestCaseDecisions.SUCCESS;
|
|
372
|
+
}
|
|
373
|
+
return TestCaseDecisions.FAIL;
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const authTests = TestPlan.newInstance<AuthContext>({
|
|
379
|
+
scope: 'Authentication Tests',
|
|
380
|
+
hooks: {
|
|
381
|
+
before: async (testPlan: ITestContext<AuthContext>) => {
|
|
382
|
+
// Login and store token in context
|
|
383
|
+
const loginResponse = await app.request('/api/auth/login', {
|
|
384
|
+
method: 'POST',
|
|
385
|
+
headers: { 'Content-Type': 'application/json' },
|
|
386
|
+
body: JSON.stringify({
|
|
387
|
+
email: 'test@example.com',
|
|
388
|
+
password: 'password123',
|
|
389
|
+
}),
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const { token, userId } = await loginResponse.json();
|
|
393
|
+
|
|
394
|
+
// Bind to context for use in test cases
|
|
395
|
+
testPlan.bind({ key: 'token', value: token });
|
|
396
|
+
testPlan.bind({ key: 'userId', value: userId });
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
testCases: [
|
|
400
|
+
TestCase.withOptions({
|
|
401
|
+
code: 'AUTH-001',
|
|
402
|
+
description: 'Authenticated user can access profile',
|
|
403
|
+
expectation: 'Returns user profile with status 200',
|
|
404
|
+
handler: new SecureEndpointHandler({ context: {} as any }),
|
|
405
|
+
}),
|
|
406
|
+
],
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
TestDescribe.withTestPlan({ testPlan: authTests }).run();
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## Testing Repositories
|
|
413
|
+
|
|
414
|
+
Test your data access layer directly:
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
// __tests__/todo.repository.test.ts
|
|
418
|
+
import {
|
|
419
|
+
TestPlan,
|
|
420
|
+
TestDescribe,
|
|
421
|
+
TestCase,
|
|
422
|
+
TestCaseHandler,
|
|
423
|
+
TestCaseDecisions,
|
|
424
|
+
} from '@venizia/ignis-helpers';
|
|
425
|
+
import { TodoRepository } from '../src/repositories/todo.repository';
|
|
426
|
+
import { Container } from '@venizia/ignis-inversion';
|
|
427
|
+
|
|
428
|
+
// Setup container for DI
|
|
429
|
+
const container = new Container();
|
|
430
|
+
|
|
431
|
+
class CreateTodoRepoHandler extends TestCaseHandler {
|
|
432
|
+
async execute() {
|
|
433
|
+
const todoRepo = container.get<TodoRepository>('repositories.TodoRepository');
|
|
434
|
+
|
|
435
|
+
const created = await todoRepo.create({
|
|
436
|
+
title: 'Repository Test',
|
|
437
|
+
description: 'Testing repository layer',
|
|
438
|
+
isCompleted: false,
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
return { todo: created };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
getValidator() {
|
|
445
|
+
return (result: { todo: any }) => {
|
|
446
|
+
if (result.todo && result.todo.id && result.todo.title === 'Repository Test') {
|
|
447
|
+
return TestCaseDecisions.SUCCESS;
|
|
448
|
+
}
|
|
449
|
+
return TestCaseDecisions.FAIL;
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
class FindTodoRepoHandler extends TestCaseHandler {
|
|
455
|
+
async execute() {
|
|
456
|
+
const todoRepo = container.get<TodoRepository>('repositories.TodoRepository');
|
|
457
|
+
|
|
458
|
+
const todos = await todoRepo.find({
|
|
459
|
+
where: { isCompleted: false },
|
|
460
|
+
limit: 10,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
return { todos, count: todos.length };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
getValidator() {
|
|
467
|
+
return (result: { todos: any[]; count: number }) => {
|
|
468
|
+
if (Array.isArray(result.todos) && result.count >= 0) {
|
|
469
|
+
return TestCaseDecisions.SUCCESS;
|
|
470
|
+
}
|
|
471
|
+
return TestCaseDecisions.FAIL;
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const repoTests = TestPlan.newInstance({
|
|
477
|
+
scope: 'Todo Repository',
|
|
478
|
+
hooks: {
|
|
479
|
+
before: async () => {
|
|
480
|
+
// Setup DI container and database connection
|
|
481
|
+
container.bind('repositories.TodoRepository').toClass(TodoRepository);
|
|
482
|
+
},
|
|
483
|
+
after: async () => {
|
|
484
|
+
// Cleanup test data
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
testCases: [
|
|
488
|
+
TestCase.withOptions({
|
|
489
|
+
code: 'REPO-001',
|
|
490
|
+
description: 'Can create a todo via repository',
|
|
491
|
+
expectation: 'Returns created todo with ID',
|
|
492
|
+
handler: new CreateTodoRepoHandler({ context: {} as any }),
|
|
493
|
+
}),
|
|
494
|
+
TestCase.withOptions({
|
|
495
|
+
code: 'REPO-002',
|
|
496
|
+
description: 'Can find todos with filters',
|
|
497
|
+
expectation: 'Returns array of matching todos',
|
|
498
|
+
handler: new FindTodoRepoHandler({ context: {} as any }),
|
|
499
|
+
}),
|
|
500
|
+
],
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
TestDescribe.withTestPlan({ testPlan: repoTests }).run();
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
## Testing Services
|
|
507
|
+
|
|
508
|
+
Test business logic in isolation:
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
// __tests__/todo.service.test.ts
|
|
512
|
+
import {
|
|
513
|
+
TestPlan,
|
|
514
|
+
TestDescribe,
|
|
515
|
+
TestCase,
|
|
516
|
+
TestCaseHandler,
|
|
517
|
+
TestCaseDecisions,
|
|
518
|
+
} from '@venizia/ignis-helpers';
|
|
519
|
+
import { TodoService } from '../src/services/todo.service';
|
|
520
|
+
|
|
521
|
+
class CompleteTodoHandler extends TestCaseHandler {
|
|
522
|
+
async execute() {
|
|
523
|
+
const todoService = new TodoService();
|
|
524
|
+
|
|
525
|
+
// Create a todo first
|
|
526
|
+
const todo = await todoService.create({
|
|
527
|
+
title: 'Test completion',
|
|
528
|
+
isCompleted: false,
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// Mark as complete
|
|
532
|
+
const completed = await todoService.markAsComplete(todo.id);
|
|
533
|
+
|
|
534
|
+
return { original: todo, completed };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
getValidator() {
|
|
538
|
+
return (result: { original: any; completed: any }) => {
|
|
539
|
+
// Original should be incomplete
|
|
540
|
+
if (result.original.isCompleted !== false) {
|
|
541
|
+
return TestCaseDecisions.FAIL;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Completed should be complete
|
|
545
|
+
if (result.completed.isCompleted !== true) {
|
|
546
|
+
return TestCaseDecisions.FAIL;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return TestCaseDecisions.SUCCESS;
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const serviceTests = TestPlan.newInstance({
|
|
555
|
+
scope: 'Todo Service',
|
|
556
|
+
testCases: [
|
|
557
|
+
TestCase.withOptions({
|
|
558
|
+
code: 'SVC-001',
|
|
559
|
+
description: 'Can mark todo as complete',
|
|
560
|
+
expectation: 'Todo isCompleted changes from false to true',
|
|
561
|
+
handler: new CompleteTodoHandler({ context: {} as any }),
|
|
562
|
+
}),
|
|
563
|
+
],
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
TestDescribe.withTestPlan({ testPlan: serviceTests }).run();
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
## Project Structure
|
|
570
|
+
|
|
571
|
+
Organize your tests alongside your source code:
|
|
572
|
+
|
|
573
|
+
```
|
|
574
|
+
my-ignis-app/
|
|
575
|
+
├── src/
|
|
576
|
+
│ ├── controllers/
|
|
577
|
+
│ ├── services/
|
|
578
|
+
│ └── repositories/
|
|
579
|
+
├── __tests__/
|
|
580
|
+
│ ├── controllers/
|
|
581
|
+
│ │ └── todo.controller.test.ts
|
|
582
|
+
│ ├── services/
|
|
583
|
+
│ │ └── todo.service.test.ts
|
|
584
|
+
│ ├── repositories/
|
|
585
|
+
│ │ └── todo.repository.test.ts
|
|
586
|
+
│ └── integration/
|
|
587
|
+
│ └── auth-flow.test.ts
|
|
588
|
+
└── package.json
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Package.json Scripts
|
|
592
|
+
|
|
593
|
+
Choose scripts based on your preferred test framework:
|
|
594
|
+
|
|
595
|
+
**Bun Test:**
|
|
596
|
+
```json
|
|
597
|
+
{
|
|
598
|
+
"scripts": {
|
|
599
|
+
"test": "bun test",
|
|
600
|
+
"test:watch": "bun test --watch",
|
|
601
|
+
"test:coverage": "bun test --coverage"
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
**Vitest:**
|
|
607
|
+
```json
|
|
608
|
+
{
|
|
609
|
+
"scripts": {
|
|
610
|
+
"test": "vitest run",
|
|
611
|
+
"test:watch": "vitest",
|
|
612
|
+
"test:coverage": "vitest run --coverage"
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
**Jest:**
|
|
618
|
+
```json
|
|
619
|
+
{
|
|
620
|
+
"scripts": {
|
|
621
|
+
"test": "jest",
|
|
622
|
+
"test:watch": "jest --watch",
|
|
623
|
+
"test:coverage": "jest --coverage"
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
**Playwright (E2E):**
|
|
629
|
+
```json
|
|
630
|
+
{
|
|
631
|
+
"scripts": {
|
|
632
|
+
"test:e2e": "playwright test",
|
|
633
|
+
"test:e2e:ui": "playwright test --ui"
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
## Best Practices
|
|
639
|
+
|
|
640
|
+
### 1. Use Descriptive Test Codes
|
|
641
|
+
|
|
642
|
+
```typescript
|
|
643
|
+
TestCase.withOptions({
|
|
644
|
+
code: 'AUTH-LOGIN-001', // Feature-Action-Number
|
|
645
|
+
description: 'User can login with valid credentials',
|
|
646
|
+
expectation: 'Returns JWT token and user ID',
|
|
647
|
+
// ...
|
|
648
|
+
});
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### 2. Isolate Test Data
|
|
652
|
+
|
|
653
|
+
```typescript
|
|
654
|
+
hooks: {
|
|
655
|
+
beforeEach: async (testPlan) => {
|
|
656
|
+
// Create fresh test data for each test
|
|
657
|
+
const testTodo = await createTestTodo();
|
|
658
|
+
testPlan.bind({ key: 'testTodoId', value: testTodo.id });
|
|
659
|
+
},
|
|
660
|
+
afterEach: async (testPlan) => {
|
|
661
|
+
// Clean up after each test
|
|
662
|
+
const todoId = testPlan.getSync({ key: 'testTodoId' });
|
|
663
|
+
await deleteTestTodo(todoId);
|
|
664
|
+
},
|
|
665
|
+
}
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
### 3. Test Edge Cases
|
|
669
|
+
|
|
670
|
+
```typescript
|
|
671
|
+
// Test empty results
|
|
672
|
+
TestCase.withOptions({
|
|
673
|
+
code: 'TODO-FIND-002',
|
|
674
|
+
description: 'Returns empty array when no todos match filter',
|
|
675
|
+
expectation: 'Empty array with status 200',
|
|
676
|
+
handler: new FindNonExistentHandler({ context: {} as any }),
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
// Test validation errors
|
|
680
|
+
TestCase.withOptions({
|
|
681
|
+
code: 'TODO-CREATE-003',
|
|
682
|
+
description: 'Rejects todo without title',
|
|
683
|
+
expectation: 'Status 400 with validation error',
|
|
684
|
+
handler: new CreateInvalidTodoHandler({ context: {} as any }),
|
|
685
|
+
});
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
### 4. Keep Handlers Focused
|
|
689
|
+
|
|
690
|
+
Each handler should test one specific behavior:
|
|
691
|
+
|
|
692
|
+
```typescript
|
|
693
|
+
// Good: Focused on one behavior
|
|
694
|
+
class CreateTodoHandler extends TestCaseHandler {
|
|
695
|
+
async execute() { /* only create logic */ }
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Avoid: Multiple behaviors in one handler
|
|
699
|
+
class CreateAndUpdateAndDeleteHandler extends TestCaseHandler {
|
|
700
|
+
async execute() { /* too many things */ }
|
|
701
|
+
}
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
## Next Steps
|
|
705
|
+
|
|
706
|
+
- [Testing Reference](../../references/helpers/testing.md) - Complete API documentation
|
|
707
|
+
- [Best Practices](../../best-practices/code-style-standards.md) - Code quality standards
|
|
708
|
+
- [Troubleshooting](../../best-practices/troubleshooting-tips.md) - Common issues
|
|
709
|
+
|
|
710
|
+
## Summary
|
|
711
|
+
|
|
712
|
+
| What to Test | How |
|
|
713
|
+
|--------------|-----|
|
|
714
|
+
| **Controllers** | Use `app.request()` to make HTTP calls |
|
|
715
|
+
| **Services** | Instantiate and call methods directly |
|
|
716
|
+
| **Repositories** | Use DI container, test with real/mock DB |
|
|
717
|
+
| **Integration** | Chain multiple operations with shared context |
|
|
718
|
+
| **E2E** | Use Playwright or similar for full flow testing |
|
|
719
|
+
|
|
720
|
+
**Key Takeaways:**
|
|
721
|
+
- Use any test framework you prefer (Jest, Vitest, Bun Test, Playwright, etc.)
|
|
722
|
+
- IGNIS provides optional testing utilities (`TestPlan`, `TestCase`, `TestCaseHandler`) built on `node:test`
|
|
723
|
+
- All frameworks work seamlessly with Ignis applications
|