> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useshipd.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Test flags locally and in unit tests

> Force flag values in unit tests with a bootstrap snapshot, run without network access, and develop against a separate environment.

## Overview

This page shows how to control flag values in unit tests, how the SDK behaves when you develop without network access, and how to keep local work out of production with a `development` environment. None of these patterns need a test-only package. They use regular [TypeScript client options](/reference/sdk#createclient) or [Go options](/reference/go-sdk#new).

## Force values in unit tests

In TypeScript, pass a `bootstrap` snapshot and a stub `fetch` that always answers `304 Not Modified`; the client serves that snapshot immediately. The Go helper below serves test values as defaults and uses a stub HTTP client so no request reaches the network, which is enough for application branching tests. To test Go targeting rules, pass a snapshot as `Bootstrap`, as shown [below](#test-targeting-rules).

```ts TypeScript theme={null}
// test/shipd.ts
import { createClient, SNAPSHOT_SCHEMA_VERSION, type FlagSnapshot } from "shipdit";
import type { FlagDefinitions } from "../src/generated/flags";

type Values = Partial<Record<keyof FlagDefinitions, unknown>>;

/** Build a snapshot where every listed flag serves the given value. */
function snapshotFor(values: Values): FlagSnapshot {
  const flags: FlagSnapshot["flags"] = {};
  for (const [key, value] of Object.entries(values)) {
    const type =
      typeof value === "boolean"
        ? "boolean"
        : typeof value === "number"
          ? "number"
          : typeof value === "string"
            ? "variant"
            : "json";
    flags[key] = {
      key,
      type,
      enabled: true,
      version: 1,
      default: value,
      rules: [],
      fallthrough: { kind: type, value },
      ...(type === "variant" ? { variants: [value as string] } : {}),
    } as FlagSnapshot["flags"][string];
  }
  return {
    schemaVersion: SNAPSHOT_SCHEMA_VERSION,
    projectId: "proj_test",
    environmentId: "env_test",
    environmentKey: "test",
    version: 1,
    generatedAt: new Date(0).toISOString(),
    flags,
  };
}

export function createTestClient(values: Values = {}) {
  return createClient<FlagDefinitions>({
    sdkKey: "shipd_sdk_server_test",
    bootstrap: snapshotFor(values),
    refreshIntervalMs: 0, // fetch once, then stop
    events: false, // don't send diagnostic events
    fetch: async () => new Response(null, { status: 304 }),
    defaults: {
      "new-checkout": false,
      "search-variant": "control",
      "max-retries": 3,
      "checkout-payload": { mode: "strict", showPromo: true },
    },
  });
}
```

```go Go theme={null}
// testclient_test.go
package checkout

import (
	"errors"
	"net/http"
	"testing"

	shipd "github.com/alasti-company/useshipd.com/sdks/go"
)

type offlineHTTPClient struct{}

func (offlineHTTPClient) Do(*http.Request) (*http.Response, error) {
	return nil, errors.New("offline test")
}

func createTestClient(t *testing.T, enabled bool) *shipd.Client {
	t.Helper()
	client, err := shipd.New(shipd.Options{
		SDKKey:         "shipd_sdk_server_test",
		Defaults:       map[string]any{"new-checkout": enabled},
		DisablePolling: true, DisableEvents: true,
		HTTPClient: offlineHTTPClient{},
	})
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = client.Close() })
	return client
}
```

Flags you don't list aren't in the snapshot, so they return their `defaults`.

```ts TypeScript theme={null}
// src/checkout.ts
import type { ShipdClient } from "shipdit";
import type { FlagDefinitions } from "./generated/flags";

export function pickCheckout(shipd: ShipdClient<FlagDefinitions>, userId: string) {
  return shipd.isEnabled("new-checkout", { userId }) ? "new" : "legacy";
}
```

```go Go theme={null}
// checkout.go
package checkout

import shipd "github.com/alasti-company/useshipd.com/sdks/go"

func pickCheckout(client *shipd.Client, userID string) string {
	if client.IsEnabled("new-checkout", shipd.EvaluationContext{UserID: userID}) {
		return "new"
	}
	return "legacy"
}
```

```ts TypeScript theme={null}
// src/checkout.test.ts
import { afterEach, expect, it } from "vitest";
import { createTestClient } from "../test/shipd";
import { pickCheckout } from "./checkout";

let shipd: ReturnType<typeof createTestClient>;

afterEach(() => shipd.close());

it("uses the new checkout when the flag is on", () => {
  shipd = createTestClient({ "new-checkout": true });
  expect(pickCheckout(shipd, "usr_8f2k19")).toBe("new");
});

it("falls back to the legacy checkout", () => {
  shipd = createTestClient({ "new-checkout": false });
  expect(pickCheckout(shipd, "usr_8f2k19")).toBe("legacy");
});
```

```go Go theme={null}
// checkout_test.go
package checkout

import "testing"

func TestPickCheckout(t *testing.T) {
	for _, test := range []struct {
		enabled bool
		want    string
	}{
		{true, "new"}, {false, "legacy"},
	} {
		client := createTestClient(t, test.enabled)
		if got := pickCheckout(client, "usr_8f2k19"); got != test.want {
			t.Errorf("enabled=%v: got %q, want %q", test.enabled, got, test.want)
		}
	}
}
```

Passing the client into the code under test (instead of importing a module-level singleton) keeps each test independent. If your code imports the singleton from `src/lib/shipd.ts`, mock that module with your test runner (`vi.mock` or `jest.mock`) and return a test client.

### Test targeting rules

To test a targeting rule itself, add `rules` to a flag in the snapshot and pass context to the evaluation. In TypeScript, pass it as `bootstrap`; the snapshot types are in `FlagSnapshot`, exported from `shipdit`. In Go, pass the snapshot JSON as `Bootstrap`:

```go targeting_test.go theme={null}
package checkout

import (
	shipd "github.com/alasti-company/useshipd.com/sdks/go"
	"testing"
)

func TestTargeting(t *testing.T) {
	const snapshot = `{"schemaVersion":1,"projectId":"p","environmentId":"e","environmentKey":"test",
  "version":1,"generatedAt":"2026-10-10T00:00:00Z","flags":{"new-checkout":{"key":"new-checkout",
  "type":"boolean","enabled":true,"default":false,"fallthrough":{"kind":"boolean","value":false},
  "rules":[{"id":"pro","conditions":[{"kind":"attribute","attribute":"plan","operator":"eq","value":"pro"}],
  "serve":{"kind":"boolean","value":true}}]}}}`

	client, err := shipd.New(shipd.Options{
		SDKKey: "shipd_sdk_server_test", Bootstrap: []byte(snapshot),
		DisablePolling: true, DisableEvents: true, HTTPClient: offlineHTTPClient{},
	})
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = client.Close() })
	if !client.IsEnabled("new-checkout", shipd.EvaluationContext{Attributes: map[string]any{"plan": "pro"}}) {
		t.Fatal("pro targeting rule did not match")
	}
}
```

With an offline client, `Ready` returns the fetch error immediately instead of waiting for its deadline, or `nil` when `Bootstrap` is set.

### React components

Render with a test client in `ShipdProvider`. Because `bootstrap` is set, `useReady()` is `true` after the first effect.

```tsx src/components/CheckoutNav.test.tsx theme={null}
import { render, screen } from "@testing-library/react";
import { afterEach, expect, it } from "vitest";
import { ShipdProvider } from "../lib/shipd-react";
import { createTestClient } from "../../test/shipd";
import { CheckoutNav } from "./CheckoutNav";

let client: ReturnType<typeof createTestClient>;

afterEach(() => client.close());

it("links to the new checkout", async () => {
  client = createTestClient({ "new-checkout": true });
  render(
    <ShipdProvider client={client}>
      <CheckoutNav />
    </ShipdProvider>,
  );
  const link = await screen.findByRole("link", { name: "Checkout" });
  expect(link.getAttribute("href")).toBe("/checkout/new");
});
```

## Serve only defaults

Sometimes you want a client that never contacts shipd, for example when `.env` has no SDK key. `createClient` throws if `sdkKey` is empty, so there's no key-less mode. Instead, create a client with a placeholder key and a `fetch` that always fails. Every check returns its default.

```ts TypeScript theme={null}
// src/lib/shipd.ts
import { createClient } from "shipdit";
import type { FlagDefinitions } from "../generated/flags";

const defaults = {
  "new-checkout": false,
  "search-variant": "control",
  "max-retries": 3,
  "checkout-payload": { mode: "strict", showPromo: true },
} as const;

const sdkKey = process.env.SHIPD_SDK_KEY?.trim();

export const shipd = sdkKey
  ? createClient<FlagDefinitions>({
      sdkKey,
      defaults,
    })
  : createClient<FlagDefinitions>({
      sdkKey: "local",
      defaults,
      refreshIntervalMs: 0,
      readyTimeoutMs: 0,
      events: false,
      onError: () => {}, // the stubbed fetch always fails
      fetch: async () => new Response(null, { status: 503 }),
    });
```

```go Go theme={null}
// main.go
package main

import (
	"errors"
	shipd "github.com/alasti-company/useshipd.com/sdks/go"
	"log"
	"net/http"
)

type offlineHTTPClient struct{}

func (offlineHTTPClient) Do(*http.Request) (*http.Response, error) {
	return nil, errors.New("offline")
}

func main() {
	client, err := shipd.New(shipd.Options{
		SDKKey: "local",
		Defaults: map[string]any{
			"new-checkout": false, "search-variant": "control", "max-retries": 3,
			"checkout-payload": map[string]any{"mode": "strict", "showPromo": true},
		},
		DisablePolling: true, DisableEvents: true,
		HTTPClient: offlineHTTPClient{},
	})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()
	log.Printf("checkout: %v", client.IsEnabled("new-checkout", shipd.EvaluationContext{}))
}
```

The shipd dashboard uses this pattern so it boots from an empty `.env`. In a browser app, also pass `persist: false` so no snapshot is cached.

## Develop without network access

You don't need special configuration to work offline:

* TypeScript `ready()` resolves as soon as the first fetch fails, or after `readyTimeoutMs` (5 seconds by default) if the request hangs. It never rejects.
* Every check returns its default until a snapshot loads.
* The browser client loads the last snapshot it saved in `localStorage`, so you keep seeing the values from your last online session.
* Polling continues, so the client picks up the snapshot when the network returns.

Go `Ready(ctx)` returns a request or context error when startup fails; handle it and continue with defaults if appropriate. Go keeps snapshots in memory and does not persist them to disk. See [Local evaluation](/concepts/local-evaluation) for the full failure model.

## Use a development environment

Keep local work away from production flags with a separate environment and SDK key.

**1. Create the environment**

In the dashboard, open your project's **Environments** page and click **Create environment**. Name it `Development` with the slug `development`.

**2. Create a key for it**

Select the `development` environment, go to **SDK keys**, and click **Create SDK key**. Keys are created for the active environment.

**3. Point your local env at it**

```bash .env.local theme={null}
SHIPD_SDK_KEY=shipd_sdk_server_…   # development key
```

**4. Push your flag definitions**

A management API key is bound to one environment. Run `shipd login`, approve the `development` environment in the browser, then push:

```bash theme={null}
shipd login
shipd push
```

You can now add rules, run rollouts, and change the **Fallthrough** in `development` without affecting production. See [Environments](/concepts/environments).

## Next

* [Local evaluation](/concepts/local-evaluation)
* [Sync flags with the CLI](/config-as-code/sync)
* [SDK reference](/reference/sdk)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.