bingo-testers
Test utilities for composable, testable, type-safe templates. ⚗️
npm i -D bingo-testers
pnpm add -D bingo-testers
yarn add -D bingo-testers
The separate bingo-testers
package includes testing utilities that run Inputs and Templates in fully virtualized environments.
This is intended for use in unit tests that should mock out all Contexts.
diffCreatedDirectory
Produces a nested object diff comparing the files
between an actual directory and produced results from a Creation.
Parameters:
actual
(required): The directory on disk, such as one retrieved by thebingo-fs
intakeFromDirectory
APIcreated
(required): The file system produced by a template, such as one generated by theproduceTemplate
API
For example, this test snippet runs an integration test for a template repository, making sure its files on disk match its own everything
Preset:
import { produceTemplate } from "bingo";import { intakeFromDirectory } from "bingo-fs";import { diffCreatedDirectory } from "bingo-testers";
import { exampleTemplate } from "./exampleTemplate.js";
const actual = await intakeFromDirectory(".", { exclude: /node_modules|^\.git$/,});
const created = await produceTemplate(exampleTemplate, { preset: "everything",});
expect(diffCreatedDirectory(actual, created)).toBeUndefined();
DiffedCreatedDirectory
diffCreatedDirectory
will return an object matching a DiffedCreatedDirectory
type.
Any files that are different in the created
argument compared to the actual
argument will be included in that object.
Differences are computed as:
- If a file exists in
created
but not inactual
, it will be included as-is - If a file exists in both but has different text content and/or
mode
, it will be included as a diff usingdiff
’screateTwoFilesPatch
, omitting headers before the@@
For example, if a src/index.ts
has content abc
in actual
but content bbc
in created
, the diff would look like:
{ "src": { "index.ts": `@@ -1,1 +1,1 @@-abc+bbc` }}
testInput
For Inputs, a testInput
function is exported that is analogous to runInput
.
It takes in similar arguments:
input
(required): an Inputcontext
(optional): any properties from an Input Context
For example, this test asserts that an inputNow
returns a numeric timestamp:
import { testInput } from "bingo-testers";import { describe, expect, it } from "vitest";
import { inputNow } from "./inputNow";
describe("inputNow", () => { it("returns a numeric timestamp", async () => { const actual = await testInput(inputNow);
expect(actual).toBeTypeOf("number"); });});
As with runInput
, testInput
returns the data from the Input.
settings
and all its properties are optional.
However, some properties will cause testInput
to throw an error if they’re not provided and the Input attempts to use them:
args
: throws an error if accessed at allfetchers
: each throw an error if called as a functionfs
: each method throws an error if called as a functionrunner
: throws an error if called as a functiontake
: throws an error if called as a function
args
Input Args values may be provided under args
.
This test passes a filePath
arg to an inputFromFile
input, along with fs
:
import { testInput } from "bingo-testers";import { describe, expect, it } from "vitest";
import { inputFromFile } from "./inputFromFile.js";
describe("inputFromFile", () => { it("returns the file's text when it exists", async () => { const text = "abc123";
const actual = await testInput(inputFromFile, { args: { filePath: "file.txt", }, fs: { readFile: () => Promise.resolve(text), }, });
expect(actual).toBe(text); });});
fetchers
A mock object to act as the global fetch
.
This is typically created by passing a function to createSystemFetchers
.
For example, this test asserts that an inputCatFact
Input returns the fact
property of a response:
import { createSystemFetchers } from "bingo-systems";import { testInput } from "bingo-testers";import { Octokit } from "octokit";import { describe, expect, it, vi } from "vitest";
import { inputCatFact } from "./inputCatFact";
describe("inputCatFact", () => { it("returns the cat fact from the API", async () => { const fact = "Owning a cat is actually proven to be beneficial for your health.";
const fetch = vi.fn().mockResolvedValueOnce({ json: () => Promise.resolve({ fact }), }); const fetchers = createSystemFetchers(fetch);
const actual = await testInput(inputCatFact, { fetchers });
expect(actual).toEqual(fact); expect(fetch).toHaveBeenCalledWith("https://catfact.ninja/fact"); });});
See Packages > bingo-systems
> Fetchers for documentation on system fetchers.
fs
An object containing mocks to act as a file system.
This can be any of the properties from a ReadingFileSystem
.
For example, this test asserts that an inputFromFile
input returns the text of a file from disk:
import { testInput } from "bingo-testers";import { describe, expect, it, vi } from "vitest";
import { inputFromFile } from "./inputCatFact";
describe("inputFromFile", () => { it("returns the contents of a file", async () => { const contents = "abc123"; const readFile = vi.fn().mockResolvedValue(contents);
const actual = await testInput(inputFromFile, { args: { fileName: "text.txt" }, fs: { readFile }, });
expect(actual).toEqual(contents); expect(readFile).toHaveBeenCalledWith("text.txt"); });});
If the input calls to any property that isn’t provided, an error will be thrown.
runner
A mock function to act as a SystemRunner
.
For example, this test asserts that an inputGitUserEmail
Input returns the text from running git config user.email
:
import { testInput } from "bingo-testers";import { describe, expect, it, vi } from "vitest";
import { inputGitUserEmail } from "./inputGitUserEmail";
describe("inputGitUserEmail", () => { it("returns text from git config user.email", async () => { const email = "rick.astley@example.com";
const runner = vi.fn().mockResolvedValueOnce({ stdout: email, });
const actual = await testInput(inputGitUserEmail, { runner });
expect(actual).toEqual(email); expect(runner).toHaveBeenCalledWith("git config user.email"); });});
take
The Context take
function may be provided under take
.
This is how to simulate the results of calling to other Inputs.
For example, this test asserts that an inputNpmUsername
Input uses the result of an inputNpmWhoami
Input:
import { testInput } from "bingo-testers";import { describe, expect, it, vi } from "vitest";
import { inputNpmUsername } from "./inputNpmUsername";import { inputNpmWhoami } from "./inputNpmWhoami";
describe("inputNpmUsername", () => { it("uses the result of npm whoami when available", async () => { const username = "joshuakgoldberg";
const take = vi.fn().mockResolvedValue({ stdout: username, });
const actual = await testInput(inputNpmUsername);
expect(actual).toBe(username); expect(take).toHaveBeenCalledWith(inputNpmWhoami); });});
testOptions
For Template prepare()
functions, a testOptions
function is exported that is analogous to prepareOptions
.
It takes in similar arguments:
template
(required): a Templatecontext
(optional): any properties from an Options Context
For example, this test asserts that a template defaults its value
option to "default"
when not provided:
import { testOptions } from "bingo-testers";import { describe, expect, it } from "vitest";
import { exampleTemplate } from "./exampleTemplate.js";
describe("exampleTemplate", () => { describe("value", () => { it("defaults to 'default' when not provided", async () => { const actual = await testOptions(exampleTemplate);
expect(actual).toEqual({ value: "default", }); }); });});
As with prepareOptions
, testOptions
returns a Promise for the template’s options.
settings
and all its properties are optional.
However, some properties will cause testOptions
to throw an error if they’re not provided and the template attempts to use them:
options
: each property throws an error if accessed at alltake
: by default, throws an error if called as a function
options
Simulated user-provided template options may be provided under options
.
For example, this test asserts that a template defaults its title
to a title-case version of its title
:
import { testOptions } from "bingo-testers";import { describe, expect, it } from "vitest";import { z } from "zod";
import { exampleTemplate } from "./exampleTemplate.js";
describe("exampleTemplate", () => { describe("title", () => { it("defaults to a lowercase title if not provided", async () => { const actual = await testOptions(exampleTemplate, { options: { name: "my-app", }, });
expect(actual).toEqual({ name: "my-app", title: "My App", }); }); });});
take
The Context take
function may be provided under take
.
This is how to simulate the results of Inputs.
For example, this test asserts that a template defaults its name
to the property in package.json
:
import { testOptions } from "bingo-testers";import { inputFromFileJSON } from "input-from-file-json";import { describe, expect, it, vi } from "vitest";
import { exampleTemplate } from "./exampleTemplate.js";
describe("exampleTemplate", () => { describe("name", () => { it("uses the package.json name if it exists", async () => { const take = vi.fn().mockResolvedValue({ name: "create-example" });
const actual = await testOptions(exampleTemplate, { take });
expect(actual).toEqual({ name: "create-example" }); expect(take).toHaveBeenCalledWith(inputFromFileJSON, { filePath: "package.json", }); }); });});
Custom Engine Testing
testOptions
can be given any object in place of a template as long as the object contains:
options
: an object mapping keys to Zod schemasprepare
: a function that returns an object whose properties are default values for those options
For example, the Stratum engine’s Bases can be tested with testOptions
:
import { testOptions } from "bingo-testers";import { createBase } from "bingo-stratum";import { z } from "zod";
const base = createBase({ options: { value: z.string().optional(), }, prepare(options) { return { value: "default", }; },});
describe("base", () => { describe("value", () => { it("defaults to 'default' when not provided", async () => { const actual = await testOptions(base);
expect(actual).toEqual({ value: "default", }); }); });});
testTemplate
For Templates, a testTemplate
function is exported that is analogous to produceTemplate
.
It takes in similar arguments:
template
(required): a Templatecontext
(optional): any properties from a Template Context
For example, this test asserts that a template creates an .nvmrc
file:
import { testTemplate } from "bingo-testers";import { describe, expect, it } from "vitest";
import { exampleTemplate } from "./exampleTemplate.js";
describe("exampleTemplate", () => { it("creates an .nvmrc file", () => { const actual = testTemplate(exampleTemplate);
expect(actual).toEqual({ files: { ".nvmrc": "20.12.2" }, }); });});
As with produceTemplate
, testTemplate
returns the template’s Creation.
Both Direct Creations and Indirect Creations will be present.
settings
and all its properties are optional.
However, some properties will cause testTemplate
to throw an error if they’re not provided and the Block attempts to use them:
options
: each property throws an error if accessed at all
options
Simulated user-provided template options may be provided under options
.
For example, this test asserts that a template creates an .nvmrc
file with content equal to its version
option:
import { testTemplate } from "bingo-testers";import { describe, expect, it } from "vitest";
import { exampleTemplate } from "./exampleTemplate.js";
describe("exampleTemplate", () => { it("returns an .nvmrc", () => { const actual = testTemplate(exampleTemplate, { options: { version: "20.12.2" }, });
expect(actual).toEqual({ files: { ".nvmrc": "20.12.2" }, }); });});