1
1
# Hyperjump - JSON Schema Test Coverage
2
2
3
- This package provides tools for testing JSON Schemas and providing test coverage
4
- for schema files in JSON or YAML in your code base. Integration is provided for
5
- Vitest, but the component for collecting the coverage data is also exposed if
6
- you want to do some other integration.
3
+ This package provides test coverage support for JSON Schemas files in JSON and
4
+ YAML in your project. Integration is provided for Vitest, but the low level
5
+ components for collecting the coverage data is also exposed if you want to do
6
+ some other integration. It uses the [ istanbul] coverage format, so you can
7
+ generate any reports that support [ istanbul] .
7
8
8
- Validation is done by ` @hyperjump/json-schema ` , so you can use any version of
9
- JSON Schema supported by that package .
9
+ Validation is done by [ @hyperjump/json-schema ] , so you can use any version of
10
+ JSON Schema.
10
11
11
12
```
12
13
-------------|---------|----------|---------|---------|-------------------
@@ -19,10 +20,14 @@ All files | 81.81 | 66.66 | 80 | 88.88 |
19
20
20
21
![ HTML coverage example] ( coverage.png )
21
22
23
+ Istanbul reporters report in terms of Statements, Branches, and Functions, which
24
+ aren't terms that makes sense for JSON Schema. I've mapped those concepts to
25
+ what makes sense for schemas.
26
+
22
27
** Legend**
23
28
- ** Statements** = Keywords and subschemas
24
29
- ** Branches** = true/false branches for each keyword (except for keywords that
25
- don't branch such as annotation-only keywords)
30
+ don't branch such as annotation-only keywords like ` title ` and ` description ` )
26
31
- ** Functions** = Subschemas
27
32
28
33
## Limitations
@@ -31,9 +36,11 @@ The following are a list of known limitations. Some might be able to be
31
36
addressed at some point, while others might not.
32
37
33
38
- Keywords can pass/fail for multiple reasons, but not all branches are captured
34
- - Example: ` type: ["object", "boolean"] ` . If you test with an object and a
35
- number, you've covered pass/fail, but haven't tested that a boolean should
36
- pass.
39
+ - Example: ` type: ["object", "boolean"] ` . If you test with an object and then
40
+ test with a number, you've covered the pass and fail branches, but haven't
41
+ tested that a boolean should also pass.
42
+ - There's currently no way to produce a report that uses JSON Schema-friendly
43
+ terms rather than "statements" and "functions".
37
44
38
45
## Vitest
39
46
@@ -47,10 +54,10 @@ By default, it will track coverage for any file with a `*.schema.json`,
47
54
48
55
** Options**
49
56
50
- - ** include** -- An array of glob paths of schemas you want to track coverage
51
- for. For example, if you keep your schemas in a folder called ` schemas ` and
52
- they just have plain extensions (` *.json ` ) instead of schema extensions
53
- ` *.schema.json ` , you could use ` ["./ schemas/**/*.json"] ` .
57
+ - ** include** -- An array of paths of schemas you want to track coverage for.
58
+ For example, if you keep your schemas in a folder called ` schemas ` and they
59
+ just have plain extensions (` *.json ` ) instead of schema extensions
60
+ ` *.schema.json ` , you could use ` ["schemas/**/*.json"] ` .
54
61
55
62
` vitest-schema.config.js `
56
63
``` TypeScript
@@ -59,10 +66,11 @@ import type { JsonSchemaCoverageProviderOptions } from "@hyperjump/json-schema-c
59
66
60
67
export default defineConfig ({
61
68
test: {
69
+ include: [" schema-tests/" ],
62
70
coverage: {
63
71
provider: " custom" ,
64
72
customProviderModule: " @hyperjump/json-schema-coverage/vitest-coverage-provider" ,
65
- include: [" ./ schemas/**/*.json" ] // Optional
73
+ include: [" schemas/**/*.json" ] // Optional
66
74
} as JsonSchemaCoverageProviderOptions
67
75
}
68
76
});
@@ -73,47 +81,51 @@ vitest run --config=vitest-schema.config.js --coverage
73
81
```
74
82
75
83
When you use the provided custom matcher ` matchJsonSchema ` /` toMatchJsonSchema ` ,
76
- if vitest has coverage is enabled, it will collect coverage data from those
77
- tests.
84
+ if vitest has coverage enabled, it will collect coverage data from those tests.
78
85
79
86
``` JavaScript
80
87
import { describe , expect , test } from " vitest" ;
81
88
import " @hyperjump/json-schema-coverage/vitest-matchers" ;
82
89
83
90
describe (" Worksheet" , () => {
84
- test (" matches with uri" , async () => {
85
- await expect ({ foo: 42 }).toMatchJsonSchema (" ./schema.json" );
91
+ test (" matches with chai-style matcher" , async () => {
92
+ // 🚨 DON'T FORGET THE `await` 🚨
93
+ await expect ({ foo: 42 }).to .matchJsonSchema (" ./schema.json" );
86
94
});
87
95
88
- test (" doesn't match with uri" , async () => {
96
+ test (" doesn't match with jest-style matcher" , async () => {
97
+ // 🚨 DON'T FORGET THE `await` 🚨
89
98
await expect ({ foo: null }).not .toMatchJsonSchema (" ./schema.json" );
90
99
});
91
100
});
92
101
```
93
102
94
- Instead of referring to the file path, you can register the schema and use its
95
- ` $id ` . Another reason to register a schema is if your schema references another
96
- schema.
103
+ Instead of referring to the file path, you can use ` registerSchema ` to register
104
+ the schema and then use its ` $id ` . Another reason to register a schema is if
105
+ your schema references external schema and you need to register those schemas
106
+ for the validation to work.
97
107
98
108
``` JavaScript
99
109
import { describe , expect , test } from " vitest" ;
100
110
import { registerSchema , unregisterSchema } from " @hyperjump/json-schema-coverage/vitest-matchers" ;
101
111
102
112
describe (" Worksheet" , () => {
103
- beforeEach (() => {
104
- registerSchema (" ./schema.json" );
113
+ beforeEach (async () => {
114
+ await registerSchema (" ./schema.json" );
105
115
});
106
116
107
- afterEach (() => {
108
- unregisterSchema (" ./schema.json" );
117
+ afterEach (async () => {
118
+ await unregisterSchema (" ./schema.json" );
109
119
});
110
120
111
- test (" matches with uri" , async () => {
121
+ test (" matches with jest-style matcher" , async () => {
122
+ // 🚨 DON'T FORGET THE `await` 🚨
112
123
await expect ({ foo: 42 }).toMatchJsonSchema (" https://example.com/main" );
113
124
});
114
125
115
- test (" doesn't match with uri" , async () => {
116
- await expect ({ foo: null }).not .toMatchJsonSchema (" https://example.com/main" );
126
+ test (" doesn't match with chai-style matcher" , async () => {
127
+ // 🚨 DON'T FORGET THE `await` 🚨
128
+ await expect ({ foo: null }).not .to .matchJsonSchema (" https://example.com/main" );
117
129
});
118
130
});
119
131
```
@@ -127,15 +139,172 @@ import "@hyperjump/json-schema-coverage/vitest-matchers";
127
139
128
140
describe (" Worksheet" , () => {
129
141
test (" matches with schema" , async () => {
142
+ // 🚨 DON'T FORGET THE `await` 🚨
130
143
await expect (" foo" ).to .matchJsonSchema ({ type: " string" });
131
144
});
132
145
133
146
test (" doesn't match with schema" , async () => {
147
+ // 🚨 DON'T FORGET THE `await` 🚨
134
148
await expect (42 ).to .not .matchJsonSchema ({ type: " string" });
135
149
});
136
150
});
137
151
```
138
152
139
- ## TestCoverageEvaluationPlugin
153
+ ## Vitest API
154
+
155
+ These are the functions available when working with the vitest integration.
156
+
157
+ ``` JavaScript
158
+ import { ... } from " @hyperjump/json-schema-coverage/vitest-matchers"
159
+ ```
160
+
161
+ - ** matchJsonSchema** : (uriOrSchema: string | SchemaObject | boolean) => Promise\< void>
162
+
163
+ A vitest matcher that can be used to validate a JSON-compatible value. It
164
+ can take a relative or full URI for a schema in your codebase. Use relative
165
+ URIs to reference a file and full URIs to reference the ` $id ` of a schema
166
+ you registered using the ` registerSchema ` function.
167
+
168
+ You can use this matcher with an inline schema as well, but you will only
169
+ get coverage for schemas that are in files.
170
+ - ** toMatchJsonSchema** : (uriOrSchema: string | SchemaObject | boolean) => Promise\< void>
171
+
172
+ An alias for ` matchJsonSchema ` for those who prefer Jest-style matchers.
173
+ - ** registerSchema** : (path: string) => Promise<void >
174
+
175
+ Register a schema in your code base by it's path.
176
+
177
+ _ ** NOTE** : This is ** not** the same as the function from
178
+ [ @hyperjump/json-schema ] that takes a schema._
179
+ - ** unregisterSchema** : (path: string) => Promise<void >
180
+
181
+ Remove a registered schema in your code base by it's path.
182
+
183
+ _ ** NOTE** : This is ** not** the same as the function from
184
+ [ @hyperjump/json-schema ] that takes the schema's ` $id ` ._
185
+ - ** defineVocabulary** : (vocabularyUri: string, keywords: Record<string, string>) => void
186
+
187
+ If your schemas use a custom vocabulary, you can register your vocabulary
188
+ with this function.
189
+
190
+ _ ** NOTE** : This is the same as the function from [ @hyperjump/json-schema ] _
191
+ - ** addKeyword** : (keywordHandler: Keyword) => void
192
+
193
+ If your schemas use a custom vocabulary, you can register your custom
194
+ keywords with this function.
195
+
196
+ _ ** NOTE** : This is the same as the function from [ @hyperjump/json-schema ] _
197
+ - ** loadDialect** : (dialectUri: string, dialect: Record<string, boolean>, allowUnknowKeywords?: boolean) => void
198
+
199
+ If your schemas use a custom dialect, you can register it with this
200
+ function.
201
+
202
+ _ ** NOTE** : This is the same as the function from [ @hyperjump/json-schema ] _
203
+
204
+ ## Low-Level API
205
+
206
+ These are used internally. They can be used to get coverage without using the
207
+ Vitest integration.
208
+
209
+ ``` JavaScript
210
+ import { ... } from " @hyperjump/json-schema-coverage"
211
+ ```
212
+
213
+ ### CoverageMapService
214
+
215
+ The ` CoverageMapService ` creates [ istanbul] coverage maps for your schemas and
216
+ stores them for use by the ` TestCoverageEvaluationPlugin ` . A coverage map stores
217
+ the file positions of all the keywords, schemas, and branches in a schema.
218
+
219
+ - ** CoverageMapService.addFromFile** -- (schemaPath: string): Promise\< string>
220
+
221
+ This method takes a file path to a schema, generates a coverage map for it,
222
+ and stores it for later use. It returns the identifier for the schema
223
+ (usually the value of ` $id ` ).
224
+ - ** CoverageMapService.addCoverageMap** -- (coverageMap: CoverageMapData): void
225
+
226
+ If you have a coverage map you created yourself or got from some other
227
+ source, you can add it using this method. You probably don't need this. Use
228
+ ` addFromFile ` to create and store the coverage map for you.
229
+ - ** CoverageMapService.getSchemaPath** -- (schemaUri: string): string
230
+
231
+ Get the file path for the schema that is identified by the given URI.
232
+ - ** CoverageMapService.getCoverageMap** -- (schemaUri: string): CoverageMapData
233
+
234
+ Retrieve a coverage map that was previously added through ` addFromFile ` or
235
+ ` addCoverageMap ` .
236
+
237
+ ### TestCoverageEvaluationPlugin
238
+
239
+ The ` TestCoverageEvaluationPlugin ` hooks into the evaluation process of the
240
+ [ @hyperjump/json-schema ] validator and uses the ` CoverageMapService ` to record
241
+ when a keyword or schema is visited. Once the evaluation process is completed,
242
+ it contains an [ istanbul] coverage file. These files can then be used to
243
+ generate any report that supports [ istanbul] . See the following example for an
244
+ example of how to use the evaluation plugin.
245
+
246
+ ### Nyc Example
247
+
248
+ The following is an example of using the Low-Level API to generate coverage
249
+ without Vitest. This uses the [ nyc] CLI to generate reports from the coverage
250
+ files that are generated. Once you run the script, you can run the following
251
+ command to generate a report.
252
+
253
+ ``` bash
254
+ npx nyc report --extension .schema.json
255
+ ```
256
+
257
+ ``` TypeScript
258
+ import { randomUUID } from " node:crypto" ;
259
+ import { existsSync } from " node:fs" ;
260
+ import { mkdir , rm , writeFile } from " node:fs/promises" ;
261
+ import { validate } from " @hyperjump/json-schema/draft-2020-12" ;
262
+ import { BASIC } from " @hyperjump/json-schema/experimental" ;
263
+ import {
264
+ CoverageMapService ,
265
+ TestCoverageEvaluationPlugin
266
+ } from " @hyperjump/json-schema-coverage" ;
267
+
268
+ const schemaUnderTest = ` scratch/foo.schema.json ` ;
269
+
270
+ // Tell the CoverageMapService which schemas we want coverage for.
271
+ const coverageService = new CoverageMapService ();
272
+ await coverageService .addFromFile (schemaUnderTest );
273
+
274
+ const validateFoo = await validate (schemaUnderTest );
275
+
276
+ // A function to run tests and write coverage files where nyc expects them.
277
+ const test = async (instance : any , valid : boolean ) => {
278
+ // Validate with the TestCoverageEvaluationPlugin
279
+ const testCoveragePlugin = new TestCoverageEvaluationPlugin (coverageService );
280
+ const output = validateFoo (instance , {
281
+ outputFormat: BASIC ,
282
+ plugins: [testCoveragePlugin ]
283
+ });
284
+
285
+ // Write the coverage file
286
+ const filePath = ` .nyc_output/${randomUUID ()}.json ` ;
287
+ await writeFile (filePath , JSON .stringify (testCoveragePlugin .coverage ));
288
+
289
+ // Report failures
290
+ if (output .valid !== valid ) {
291
+ const instanceJson = JSON .stringify (instance , null , " " );
292
+ const outputJson = JSON .stringify (output , null , " " );
293
+ console .log (" TEST FAILED:" , instanceJson , " \n OUTPUT:" , outputJson );
294
+ }
295
+ };
296
+
297
+ // Initialize coverage directory
298
+ if (existsSync (" .nyc_output" )) {
299
+ await rm (" .nyc_output" , { recursive: true });
300
+ }
301
+ await mkdir (" .nyc_output" );
302
+
303
+ // Run the tests
304
+ await test ({ foo: 42 }, true );
305
+ await test ({ foo: null }, false );
306
+ ```
140
307
141
- TODO
308
+ [ @hyperjump/json-schema ] : https://www.npmjs.com/package/@hyperjump/json-schema
309
+ [ istanbul ] : https://istanbul.js.org/
310
+ [ nyc ] : https://www.npmjs.com/package/nyc
0 commit comments