How to Test Google Genkit AI Applications
Google Genkit is an open-source framework for building AI-powered applications in TypeScript and Go. It provides typed flows, reusable prompts, tool definitions, and observability built-in. The testing story is practical — Genkit flows are just async functions with well-defined inputs and outputs, and the framework provides test utilities that let you run flows without calling a real model.
This guide covers unit testing Genkit flows with mocked models, testing tool implementations, running Genkit evaluations, and integration testing against live models.
Genkit Fundamentals for Testing
A Genkit application has three main testable units:
// Flows — typed async functions with AI calls inside
const summarizeFlow = ai.defineFlow(
{ name: "summarize", inputSchema: z.string(), outputSchema: z.string() },
async (text) => { /* ... */ }
);
// Tools — functions the model can call
const getWeatherTool = ai.defineTool(
{ name: "getWeather", inputSchema: z.object({ city: z.string() }) },
async ({ city }) => { /* call weather API */ }
);
// Prompts — reusable prompt templates
const summaryPrompt = ai.definePrompt(
{ name: "summary", inputSchema: z.string() },
"Summarize the following text in 2 sentences: {{input}}"
);Each is independently testable, which is Genkit's main advantage for testing.
Project Setup
npm install genkit @genkit-ai/googleai
npm install -D vitestBasic Genkit initialization for tests:
// src/ai/genkit.ts
import { genkit } from "genkit";
import { googleAI } from "@genkit-ai/googleai";
export const ai = genkit({
plugins: [googleAI()],
model: "googleai/gemini-2.0-flash",
});Unit Testing Flows with Mocked Models
Genkit's runFlow can be used in tests after you configure a test model that returns deterministic responses:
// src/ai/flows/summarize.ts
import { z } from "zod";
import { ai } from "../genkit";
export const summarizeFlow = ai.defineFlow(
{
name: "summarizeFlow",
inputSchema: z.string(),
outputSchema: z.string(),
},
async (text) => {
const { text: summary } = await ai.generate({
prompt: `Summarize in 2 sentences: ${text}`,
});
return summary;
}
);Test with a mock model plugin:
// src/ai/flows/__tests__/summarize.test.ts
import { describe, it, expect } from "vitest";
import { genkit } from "genkit";
import { z } from "zod";
// Create a test instance with a mock model
function createTestAi(mockResponse: string) {
return genkit({
plugins: [],
model: {
name: "test-model",
generate: async () => ({
candidates: [
{
index: 0,
finishReason: "stop",
message: {
role: "model",
content: [{ text: mockResponse }],
},
},
],
usage: { inputTokens: 10, outputTokens: 5 },
}),
},
});
}
describe("summarizeFlow", () => {
it("returns the model output as the flow result", async () => {
const testAi = createTestAi("This is a two-sentence summary. It covers the main points.");
const summarizeFlowUnderTest = testAi.defineFlow(
{
name: "summarizeFlow",
inputSchema: z.string(),
outputSchema: z.string(),
},
async (text) => {
const { text: summary } = await testAi.generate({
prompt: `Summarize in 2 sentences: ${text}`,
});
return summary;
}
);
const result = await summarizeFlowUnderTest("Long article text here...");
expect(result).toBe(
"This is a two-sentence summary. It covers the main points."
);
});
});A Better Pattern: Inject the AI Instance
For easier testing, inject the ai instance instead of importing it directly:
// src/ai/flows/summarize.ts
import { Genkit, z } from "genkit";
export function createSummarizeFlow(ai: Genkit) {
return ai.defineFlow(
{
name: "summarizeFlow",
inputSchema: z.string(),
outputSchema: z.string(),
},
async (text) => {
const { text: summary } = await ai.generate({
prompt: `Summarize in 2 sentences: ${text}`,
});
return summary;
}
);
}Now tests can inject a test AI instance:
// src/ai/flows/__tests__/summarize.test.ts
import { describe, it, expect, vi } from "vitest";
import { createSummarizeFlow } from "../summarize";
function mockAi(responseText: string) {
return {
defineFlow: (config: any, handler: any) => handler,
generate: vi.fn().mockResolvedValue({ text: responseText }),
} as any;
}
describe("summarizeFlow handler", () => {
it("calls generate with the correct prompt", async () => {
const ai = mockAi("Short summary here.");
const handler = createSummarizeFlow(ai);
const result = await handler("This is the input text");
expect(ai.generate).toHaveBeenCalledWith(
expect.objectContaining({
prompt: expect.stringContaining("This is the input text"),
})
);
expect(result).toBe("Short summary here.");
});
it("propagates errors from the model", async () => {
const ai = {
defineFlow: (_: any, handler: any) => handler,
generate: vi.fn().mockRejectedValue(new Error("Rate limit exceeded")),
} as any;
const handler = createSummarizeFlow(ai);
await expect(handler("any input")).rejects.toThrow("Rate limit exceeded");
});
});Testing Tools
Tools are regular async functions — test them directly without involving Genkit:
// src/ai/tools/weather.ts
import { z } from "zod";
import { ai } from "../genkit";
export const weatherTool = ai.defineTool(
{
name: "getWeather",
description: "Get current weather for a city",
inputSchema: z.object({ city: z.string() }),
outputSchema: z.object({
temperature: z.number(),
condition: z.string(),
}),
},
async ({ city }) => {
const response = await fetch(
`https://api.weather.example.com/current?q=${city}`
);
if (!response.ok) throw new Error(`Weather API error: ${response.status}`);
return response.json();
}
);Test the tool handler:
// src/ai/tools/__tests__/weather.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
// Mock fetch
global.fetch = vi.fn();
describe("getWeather tool", () => {
beforeEach(() => vi.clearAllMocks());
it("returns temperature and condition for valid city", async () => {
vi.mocked(fetch).mockResolvedValue({
ok: true,
json: () =>
Promise.resolve({ temperature: 22, condition: "Sunny" }),
} as Response);
// Test the tool handler by extracting the handler function
// In Genkit, defineTool returns an object with a .run() method
const { weatherTool } = await import("../weather");
const result = await weatherTool({ city: "London" });
expect(result).toEqual({ temperature: 22, condition: "Sunny" });
expect(fetch).toHaveBeenCalledWith(
expect.stringContaining("q=London")
);
});
it("throws when the weather API returns an error", async () => {
vi.mocked(fetch).mockResolvedValue({
ok: false,
status: 503,
} as Response);
const { weatherTool } = await import("../weather");
await expect(weatherTool({ city: "InvalidCity" })).rejects.toThrow(
"Weather API error: 503"
);
});
});Testing Flows That Use Tools
When a flow uses tools, test the tool invocation path:
// src/ai/flows/weatherFlow.ts
export function createWeatherFlow(ai: Genkit, weatherFn: (city: string) => Promise<WeatherData>) {
return ai.defineFlow(
{ name: "weatherFlow", inputSchema: z.string(), outputSchema: z.string() },
async (city) => {
const weather = await weatherFn(city);
const { text } = await ai.generate({
prompt: `The weather in ${city} is ${weather.condition} at ${weather.temperature}°C. Write a friendly one-sentence summary.`,
});
return text;
}
);
}Test:
describe("weatherFlow", () => {
it("fetches weather and generates a summary", async () => {
const mockWeatherFn = vi.fn().mockResolvedValue({
temperature: 18,
condition: "Cloudy",
});
const ai = mockAi("It's a cool, cloudy day in Paris today!");
const handler = createWeatherFlow(ai, mockWeatherFn);
const result = await handler("Paris");
expect(mockWeatherFn).toHaveBeenCalledWith("Paris");
expect(ai.generate).toHaveBeenCalledWith(
expect.objectContaining({
prompt: expect.stringContaining("Paris"),
})
);
expect(result).toBe("It's a cool, cloudy day in Paris today!");
});
});Running Genkit Evaluations
Genkit has a built-in evaluation framework for measuring LLM output quality. Define datasets and metrics:
// src/ai/evals/summarize.eval.ts
import { ai } from "../genkit";
import { createSummarizeFlow } from "../flows/summarize";
const summarizeFlow = createSummarizeFlow(ai);
// Define the evaluation dataset
export const summarizeEvalDataset = [
{
input:
"The Eiffel Tower was built in 1889 for the World's Fair in Paris. It stands 330 meters tall.",
reference:
"The Eiffel Tower, constructed in 1889 for the Paris World's Fair, is a 330-meter tall landmark.",
},
{
input:
"JavaScript was created in 10 days by Brendan Eich in 1995. It was originally called Mocha.",
reference:
"Brendan Eich created JavaScript in just 10 days in 1995, initially naming it Mocha.",
},
];
// Run evaluation
export async function runSummarizeEval() {
const results = await Promise.all(
summarizeEvalDataset.map(async ({ input, reference }) => {
const output = await summarizeFlow(input);
return { input, reference, output };
})
);
return results;
}Add automated quality assertions:
// src/ai/evals/__tests__/summarize.eval.test.ts
import { describe, it, expect } from "vitest";
import { runSummarizeEval } from "../summarize.eval";
describe.skipIf(!process.env.RUN_EVALS)("Summarize flow evaluation", () => {
it("produces summaries within reasonable length", async () => {
const results = await runSummarizeEval();
for (const { input, output } of results) {
// A 2-sentence summary should be shorter than the input
expect(output.length).toBeLessThan(input.length);
// Should produce something non-empty
expect(output.trim().length).toBeGreaterThan(10);
}
}, 60_000);
});Testing with the Genkit Dev UI
During development, use the Genkit Dev UI to test flows interactively:
npx genkit start -- tsx src/index.tsOpen http://localhost:4000 to run flows manually, inspect traces, and view evaluation results. This complements automated tests — use it to debug why a flow returns unexpected output before writing a test to prevent regression.
Integration Testing Against Live Models
// src/ai/flows/__tests__/summarize.integration.test.ts
import { describe, it, expect } from "vitest";
import { ai } from "../../genkit";
import { createSummarizeFlow } from "../summarize";
const LIVE = process.env.GOOGLE_GENAI_API_KEY && process.env.RUN_LIVE_TESTS;
describe.skipIf(!LIVE)("summarizeFlow (live)", () => {
it("produces a shorter summary than the input", async () => {
const flow = createSummarizeFlow(ai);
const input =
"Machine learning is a subset of artificial intelligence that gives systems the ability to automatically learn and improve from experience without being explicitly programmed. It focuses on developing computer programs that can access data and use it to learn for themselves.";
const summary = await flow(input);
expect(summary.length).toBeLessThan(input.length);
expect(summary.trim().length).toBeGreaterThan(20);
}, 30_000);
});What to Test
| Layer | What to Test | How |
|---|---|---|
| Tools | Input validation, external API calls, error handling | Unit test the handler directly |
| Flow handlers | Business logic, prompt construction, output parsing | Inject mock AI instance |
| Flow integration | Tool invocation, model call sequence | Mock individual dependencies |
| Evaluations | Output quality metrics | Run against live model, gate on threshold |
| E2E | Full user scenario through your app | Playwright / integration test |
Genkit's design makes the unit test layer genuinely useful because flows are just functions. Mock the generate call, test your business logic, and reserve live API calls for evaluation tests that run on a schedule. That balance gives you fast feedback in CI without burning API budget on every commit.