11# Hyperjump - JSON Schema Test Coverage
22
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] .
78
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.
1011
1112```
1213-------------|---------|----------|---------|---------|-------------------
@@ -19,10 +20,14 @@ All files | 81.81 | 66.66 | 80 | 88.88 |
1920
2021![ HTML coverage example] ( coverage.png )
2122
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+
2227** Legend**
2328- ** Statements** = Keywords and subschemas
2429- ** 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 ` )
2631- ** Functions** = Subschemas
2732
2833## Limitations
@@ -31,9 +36,11 @@ The following are a list of known limitations. Some might be able to be
3136addressed at some point, while others might not.
3237
3338- 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".
3744
3845## Vitest
3946
@@ -47,10 +54,10 @@ By default, it will track coverage for any file with a `*.schema.json`,
4754
4855** Options**
4956
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"] ` .
5461
5562` vitest-schema.config.js `
5663``` TypeScript
@@ -59,10 +66,11 @@ import type { JsonSchemaCoverageProviderOptions } from "@hyperjump/json-schema-c
5966
6067export default defineConfig ({
6168 test: {
69+ include: [" schema-tests/" ],
6270 coverage: {
6371 provider: " custom" ,
6472 customProviderModule: " @hyperjump/json-schema-coverage/vitest-coverage-provider" ,
65- include: [" ./ schemas/**/*.json" ] // Optional
73+ include: [" schemas/**/*.json" ] // Optional
6674 } as JsonSchemaCoverageProviderOptions
6775 }
6876});
@@ -73,47 +81,51 @@ vitest run --config=vitest-schema.config.js --coverage
7381```
7482
7583When 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.
7885
7986``` JavaScript
8087import { describe , expect , test } from " vitest" ;
8188import " @hyperjump/json-schema-coverage/vitest-matchers" ;
8289
8390describe (" 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" );
8694 });
8795
88- test (" doesn't match with uri" , async () => {
96+ test (" doesn't match with jest-style matcher" , async () => {
97+ // 🚨 DON'T FORGET THE `await` 🚨
8998 await expect ({ foo: null }).not .toMatchJsonSchema (" ./schema.json" );
9099 });
91100});
92101```
93102
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.
97107
98108``` JavaScript
99109import { describe , expect , test } from " vitest" ;
100110import { registerSchema , unregisterSchema } from " @hyperjump/json-schema-coverage/vitest-matchers" ;
101111
102112describe (" Worksheet" , () => {
103- beforeEach (() => {
104- registerSchema (" ./schema.json" );
113+ beforeEach (async () => {
114+ await registerSchema (" ./schema.json" );
105115 });
106116
107- afterEach (() => {
108- unregisterSchema (" ./schema.json" );
117+ afterEach (async () => {
118+ await unregisterSchema (" ./schema.json" );
109119 });
110120
111- test (" matches with uri" , async () => {
121+ test (" matches with jest-style matcher" , async () => {
122+ // 🚨 DON'T FORGET THE `await` 🚨
112123 await expect ({ foo: 42 }).toMatchJsonSchema (" https://example.com/main" );
113124 });
114125
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" );
117129 });
118130});
119131```
@@ -127,15 +139,172 @@ import "@hyperjump/json-schema-coverage/vitest-matchers";
127139
128140describe (" Worksheet" , () => {
129141 test (" matches with schema" , async () => {
142+ // 🚨 DON'T FORGET THE `await` 🚨
130143 await expect (" foo" ).to .matchJsonSchema ({ type: " string" });
131144 });
132145
133146 test (" doesn't match with schema" , async () => {
147+ // 🚨 DON'T FORGET THE `await` 🚨
134148 await expect (42 ).to .not .matchJsonSchema ({ type: " string" });
135149 });
136150});
137151```
138152
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+ ```
140307
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