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