From 5c253efcd1526a2f762405ef568ba61e0e6bc675 Mon Sep 17 00:00:00 2001 From: Mika Vilpas Date: Mon, 1 Apr 2024 10:48:42 +0300 Subject: [PATCH] feat(testing): export jest matchers for parjs users to use feat(testing): export jest test utilities for users to use This will help developers working with Parjs to write tests for their parsers. feat(testing): export jest types for parjs users --- documentation/using-parjs.md | 67 +++++++++++++++++++ jest.config.mjs | 2 +- package.json | 6 ++ src/utilities/index.ts | 1 + .../jest-setup.ts => utilities/jest/index.ts} | 12 ++-- 5 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 src/utilities/index.ts rename src/{test/helpers/jest-setup.ts => utilities/jest/index.ts} (90%) diff --git a/documentation/using-parjs.md b/documentation/using-parjs.md index f5e7f3f..e4b76d0 100644 --- a/documentation/using-parjs.md +++ b/documentation/using-parjs.md @@ -48,3 +48,70 @@ This will help you see: - The input that was consumed - The position in the input - The value that was parsed (returned by the parser) + +## Example workflow with a test runner + +Working on your parser by writing tests is an interactive and fun way to develop. Here is a workflow that you can use: + +1. Write your parser implementation in a file +2. Write a test file that imports the parser and tests it +3. Have a test runner that runs the tests and shows the results + + - You can use Jest, Vitest, Mocha, or any other test runner + +Let's write a simple parser that parses a shopping list. The shopping list will have two sections: one for fruit and one for vegetables. Each section will have a list of items. + +Start writing your parser implementation little by little: + +```ts +// shopping-list.ts +import { string } from "parjs"; +import { many1, manySepBy, or, recover, then } from "parjs/combinators"; + +export const fruitSection = string("Remember to buy ").pipe( + then(string("apples").pipe(or(string("bananas")))) +); +``` + +Then write a test file that tests the parser: + +```ts +// shopping-list.test.ts +import { type ParjsResult } from "parjs"; +import { fruitSection, shoppingList, vegetablesSection } from "./shopping-list"; + +describe("fruitSection", () => { + it("can parse apples", () => { + const result: ParjsResult<["Remember to buy ", "apples" | "bananas"]> = + fruitSection.parse("Remember to buy apples"); + // (^ you can leave out the explicit type in your test code) + + expect(result.isOk).toBe(true); + expect(result.value).toEqual(["Remember to buy ", "apples"]); + }); + + it("can parse bananas", () => { + // NOTE: if you are using jest, you can also simplify the testing logic by + // importing the jest utilities that parjs uses internally. If you have any + // issues, you can just fall back to the simple method described above. + // + // To do this, you need to add the following line to the top of your test + // file: + // + // import "parjs/utilities"; + // + // You can also add it to your test setup file, which is run before any + // tests are executed. + expect(fruitSection.parse("Remember to buy bananas")).toBeSuccessful([ + "Remember to buy ", + "bananas" + ]); + }); +}); +``` + +Now you can run your tests with your test runner. + +When you add more functionality, you can add more tests. You can also use the `.debug()` method to inspect the behaviour of your parser as described above. If your test runner executes the tests automatically, you can iterate on the parser very quickly. + +Tip: If you have access to an AI assistant such as Github Copilot, you can use it to very quickly generate test cases. This is a great application of AI since the tests are very repetitive and self contained. diff --git a/jest.config.mjs b/jest.config.mjs index 3326809..6db1fa9 100644 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -2,7 +2,7 @@ import path from "path"; /** @type {import("jest").Config} */ const config = { - setupFilesAfterEnv: [path.join("", "src", "test", "helpers", "jest-setup.ts")], + setupFilesAfterEnv: [path.join("", "src", "utilities", "jest", "index.ts")], testEnvironment: "node", testPathIgnorePatterns: ["dist"], transform: { diff --git a/package.json b/package.json index 98f0d0f..825688a 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,12 @@ "import": "./dist/lib/internal.js", "default": "./dist/lib/internal.js", "types": "./dist/lib/internal.d.ts" + }, + "./utilities": { + "require": "./dist/utilities/index.js", + "import": "./dist/utilities/index.js", + "default": "./dist/utilities/index.js", + "types": "./dist/utilities/index.d.ts" } }, "typings": "dist/lib/index", diff --git a/src/utilities/index.ts b/src/utilities/index.ts new file mode 100644 index 0000000..15fe313 --- /dev/null +++ b/src/utilities/index.ts @@ -0,0 +1 @@ +export * from "./jest/"; diff --git a/src/test/helpers/jest-setup.ts b/src/utilities/jest/index.ts similarity index 90% rename from src/test/helpers/jest-setup.ts rename to src/utilities/jest/index.ts index 467bc50..5c2aae7 100644 --- a/src/test/helpers/jest-setup.ts +++ b/src/utilities/jest/index.ts @@ -1,6 +1,6 @@ import { expect } from "@jest/globals"; import type { MatcherFunction, SyncExpectationResult } from "expect"; -import type { ResultKind } from "../../lib"; +import type { ResultKind } from "../../lib/internal/result"; import { isParjsFailure, isParjsResult, isParjsSuccess } from "../../lib/internal/result"; // helper @@ -9,7 +9,7 @@ const fail = (message: string): SyncExpectationResult => ({ message: () => message }); -const toBeSuccessful: MatcherFunction<[value: unknown]> = +export const toBeSuccessful: MatcherFunction<[value: unknown]> = // jest recommends to type the parameters as `unknown` and to validate the values function (actual: unknown, expected: unknown): SyncExpectationResult { if (!isParjsResult(actual)) { @@ -46,7 +46,7 @@ const toBeSuccessful: MatcherFunction<[value: unknown]> = }; }; -const toBeFailure: MatcherFunction<[kind?: string]> = function ( +export const toBeFailure: MatcherFunction<[kind?: string]> = function ( actual: unknown, expected: unknown ): SyncExpectationResult { @@ -84,12 +84,10 @@ expect.extend({ declare global { // eslint-disable-next-line @typescript-eslint/no-namespace - namespace jest { - interface Matchers { + export namespace jest { + export interface Matchers { toBeSuccessful(value?: T): R; toBeFailure(kind?: ResultKind): R; } } } - -export {};