Skip to content

Commit

Permalink
feat(testing): export jest matchers for parjs users to use
Browse files Browse the repository at this point in the history
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
  • Loading branch information
barona-mika-vilpas authored and mikavilpas committed Apr 8, 2024
1 parent ba45fdc commit 5c253ef
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 8 deletions.
67 changes: 67 additions & 0 deletions documentation/using-parjs.md
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion jest.config.mjs
Expand Up @@ -2,7 +2,7 @@ import path from "path";

/** @type {import("jest").Config} */
const config = {
setupFilesAfterEnv: [path.join("<rootDir>", "src", "test", "helpers", "jest-setup.ts")],
setupFilesAfterEnv: [path.join("<rootDir>", "src", "utilities", "jest", "index.ts")],
testEnvironment: "node",
testPathIgnorePatterns: ["dist"],
transform: {
Expand Down
6 changes: 6 additions & 0 deletions package.json
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/utilities/index.ts
@@ -0,0 +1 @@
export * from "./jest/";
12 changes: 5 additions & 7 deletions src/test/helpers/jest-setup.ts → 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
Expand All @@ -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)) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -84,12 +84,10 @@ expect.extend({

declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace jest {
interface Matchers<R> {
export namespace jest {
export interface Matchers<R> {
toBeSuccessful<T>(value?: T): R;
toBeFailure(kind?: ResultKind): R;
}
}
}

export {};

0 comments on commit 5c253ef

Please sign in to comment.