Testing
TypeSpec provides a testing framework to assist in testing libraries. The examples here are shown using vitest, but any other JS test framework can be used that will provide more advanced features like vitest, which is used in this project.
Setting up vitest
Section titled âSetting up vitestâThis step is a basic explanation of how to setup vitest. Please refer to the vitest documentation for more details.
-
Add vitest to your dependencies
package.json {"name": "my-library","scripts": {"test": "vitest run","test:watch": "vitest"},"devDependencies": {"vitest": "^3.1.4"}} -
Add a
vitest.config.tsfile at the root of your project.vitest.config.ts import { defineConfig, mergeConfig } from "vitest/config";export default defineConfig({test: {environment: "node",// testTimeout: 10000, // Uncomment to increase the default timeoutisolate: false, // Your test shouldn't have side effects doing this will improve performance.},});
Quick start
Section titled âQuick startâDefine the tester
Section titled âDefine the testerâDefine a tester for your library. This should be a root level file. It will ensure that file system calls are cached in between tests.
import { createTester } from "@typespec/compiler/testing";
const MyTester = createTester({ libraries: ["@typespec/http", "@typespec/openapi", "my-library"], // Add other libraries you depend on in your tests});Write your first test
Section titled âWrite your first testâimport { t } from "@typespec/compiler/testing";import { MyTester } from "./tester.js";import { it } from "vitest";
// Check everything works fineit("does this", async () => { const { Foo } = await MyTester.compile(t.code` model ${t.model("Foo")} {} `); strictEqual(Foo.name, "Foo");});
// Check diagnostics are emittedit("errors", async () => { const diagnostics = await MyTester.diagnose(` model Bar {} `); expectDiagnostics(diagnostics, { code: "...", message: "..." });});Tester API
Section titled âTester APIâcompile
Section titled âcompileâCompile the given code and assert no diagnostics were emitted.
// Check everything works fineit("does this", async () => { const { Foo } = await MyTester.compile(t.code` model ${t.model("Foo")} {} `); strictEqual(Foo.name, "Foo");});diagnose
Section titled âdiagnoseâCompile the given code and return the diagnostics.
it("errors", async () => { const diagnostics = await MyTester.diagnose(` model Bar {} `); expectDiagnostics(diagnostics, { code: "...", message: "..." });});compileAndDiagnose
Section titled âcompileAndDiagnoseâReturns a tuple of the result (same as compile) and the diagnostics (same as diagnose).
it("does this", async () => { const [diagnostics, { Foo }] = await MyTester.compileAndDiagnose(t.code` model ${t.model("Foo")} {} `); strictEqual(Foo.name, "Foo"); expectDiagnostics(diagnostics, { code: "...", message: "..." });});Tester chains
Section titled âTester chainsâThe tester uses a builder pattern to allow you to configure a tester. Each pipe provides a clone of the tester allowing you to create different testers without modifying the original one.
This will inject the given files in the tester.
import { mockFile } from "@typespec/compiler/testing";
const TesterWithFoo = MyTester.files({ "foo.tsp": ` model Foo {} `, "bar.js": mockFile.js({ $myDec: () => {}, }),});
await TesterWithFoo.compile(` import "./foo.tsp"; import "./bar.js";`);Import the given path or libraries
import { mockFile } from "@typespec/compiler/testing";
const TesterWithFoo = MyTester.import("my-library", "./foo.tsp");
await TesterWithFoo.compile(` model Bar is Foo;`);Example combining with files
import { mockFile } from "@typespec/compiler/testing";
const TesterWithFoo = MyTester.files({ "foo.tsp": ` model Foo {} `,}).import("./foo.tsp");
await TesterWithFoo.compile(` model Bar is Foo;`);importLibraries
Section titled âimportLibrariesâImport all the libraries originally defined in the createTester call.
const MyTester = createTester({ libraries: ["@typespec/http", "@typespec/openapi", "my-library"], // Add other libraries you depend on in your tests});
MyTester.importLibraries();
// equivalent toMyTester.import("@typespec/http", "@typespec/openapi", "my-library");Add the given using
import { mockFile } from "@typespec/compiler/testing";
const TesterWithFoo = MyTester.using("Http", "MyOrg.MyLibrary");Wrap the source of the main file.
import { mockFile } from "@typespec/compiler/testing";
const TesterWithFoo = MyTester.wrap(x=> ` model Common {} ${x} `);});
await TesterWithFoo.compile(` model Bar is Common;`);Collecting types
Section titled âCollecting typesâThe base tester provides a way to easily collect types from the test code in order to use them in the test. There are 3 ways this can be achieved:
| Option | Type inferred/validated |
|---|---|
1. t helper with t.code and t.<entity> | â |
2. Flourslash syntax (/*foo*/) | |
3. @test decorator |
- Using the
thelper witht.codeandt.<entity>
const { Foo } = await MyTester.compile(t.code` model ${t.model("Foo")} {}`); // type of Foo is automatically inferred and validated to be a ModelstrictEqual(Foo.name, "Foo");- Using flourslash syntax to mark the types you want to collect (
/*foo*/)
const { Foo } = await MyTester.compile(t.code` model /*foo*/Foo {}`); // Foo is typed as an EntitystrictEqual(Foo.entityKind, "Type");strictEqual(Foo.type, "Model");strictEqual(Foo.name, "Foo");- Using the
@testdecorator
This is mostly kept for backwards compatibility with the old test host. It has the limitation of only being to target decorable types.
It is preferable to use the t helper when possible or the flourslash syntax for more complex cases.
const { Foo } = await MyTester.compile(t.code` @test model Foo {}`); // Foo is typed as an EntitystrictEqual(Foo.entityKind, "Type");strictEqual(Foo.type, "Model");strictEqual(Foo.name, "Foo");Migrate from test host
Section titled âMigrate from test hostâPR with examples https://github.com/microsoft/typespec/pull/7151
Replace test host setup
Section titled âReplace test host setupâimport { createTestHost, createTestWrapper } from "@typespec/compiler/testing";import { HttpTestLibrary } from "@typespec/http/testing";import { RestTestLibrary } from "@typespec/rest/testing";import { MyTestLibrary } from "../src/testing/index.js";
export async function createMyTestHost() { return createTestHost({ libraries: [HttpTestLibrary, RestTestLibrary, MyTestLibrary], });}export async function createMyTestRunner() { const host = await createOpenAPITestHost(); return createTestWrapper(host, { autoUsings: ["TypeSpec.My"] });}
import { resolvePath } from "@typespec/compiler";import { createTester } from "@typespec/compiler/testing";
export const Tester = createTester(resolvePath(import.meta.dirname, ".."), { libraries: ["@typespec/http", "@typespec/rest", "@typespec/my"],}) .importLibraries() .using("My");Update test files
Section titled âUpdate test filesâRemove the beforeEach runner setup and use Tester directly. The program is available on the compile result.
import { createMyTestRunner } from "./test-host.js"; import { t } from "@typespec/compiler/testing"; import { Tester } from "./test-host.js";
let runner: BasicTestRunner; beforeEach(async () => { runner = await createMyTestRunner(); });
it("mark property as being an attribute", async () => { const { id } = (await runner.compile(`model Blob { @test @Xml.attribute id : string }`)) as { id: ModelProperty }; expect(isAttribute(runner.program, id)).toBe(true); const { id, program } = await Tester.compile(t.code`model Blob { @Xml.attribute ${t.modelProperty("id")} : string }`); expect(isAttribute(program, id)).toBe(true);});Injecting files
Section titled âInjecting filesâReplace host.addTypeSpecFile() / host.addJsFile() with the .files() chain and mockFile.js():
host.addJsFile("./dec.js", { $myDec: () => null });const diagnostics = await runner.diagnose(` import "./dec.js"; extern dec myDec(target: unknown);`);import { mockFile } from "@typespec/compiler/testing";const diagnostics = await Tester.files({ "./dec.js": mockFile.js({ $myDec: () => null }),}).diagnose(` import "./dec.js"; extern dec myDec(target: unknown);`);Emitter testing
Section titled âEmitter testingâFor emitter tests, use .emit() to create an EmitterTester. The compile result includes outputs with the emitted files:
export async function createMyEmitterTestRunner() { const host = await createMyTestHost(); return createTestWrapper(host, { compilerOptions: { noEmit: false, emit: ["@typespec/my-emitter"] }, });}
export const EmitterTester = Tester.emit("@typespec/my-emitter");const runner = await createMyEmitterTestRunner();await runner.compileAndDiagnose(code, { outputDir: "tsp-output" });
const [result, diagnostics] = await EmitterTester.compileAndDiagnose(code);// result.outputs contains the emitted files as Record<string, string>