Server-Based Testing

Integration test workflows against a running server when you need to test the full HTTP layer.

The Vitest plugin runs workflows entirely in-process and is the recommended approach for most testing scenarios. However, there are cases where you may want to test against a running server:

  • Testing the full HTTP layer (middleware, authentication, request handling)
  • Reproducing behavior that only occurs in a specific framework's runtime (e.g. Next.js, Nitro)
  • Testing webhook endpoints that receive real HTTP requests

This guide shows how to set up integration tests that spawn a dev server as a sidecar process.

Vitest Configuration

Create a Vitest config with the workflow() Vite plugin for code transforms and a globalSetup script that manages the server lifecycle:

vitest.server.config.ts
import { defineConfig } from "vitest/config";
import { workflow } from "workflow/vite"; 

export default defineConfig({
  plugins: [workflow()], 
  test: {
    include: ["**/*.server.test.ts"],
    testTimeout: 60_000,
    globalSetup: "./vitest.server.setup.ts", 
    env: {
      WORKFLOW_LOCAL_BASE_URL: "http://localhost:4000", 
    },
  },
});

Note the import path: workflow/vite (not workflow/vitest). The Vite plugin handles code transforms but does not set up in-process execution. The server handles workflow execution instead.

Global Setup Script

The globalSetup script starts a dev server before tests run and tears it down afterwards. This example uses Nitro, but you can use any server framework that supports the workflow runtime.

vitest.server.setup.ts
import { spawn } from "node:child_process";
import { setTimeout as delay } from "node:timers/promises";
import type { ChildProcess } from "node:child_process";

let server: ChildProcess | null = null;
const PORT = "4000";

export async function setup() { 
  console.log("Starting server for workflow execution...");

  server = spawn("npx", ["nitro", "dev", "--port", PORT], {
    stdio: "pipe",
    detached: false,
    env: process.env,
  });

  // Wait for the server to be ready
  const ready = await new Promise<boolean>((resolve) => {
    const timeout = setTimeout(() => resolve(false), 15_000);

    server?.stdout?.on("data", (data) => {
      const output = data.toString();
      console.log("[server]", output);
      if (output.includes("listening") || output.includes("ready")) {
        clearTimeout(timeout);
        resolve(true);
      }
    });

    server?.stderr?.on("data", (data) => {
      console.error("[server]", data.toString());
    });

    server?.on("error", (error) => {
      console.error("Failed to start server:", error);
      clearTimeout(timeout);
      resolve(false);
    });
  });

  if (!ready) {
    throw new Error("Server failed to start within 15 seconds");
  }

  await delay(2_000); // Allow full initialization

  // Point the workflow runtime at the local server
  process.env.WORKFLOW_LOCAL_BASE_URL = `http://localhost:${PORT}`; 

  console.log("Server ready for workflow execution");
}

export async function teardown() { 
  if (server) {
    console.log("Stopping server...");
    server.kill("SIGTERM");
    await delay(1_000);
    if (!server.killed) {
      server.kill("SIGKILL");
    }
  }
}

The setup script sets WORKFLOW_LOCAL_BASE_URL so the workflow runtime sends step execution requests to the running server.

You can use any server framework that supports the workflow runtime. The example above uses Nitro, but you could also use Next.js, Hono, or any other supported server.

Writing Tests

Tests are written the same way as in-process integration tests:

workflows/calculate.server.test.ts
import { describe, it, expect } from "vitest";
import { start } from "workflow/api";
import { calculateWorkflow } from "./calculate";

describe("calculateWorkflow", () => {
  it("should compute the correct result", async () => {
    const run = await start(calculateWorkflow, [2, 7]);
    const result = await run.returnValue;

    expect(result).toEqual({
      sum: 9,
      product: 14,
      combined: 23,
    });
  });
});

Running Tests

Add a script to your package.json:

package.json
{
  "scripts": {
    "test": "vitest",
    "test:server": "vitest --config vitest.server.config.ts"
  }
}

When to Use This Approach

ScenarioRecommended approach
Testing workflow logic, steps, hooks, retriesIn-process plugin
Testing HTTP middleware or authenticationServer-based
Testing webhook endpoints with real HTTPServer-based
CI/CD pipeline testingIn-process plugin
Reproducing framework-specific behaviorServer-based

On this page

GitHubEdit this page on GitHub