Skip to content

bingo-testers

Test utilities for composable, testable, type-safe templates. ⚗️

Terminal window
npm i -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:

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 in actual, 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 using diff’s createTwoFilesPatch, 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:

  1. input (required): an Input
  2. context (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 all
  • fetchers: each throw an error if called as a function
  • fs: each method throws an error if called as a function
  • runner: throws an error if called as a function
  • take: 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:

  1. template (required): a Template
  2. context (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 all
  • take: 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 schemas
  • prepare: 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:

  1. template (required): a Template
  2. context (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" },
});
});
});
Made with 💝 in Boston by Josh Goldberg.