Test utilities for Yeoman generators providing a fluent API for setting up test environments and asserting generated content
The RunContext provides a fluent, chainable API for configuring and executing generator tests. It manages the complete test lifecycle from setup through execution to cleanup, with support for temporary directories, file system mocking, prompt simulation, and generator composition.
The RunContext exposes several properties for advanced test scenarios:
interface RunContext<GeneratorType> {
/** The generator instance being tested */
generator: GeneratorType;
/** The Yeoman environment instance */
env: DefaultEnvironmentApi;
/** Memory file system store */
memFs: Store<MemFsEditorFile>;
/** Memory file system editor */
editor: MemFsEditor;
/** Context configuration settings */
settings: RunContextSettings;
/** Environment options */
envOptions: BaseEnvironmentOptions;
/** Whether the test has completed */
completed: boolean;
/** Target directory for the test */
targetDirectory?: string;
/** Mocked generators for composition testing */
mockedGenerators: Record<string, BaseGenerator>;
/** Mock spawn stub when using spawn mocking */
spawnStub?: any;
/** Questions asked during prompt simulation */
askedQuestions: AskedQuestions;
}Create and configure test execution contexts for generator testing.
/**
* Run the provided Generator in a test context
* @param GeneratorOrNamespace - Generator constructor or namespace
* @param settings - Configuration options for the run context
* @param environmentOptions - Options for the Yeoman environment
* @returns RunContext instance for chaining configuration
*/
function run<GeneratorType extends BaseGenerator = DefaultGeneratorApi>(
GeneratorOrNamespace: string | GetGeneratorConstructor<GeneratorType>,
settings?: RunContextSettings,
environmentOptions?: BaseEnvironmentOptions
): RunContext<GeneratorType>;
/**
* Alias for run() - prepare a run context
* @param GeneratorOrNamespace - Generator constructor or namespace
* @param settings - Configuration options
* @param environmentOptions - Environment options
* @returns RunContext instance
*/
function create<GeneratorType extends BaseGenerator = DefaultGeneratorApi>(
GeneratorOrNamespace: string | GetGeneratorConstructor<GeneratorType>,
settings?: RunContextSettings,
environmentOptions?: BaseEnvironmentOptions
): RunContext<GeneratorType>;
/**
* Prepare temporary dir without generator support
* @param settings - Configuration options
* @returns BasicRunContext for directory-only testing
*/
function prepareTemporaryDir(settings?: RunContextSettings): BasicRunContext;Usage Examples:
import helpers from "yeoman-test";
// Basic generator test
const context = helpers.run("my-generator");
// Generator from file path
const context = helpers.run("./path/to/generator");
// With custom settings
const context = helpers.run("my-generator", {
tmpdir: false,
cwd: "/custom/path"
});
// Directory-only testing
const context = helpers.prepareTemporaryDir();Configure generator arguments, options, and behavior before execution.
/**
* Provide arguments to the run context
* @param args - Command line arguments as Array or space separated string
* @returns this for method chaining
*/
withArguments(args: string | string[]): this;
/**
* Provide options to the run context
* @param options - Command line options (e.g. --opt-one=foo)
* @returns this for method chaining
*/
withOptions(options: Partial<Omit<GetGeneratorOptions<GeneratorType>, 'env' | 'namespace' | 'resolved'>>): this;
/**
* Mock answers for prompts
* @param answers - Answers to the prompt questions
* @param options - Additional prompt options
* @returns this for method chaining
*/
withAnswers(answers: PromptAnswers, options?: Omit<DummyPromptOptions, 'mockedAnswers'>): this;
/**
* @deprecated Use withAnswers instead
* Mock the prompt with dummy answers
*/
withPrompts(answers: PromptAnswers, options?: Omit<DummyPromptOptions, 'mockedAnswers'>): this;Usage Examples:
await helpers.run("my-generator")
.withArguments(["arg1", "arg2"])
.withOptions({
skipInstall: true,
force: true
})
.withAnswers({
projectName: "test-app",
features: ["typescript", "testing"]
});
// Arguments as string
await helpers.run("my-generator")
.withArguments("init --force");Configure files, directories, and configuration before generator execution.
/**
* Add files to mem-fs. Files will be resolved relative to targetDir.
* Files with Object content will be merged to existing content.
* @param files - Object mapping file paths to content
* @returns this for method chaining
*/
withFiles(files: Record<string, string | Record<string, unknown>>): this;
/**
* Add files to specific relative path
* @param relativePath - Path relative to target directory
* @param files - Object mapping file paths to content
* @returns this for method chaining
*/
withFiles(relativePath: string, files: Record<string, string | Record<string, unknown>>): this;
/**
* Add .yo-rc.json to mem-fs
* @param content - Content for .yo-rc.json as string or object
* @returns this for method chaining
*/
withYoRc(content: string | Record<string, unknown>): this;
/**
* Add a generator config to .yo-rc.json
* @param key - Generator key for the configuration
* @param content - Configuration object
* @returns this for method chaining
*/
withYoRcConfig(key: string, content: Record<string, unknown>): this;
/**
* Mock the local configuration with the provided config
* @param localConfig - Should look like config.getAll() output
* @returns this for method chaining
*/
withLocalConfig(localConfig: any): this;
/**
* Commit mem-fs files to disk
* @returns this for method chaining
*/
commitFiles(): this;Usage Examples:
await helpers.run("my-generator")
.withFiles({
"package.json": '{"name": "existing-project"}',
"config.json": { theme: "dark", debug: true },
"templates/base.hbs": "Hello {{name}}"
})
.withYoRcConfig("my-generator", {
previousRun: true,
settings: { typescript: true }
})
.commitFiles();
// Files in subdirectory
await helpers.run("my-generator")
.withFiles("src", {
"index.ts": "export * from './lib';",
"lib.ts": "export const VERSION = '1.0.0';"
});Configure dependent generators and mocked generators for composition testing.
/**
* Provide dependent generators
* @param dependencies - Paths to generators or [generator, options] pairs
* @returns this for method chaining
*/
withGenerators(dependencies: Dependency[]): this;
/**
* Create mocked generators for testing composition
* @param namespaces - Namespaces of mocked generators
* @returns this for method chaining
*/
withMockedGenerators(namespaces: string[]): this;
/**
* Provide custom mocked generator factory
* @param mockedGeneratorFactory - Factory function for creating mocked generators
* @returns this for method chaining
*/
withMockedGeneratorFactory(mockedGeneratorFactory: MockedGeneratorFactory): this;
type Dependency = string | Parameters<DefaultEnvironmentApi['register']>;
type MockedGeneratorFactory<GenParameter extends BaseGenerator = DefaultGeneratorApi> = (
GeneratorClass?: GetGeneratorConstructor<GenParameter>
) => GetGeneratorConstructor<GenParameter>;Usage Examples:
await helpers.run("my-generator")
.withGenerators([
"./path/to/dep-generator",
["./other-generator", { namespace: "custom:name" }]
])
.withMockedGenerators([
"sub-generator:app",
"common:util"
]);
// Access mocked generators in tests
it("composes with sub-generator", () => {
result.assertGeneratorComposed("sub-generator:app");
const mock = result.getGeneratorMock("sub-generator:app");
expect(mock.callCount()).toBe(1);
});Configure the Yeoman environment and lookup behavior.
/**
* Run lookup on the environment
* @param lookups - Lookup options to run
* @returns this for method chaining
*/
withLookups(lookups: LookupOptions | LookupOptions[]): this;
/**
* Customize environment creation
* @param callback - Callback to modify environment
* @returns this for method chaining
*/
withEnvironment(callback: (env: DefaultEnvironmentApi) => any): this;
/**
* Customize environment run method
* @param callback - Custom run implementation
* @returns this for method chaining
*/
withEnvironmentRun(callback: (this: this, env: DefaultEnvironmentApi, gen: GeneratorType) => void): this;
/**
* TestAdapter options for prompt mocking
* @param options - Adapter configuration options
* @returns this for method chaining
*/
withAdapterOptions(options: Omit<TestAdapterOptions, 'mockedAnswers'>): this;Mock spawn calls for testing generators that execute external commands.
/**
* Mock spawn calls with custom stub or default implementation
* @param options - Stub function or configuration object
* @returns this for method chaining
*/
withSpawnMock<StubType = ReturnType<typeof mock.fn>>(
options?: ((...args) => any) | {
stub?: (...args) => any;
registerNodeMockDefaults?: boolean;
callback?: ({ stub, implementation }: { stub: StubType; implementation: any }) => void | Promise<void>;
}
): this;Usage Examples:
// Default spawn mocking
await helpers.run("my-generator")
.withSpawnMock();
// Custom spawn behavior
await helpers.run("my-generator")
.withSpawnMock({
stub: mock.fn(() => ({ exitCode: 0, stdout: "success" })),
callback: ({ stub }) => {
// Configure stub behavior
stub.mockReturnValueOnce({ exitCode: 1, stderr: "error" });
}
});
// Access spawn calls in tests
it("runs expected commands", () => {
const spawnArgs = result.getSpawnArgsUsingDefaultImplementation();
expect(spawnArgs[0]).toEqual(["spawnCommand", "npm", ["install"]]);
});Configure working directories and temporary directory behavior.
/**
* @deprecated Clean the provided directory, then change directory into it
* @param dirPath - Directory path (relative to CWD)
* @param callback - Callback receiving the folder path
* @returns this for method chaining
*/
inDir(dirPath: string, callback?: (folderPath: string) => void): this;
/**
* @deprecated Change directory without deleting directory content
* @param dirPath - Directory path (relative to CWD)
* @returns this for method chaining
*/
cd(dirPath: string): this;
/**
* Cleanup a temporary directory and change the CWD into it
* @param callback - Callback receiving the folder path
* @returns this for method chaining
*/
inTmpDir(callback?: (folderPath: string) => void): this;
/**
* Register a callback to prepare the destination folder
* @param callback - Callback receiving the folder path
* @returns this for method chaining
*/
doInDir(callback: (folderPath: string) => void): this;Configure callbacks for different stages of the test execution.
/**
* Execute callback when generator is ready
* @param callback - Callback receiving the generator instance
* @returns this for method chaining
*/
onGenerator(callback: (this: this, generator: GeneratorType) => any): this;
/**
* Execute callback when environment is ready
* @param callback - Callback receiving the environment instance
* @returns this for method chaining
*/
onEnvironment(callback: (this: this, environment: DefaultEnvironmentApi) => any): this;
/**
* Execute callback when target directory is set
* @param callback - Callback receiving the target directory path
* @returns this for method chaining
*/
onTargetDirectory(callback: (this: this, targetDirectory: string) => any): this;
/**
* Execute callback before preparation phase
* @param callback - Callback executed before test preparation
* @returns this for method chaining
*/
onBeforePrepare(callback: (this: this) => void | Promise<void>): this;Usage Examples:
await helpers.run("my-generator")
.onGenerator(generator => {
// Customize generator before execution
generator.options.customFlag = true;
})
.onEnvironment(env => {
// Configure environment
env.register("./custom-generator", { namespace: "custom:gen" });
})
.onTargetDirectory(targetDir => {
// Setup target directory
fs.writeFileSync(path.join(targetDir, "preset.json"), "{}");
});Execute the generator and manage test state.
/**
* Run the generator and return results
* @returns Promise resolving to RunResult instance
*/
run(): Promise<RunResult<GeneratorType>>;
/**
* Build/prepare the context without running the generator
* @returns Promise resolving when context is ready
*/
build(): Promise<void>;
/**
* Prepare the test context without running
* @returns Promise resolving when preparation is complete
*/
prepare(): Promise<void>;
/**
* Don't reset mem-fs state to aggregate snapshots from multiple runs
* @returns this for method chaining
*/
withKeepFsState(): this;
/**
* Clean the directory used for tests and restore original working directory
*/
cleanup(): void;
/**
* Clean the temporary directory
*/
cleanupTemporaryDir(): void;
/**
* Clean the test directory with optional force flag
* @param force - Force cleanup even if directory is not temporary
* @returns this for method chaining
*/
cleanTestDirectory(force?: boolean): this;
/**
* Restore original working directory
* @returns this for method chaining
*/
restore(): this;RunContext implements the Promise interface for convenient async/await usage:
/**
* RunContext implements Promise<RunResult<GeneratorType>>
*/
interface RunContext<GeneratorType> extends Promise<RunResult<GeneratorType>> {
then<TResult1, TResult2>(
onfulfilled?: (value: RunResult<GeneratorType>) => TResult1 | PromiseLike<TResult1>,
onrejected?: (reason: any) => TResult2 | PromiseLike<TResult2>
): Promise<TResult1 | TResult2>;
catch<TResult>(
onrejected?: (reason: any) => TResult | PromiseLike<TResult>
): Promise<RunResult<GeneratorType> | TResult>;
finally(onfinally?: () => void): Promise<RunResult<GeneratorType>>;
}Usage Examples:
// Promise-style usage
const result = await helpers.run("my-generator")
.withOptions({ skipInstall: true });
// Traditional promise chains
helpers.run("my-generator")
.then(result => {
result.assertFile("package.json");
})
.catch(error => {
console.error("Test failed:", error);
});Install with Tessl CLI
npx tessl i tessl/npm-yeoman-test