Skip to main content
@craft-ts
LearnLearn with EffectAI agentsGuideReference
@craft-ts/core@craft-ts/component@craft-ts/dev-tools@craft-ts/effect@craft-ts/style@craft-ts/style-testing@craft-ts/i18n@craft-ts/i18n-effect@craft-ts/mcp
ExamplesMigrationEffect compatibility & maturityAdopting CraftTS progressivelyPress kitRoadmapBacklog
Beta
  • Learn
  • Learn with Effect
  • AI agents
  • Guide
  • Reference
  • Packages
  • Resources
  • Guide overview
  • Create a project
i.Core concepts
  • The mental model
  • Which primitive should I use?
  • Anatomy of a primitive
  • Generators and yield*
  • Insertions
  • Typed insertion pipes
  • Exceptions as values
ii.Managing state
  • Local state
  • State machines
  • query
  • Mutations
  • queryParams
  • asyncProcess
  • Selecting
  • Reacting to mutations
  • Collections
  • Collection utilities
  • Persistence
  • GlobalPersisterHandler
  • Pagination placeholders
  • Schema validation
iii.Recommended approaches
  • Inject at the point of use
iv.Structuring the app
  • craftService
  • Service scopes
  • Shaping the public API
  • Abstract services
  • App start
  • Lazy services
  • Server functions
  • craftRegisterFor
  • Target wrapper
v.Routing & type-safe DI
  • Setup
  • CLI automation
  • ESLint rules
  • Route providers
  • Route guards
  • Route exception handling
  • Non-blocking navigation
  • Global error component
  • Route load errors
  • Scaling routes
vi.Components & templates
  • Components
  • Activating the style system
  • Typed styles
  • Defining a design system
  • Tokens and variables
  • Global foundation and fonts
  • Pseudo-elements and animations
  • Axes and the matrix
  • Context obligations
  • Testing visual states
  • Text contrast
  • Visual attestation
  • Template obligations
  • Typed CSS variables
  • Fine-grained reactivity
  • Progressive rendering
  • Directives and .pipe(...)
  • settledValue & pendingNode
  • Accessibility
  • Customization
  • Content projection
  • Styling a component
  • Template migrator
vii.Forms
  • Overview
  • Validators
  • Submitting
  • Nested forms
  • Exception handling
  • Complete examples
viii.Testing
  • Testing services
  • Testing components
  • Type-level tests
  • Browser boundaries
  • Page stability
  • Architecture rules
    • Overview
    • Declarative baseline
    • Unique identities
    • HTTP endpoint ownership
    • Computed purity
    • Dependency cycles
    • Mutation reactions
    • Route DI proofs
    • Route component files
    • Path boundaries
    • Exclusive links
    • Persisted identities
    • insertSelect keys
    • craftEffect and network
    • craftEffect and imperative sync
    • Interactive element names
    • Metric thresholds
    • Server-state loaders
    • Primitive loader requirements
  • Extensible architecture graph
  • Graph insights
  • Craft graph vs Nx
ix.Reactivity utilities
  • craftComputed
  • craftEffect
  • craftMethod
  • source$
  • on$
  • fromEventToSource$
  • sourceFromEvent
  • afterRecomputation
x.Internationalisation
  • Type-safe i18n
  • The catalogue
  • Tokens
  • The runtime
  • With Effect
xi.Deployment (experimental)
  • Overview
  • Manifest reference
  • Diagnostics
  • Providers
  • Alchemy provider
xii.Going further
  • SSR and hydration
  • Effect integration
  • Program operators
  • Pattern matching
  • Observability
  • AI agents
  • Temporal runtime
  1. Testing
  2. Testing services

Testing services#

Most test setups let you forget a dependency and find out at runtime. This one inverts it: you supply a register covering the service's whole dependency graph, and the compiler refuses to run the test until every node is accounted for.

Use it for any craftService. Use boundaryOnly when the test should stay close to reality — it keeps the real graph and only lets you replace browser boundaries.

The register is as small as your yields

A service that yielded one property needs one property mocked. Precise yields are what keep these tests short — see Shaping the public API.

Import#

typescript
import {
  setupCraftServiceTestingByRegister,
} from '@craft-ts/core';

Introduction#

setupCraftServiceTestingByRegister is the exhaustive testing utility for the craftService graph.

Instead of providing only the overrides you care about, you provide a full typed register where each service is marked as:

  • real

  • provided by its raw provideX(...)

  • mocked with a raw object

  • or pruned with 'notReached'

This is useful when you want explicit control over every node in the service graph.

For tests that should stay close to reality, use boundaryOnly. It keeps the application graph real by default and only lets you decide the services marked with browserBoundary: true.

Register Workflow#

The intended workflow is:

  1. start from the full dependency graph of the SUT

  2. fill each key with a real provider, 'real', a mock object, or 'notReached'

  3. pass the register to await setupCraftServiceTestingByRegister(...)

Basic Example#

typescript
import {
  craftService,
  craftUse,
  setupCraftServiceTestingByRegister,
  state,
  craftExpose,
} from '@craft-ts/core';
import { vi } from 'vitest';

const { Counter } = craftService(
  { name: 'Counter', providedIn: 'global' },
  function* () {
    yield* state('counter', 10, ({ update }) => ({
      increment: () => update((value) => value + 1),
    }));
  },
);

const { CounterConsumer, provideCounterConsumer } = craftService(
  { name: 'CounterConsumer', providedIn: 'toProvide' },
  function* () {
    const { counter } = yield* Counter();

    yield* craftExpose('read', function* () {
      return yield* counter();
    });
    yield* craftExpose('increment', function* () {
      return yield* counter.increment();
    });
  },
);

const { sut, mocks } = await setupCraftServiceTestingByRegister(
  CounterConsumer,
  {
    CounterConsumer: provideCounterConsumer(),
    Counter: {
      // A mock of the exposed `counter` state: callable, with its method.
      counter: Object.assign(vi.fn(() => 41), { increment: vi.fn() }),
    },
  },
);

expect(craftUse(sut.read())).toBe(41);
craftUse(sut.increment());
expect(mocks.Counter.counter.increment).toHaveBeenCalledTimes(1);

Register Semantics#

'real'#

Use 'real' for reachable non-provider scopes such as global or function.

typescript
await setupCraftServiceTestingByRegister(CounterConsumer, {
  CounterConsumer: provideCounterConsumer(),
  Counter: 'real',
});

Raw provider#

Use the provider returned by provideX(...) for toProvide or manuallyProvidedAtRoot services.

typescript
await setupCraftServiceTestingByRegister(RootCounter, {
  RootCounter: provideRootCounter(),
  ParentCounter: provideParentCounter(),
  ChildCounter: provideChildCounter(),
});

Raw mock object#

Use a plain object when you want to override the public service shape.

typescript
await setupCraftServiceTestingByRegister(CounterConsumer, {
  CounterConsumer: provideCounterConsumer(),
  Counter: {
    $self: vi.fn(() => 12),
    increment: vi.fn(),
  },
});

'notReached'#

Use 'notReached' only when the service is on a branch fully pruned by an ancestor mock.

typescript
await setupCraftServiceTestingByRegister(RootCounter, {
  RootCounter: provideRootCounter(),
  ParentCounter: {
    incrementParent: vi.fn(),
  },
  ChildCounter: 'notReached',
});

Return Value#

The function resolves to:

  • sut: the resolved service under test

  • mocks: only the services that were actually mocked in the register

Entries marked as 'real', 'notReached', or provided through raw providers are not exposed in mocks.

App Start Hooks#

Reachable real services declared with appStart: true must be acknowledged explicitly.

typescript
const { sut } = await setupCraftServiceTestingByRegister(
  Dashboard,
  {
    Dashboard: provideDashboard(),
    AuthSession: 'real',
    Analytics: 'real',
  },
  {
    appStart: {
      AuthSession: 'run',
      Analytics: 'ignore',
    },
  },
);

'run' injects the real service and awaits its onAppStart(...) hook. 'ignore' documents that the test intentionally skips it. Mocked services and 'notReached' branches do not require appStart entries.

Boundary-Only Mode#

setupCraftServiceTestingByRegister.boundaryOnly(...) is the recommended mode when the test should mock only browser or platform edges.

typescript
const { sut, mocks } = await setupCraftServiceTestingByRegister.boundaryOnly(
  Dashboard,
  {
    toProvideRegister: {
      Dashboard: provideDashboard(),
      FeatureConfig: provideFeatureConfig({ env: 'test' }),
    },
    boundaryRegister: {
      LocalStorageService: {
        getItem: vi.fn(() => 'cached'),
      },
      ConsoleService: 'real',
    },
    appStart: {
      AuthSession: 'run',
    },
  },
);
  • toProvideRegister contains real providers required by reachable services.

  • boundaryRegister contains the explicit decision for each reachable browser boundary.

  • non-boundary services cannot be mocked in this mode.

  • descendants of a mocked boundary are pruned and do not need entries.

The helper never decides automatically from the test environment. Use 'real' when the real boundary is appropriate, and provide a mock when the test needs deterministic platform behavior.

Alias#

setupTestingService is a backward-compatible alias of setupCraftServiceTestingByRegister.

See Also#

  • craftService

  • Architecture rules — constraints on the whole app graph

←Complete examplesTesting components→

On this page

  • Import
  • Introduction
  • Register Workflow
  • Basic Example
  • Register Semantics
  • 'real'
  • Raw provider
  • Raw mock object
  • 'notReached'
  • Return Value
  • App Start Hooks
  • Boundary-Only Mode
  • Alias
  • See Also

@craft-ts · MIT licensed

GitHubnpm

Search