Skip to content

Tester APIs

The separate create-testers package includes testing utilities that run Producers in fully virtualized environments. This is intended for use in unit tests that should mock out all System Context.

Terminal window
npm i create-testers -D

testBase

For Bases, a testBase function is exported that is analogous to produceBase. It takes in similar arguments:

  1. base (required): a Base
  2. settings (optional): production settings including simulated user-provided Options

For example, this test asserts that a Base defaults its value option to "default" when not provided:

import { testBase } from "create-testers";
import { describe, expect, it } from "vitest";
import { base } from "./base";
describe("base", () => {
describe("value", () => {
it("defaults to 'default' when not provided", async () => {
const actual = await testBase(base);
expect(actual).toEqual({
value: "default",
});
});
});
});

As with produceBase, testBase returns a Promise for the Base’s Options.

settings and all its properties are optional. However, some properties will cause testBase to throw an error if they’re not provided and the Base 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 Base Options may be provided under options.

For example, this test asserts that a Base uses a value if provided:

import { testBase } from "create-testers";
import { describe, expect, it } from "vitest";
import { base } from "./base";
describe("base", () => {
describe("value", () => {
it("uses a provided value when it exists", async () => {
const value = "override";
const actual = await testBase(base, {
options: { value },
});
expect(actual).toEqual({ value });
});
});
});

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 Base defaults its name to the property in package.json:

import { testBase } from "create-testers";
import { describe, expect, it, vi } from "vitest";
import { base } from "./base";
import { inputJsonFile } from "./inputJsonFile";
describe("base", () => {
describe("name", () => {
it("uses the package.json name if it exists", async () => {
const name = "create-create-app";
const take = vi.fn().mockResolvedValue({ name });
const actual = await testBase(base, { take });
expect(actual).toEqual({ name });
expect(take).toHaveBeenCalledWith(inputJsonFile, "package.json");
});
});
});

testBlock

For Blocks, a testBlocks function is exported that is analogous to produceBlock. It takes in similar arguments:

  1. block (required): a Block
  2. settings (optional): production settings including the Block’s Options and any Args

For example, this test asserts that an nvmrc Block creates an ".nvmrc" file with content "20.12.2":

import { testBlock } from "create-testers";
import { describe, expect, it } from "vitest";
import { blockNvmrc } from "./blockNvmrc";
describe("blockNvmrc", () => {
it("returns an .nvmrc", async () => {
const actual = await testBlock(blockNvmrc);
expect(actual).toEqual({
files: { ".nvmrc": "20.12.2" },
});
});
});

As with produceBlock, testBlock returns a Promise for the Block’s Creation. Both Direct Creations and Indirect Creations will be present.

settings and all its properties are optional. However, some properties will cause testBlock to throw an error if they’re not provided and the Block attempts to use them:

  • addons: throws an error if accessed at all
  • options: each property throws an error if accessed at all

addons

Block Addons may be provided under addons.

For example, this test asserts that a Prettier block adds a useTabs arg to its output ".prettierrc.json":

import { testBlock } from "create-testers";
import { describe, expect, expect, it } from "vitest";
import { z } from "zod";
import { base } from "./base";
const blockPrettier = base.createBlock({
addons: {
useTabs: z.boolean(),
},
produce({ addons }) {
return {
files: {
".prettierrc.json": JSON.stringify({
$schema: "http://json.schemastore.org/prettierrc",
useTabs: addons.useTabs,
}),
},
};
},
});
describe("blockPrettier", () => {
it("creates a .prettierrc.json when provided options", async () => {
const actual = await testBlock(blockPrettier, {
addons: {
config: {
useTabs: true,
},
},
});
expect(actual).toEqual({
files: {
".prettierrc.json": JSON.stringify({
$schema: "http://json.schemastore.org/prettierrc",
useTabs: true,
}),
},
});
});
});

options

Base Options may be provided under options.

For example, this test asserts that a README.md uses the title defined under options:

import { testBlock } from "create-testers";
import { describe, expect, it } from "vitest";
import { base } from "./base";
const blockReadme = base.createBlock({
produce({ options }) {
return {
files: {
"README.md": `# ${options.title}`,
},
};
},
});
describe("blockDocs", () => {
it("uses options.name for the README.md title", async () => {
const actual = await testBlock(blockReadme, {
options: {
title: "My Project",
},
});
expect(actual).toEqual({
files: {
"README.md": `# My Project`,
},
});
});
});

testInput

For Inputs, a testInput function is exported that is analogous to produceInput. It takes in similar arguments:

  1. input (required): an Input
  2. settings (optional): production settings including the Input’s Options and any Args

For example, this test asserts that an inputNow returns a numeric timestamp:

import { testInput } from "create-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 produceInput, 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
  • fetcher: by default, throws an error if called as a function
  • fs: by default, each method throws an error if called as a function
  • runner: by default, throws an error if called as a function

args

Input Args may be provided under args.

import { testInput } from "create-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);
});
});

fetcher

A mock function to act as the global fetch.

For example, this test asserts that an inputCatFact Input returns the fact property of a response:

import { testInput } from "create-testers";
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 fetcher = vi.fn().mockResolvedValueOnce({
json: () => Promise.resolve({ fact }),
});
const actual = await testInput(inputCatFact, { fetcher });
expect(actual).toEqual(fact);
expect(fetcher).toHaveBeenCalledWith("https://catfact.ninja/fact");
});
});

fs

An object containing mocks to act as a file system.

For example, this test asserts that an inputFromFile input returns the text of a file from disk:

import { testInput } from "create-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");
});
});

runner

A mock function to act as execa.

For example, this test asserts that an inputGitUserEmail Input returns the text from running git config user.email:

import { testInput } from "create-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 "create-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);
});
});

testPreset

For Presets, a testPreset function is exported that is analogous to producePreset. It takes in similar arguments:

  1. preset (required): a Preset
  2. settings (optional): production settings including the Preset’s Options and any Args

For example, this test asserts that a Preset using an nvmrc Block creates an ".nvmrc" file with content equal to its version option:

import { testPreset } from "create-testers";
import { describe, expect, it } from "vitest";
import { presetWithNvmrc } from "./presetWithNvmrc";
describe("presetWithNvmrc", () => {
it("returns an .nvmrc", async () => {
const actual = await testPreset(presetWithNvmrc, {
options: {
version: "20.12.2",
},
});
expect(actual).toEqual({
files: { ".nvmrc": "20.12.2" },
});
});
});

As with producePreset, testPreset returns a Promise for the Preset’s Creation. Both Direct Creations and Indirect Creations will be present.

settings and all its properties are optional. However, some properties will cause testPreset to throw an error if they’re not provided and the Block attempts to use them:

  • fetcher: by default, throws an error if called as a function
  • fs: by default, each method throws an error if called as a function
  • runner: by default, throws an error if called as a function

fetcher

A mock function to act as the global fetch.

For example, this test asserts that a Preset internally fetches cat facts from an API and stores them in a file:

import { testPreset } from "create-testers";
import { describe, expect, it, vi } from "vitest";
import { presetWithCatFact } from "./presetWithCatFact";
describe("presetWithCatFact", () => {
it("prints the cat fact from the API in fact.txt file", async () => {
const fact =
"Owning a cat is actually proven to be beneficial for your health.";
const fetcher = vi.fn().mockResolvedValueOnce({
json: () => Promise.resolve({ fact }),
});
const actual = await testPreset(presetWithCatFact, { fetcher });
expect(actual).toEqual({
files: {
"fact.txt": fact,
},
});
expect(fetcher).toHaveBeenCalledWith("https://catfact.ninja/fact");
});
});

fs

An object containing mocks to act as a file system.

For example, this test asserts that a Preset internally copies a backup.txt file to an current.txt file:

import { testPreset } from "create-testers";
import { describe, expect, it, vi } from "vitest";
import { presetWithBackup } from "./inputCatFact";
describe("presetWithBackup", () => {
it("copies backup.txt to current.txt when backup.txt exists", async () => {
const contents = "abc123";
const readFile = vi.fn().mockResolvedValue(contents);
const actual = await testPreset(presetWithBackup, {
fs: { readFile },
});
expect(actual).toEqual({
files: {
"current.txt": contents,
},
});
expect(readFile).toHaveBeenCalledWith("backup.txt");
});
});

runner

A mock function to act as execa.

For example, this test asserts that Preset includes the running user’s email in an AUTHORS.md file:

import { testPreset } from "create-testers";
import { describe, expect, it, vi } from "vitest";
import { presetAuthorship } from "./presetAuthorship";
describe("presetAuthorship", () => {
it("puts the running user's git config user.email in AUTHORS.md", async () => {
const email = "rick.astley@example.com";
const runner = vi.fn().mockResolvedValueOnce({
stdout: email,
});
const actual = await testPreset(presetAuthorship, { runner });
expect(actual).toEqual({
files: {
"AUTHORS.md": email,
},
});
expect(runner).toHaveBeenCalledWith("git config user.email");
});
});