Skip to main content

Testing

5 min read

Assertions

The test system provides built-in assertion methods and history tracking for verifying system behavior.


Assertion Methods

createTestSystem returns a test system with four assertion helpers:

MethodDescription
assertRequirement(type)Assert that a requirement of the given type was created
assertResolverCalled(type, times?)Assert a resolver was called, optionally a specific number of times
assertFactSet(key, value?)Assert a fact was set, optionally to a specific value
assertFactChanges(key, times)Assert a fact was changed exactly N times

All assertion methods throw descriptive errors on failure, making test output easy to read.


assertRequirement

Verify that a constraint produced a specific requirement type:

import { createTestSystem } from '@directive-run/core/testing';

test('constraint creates requirement', async () => {
  const system = createTestSystem({ modules: { user: userModule } });
  system.start();

  // Satisfy the constraint's `when` condition
  system.facts.user.userId = 123;
  await system.waitForIdle();

  // Throws a descriptive error if no FETCH_USER requirement was created
  system.assertRequirement('FETCH_USER');
});

For more detailed checks, inspect the allRequirements array directly:

test('requirement has correct payload', async () => {
  const system = createTestSystem({ modules: { user: userModule } });
  system.start();

  system.facts.user.userId = 123;
  await system.waitForIdle();

  // Dig into the full requirements array for detailed payload checks
  expect(system.allRequirements).toContainEqual(
    expect.objectContaining({
      requirement: expect.objectContaining({
        type: 'FETCH_USER',
        userId: 123,
      }),
    })
  );
});

assertResolverCalled

Verify that a mock resolver was invoked:

test('resolver was called', async () => {
  const system = createTestSystem({
    modules: { app: myModule },
    mocks: {
      resolvers: {
        FETCH_DATA: {
          resolve: (req, context) => {
            context.facts.app_data = 'loaded';
          },
        },
      },
    },
  });
  system.start();

  // Trigger the constraint -> requirement -> resolver chain
  system.facts.app.dataId = 1;
  await system.waitForIdle();

  // Was the resolver invoked at all?
  system.assertResolverCalled('FETCH_DATA');

  // Was it called exactly once? (catches accidental duplicates)
  system.assertResolverCalled('FETCH_DATA', 1);
});

You can also inspect the raw call history via resolverCalls:

// Access the raw call history for fine-grained inspection
const calls = system.resolverCalls.get('FETCH_DATA');
expect(calls).toHaveLength(1);
expect(calls[0]).toMatchObject({ type: 'FETCH_DATA', dataId: 1 });

assertFactSet

Verify that a fact was set during the test. The key is the fact name without the namespace prefix:

test('fact was set', async () => {
  const system = createTestSystem({ modules: { app: myModule } });
  system.start();

  // Mutate a fact
  system.facts.app.count = 5;

  // Verify the fact was written to (regardless of value)
  system.assertFactSet('count');

  // Verify it was set to a specific value
  system.assertFactSet('count', 5);
});

assertFactChanges

Verify the exact number of times a fact was changed:

test('fact changed exactly 3 times', () => {
  const system = createTestSystem({ modules: { app: myModule } });
  system.start();

  // Mutate the same fact three times
  system.facts.app.count = 1;
  system.facts.app.count = 2;
  system.facts.app.count = 3;

  // Confirm the exact number of mutations (catches extra writes)
  system.assertFactChanges('count', 3);
});

Fact History Tracking

The test system records every fact change with full context. Use getFactsHistory() to access the complete change log:

test('inspect fact change history', () => {
  const system = createTestSystem({ modules: { test: myModule } });
  system.start();

  // Make several fact changes across different keys
  system.facts.test.value = 10;
  system.facts.test.name = 'hello';
  system.facts.test.value = 20;

  // Every mutation is recorded in order
  const history = system.getFactsHistory();
  expect(history).toHaveLength(3);

  // Each record captures the full context of the change
  expect(history[0]).toMatchObject({
    key: 'value',           // Fact name (without namespace)
    fullKey: 'test::value',  // Full key with namespace prefix
    namespace: 'test',      // Module namespace
    previousValue: 0,       // Value before the change
    newValue: 10,           // Value after the change
  });
});

Resetting History

Reset the fact history mid-test to focus on specific operations:

test('track changes after setup', () => {
  const system = createTestSystem({ modules: { app: myModule } });
  system.start();

  // Setup phase – these changes are not what we want to test
  system.facts.app.count = 0;
  system.facts.app.name = 'initial';

  // Discard setup noise so only the action under test is tracked
  system.resetFactsHistory();

  // This is the mutation we actually care about
  system.facts.app.count = 42;

  // History now only contains the post-reset change
  const history = system.getFactsHistory();
  expect(history).toHaveLength(1);
  expect(history[0].newValue).toBe(42);
});

Event History

The test system also tracks dispatched events via the eventHistory array:

test('events are tracked', async () => {
  const system = createTestSystem({ modules: { app: myModule } });
  system.start();

  // Dispatch two events into the system
  system.dispatch({ type: 'INCREMENT' });
  system.dispatch({ type: 'INCREMENT' });

  // eventHistory records every dispatched event in order
  expect(system.eventHistory).toHaveLength(2);
  expect(system.eventHistory[0]).toMatchObject({ type: 'INCREMENT' });
});

Combining Assertions

Use multiple assertions together for thorough tests:

test('full resolver lifecycle', async () => {
  const system = createTestSystem({
    modules: { user: userModule },
    mocks: {
      resolvers: {
        FETCH_USER: {
          resolve: (req, context) => {
            context.facts.user_name = 'John';
          },
        },
      },
    },
  });
  system.start();

  // Kick off the full lifecycle by changing a fact
  system.facts.user.userId = 123;
  await system.waitForIdle();

  // Verify every link in the chain:
  // 1. Constraint produced a requirement
  system.assertRequirement('FETCH_USER');

  // 2. Resolver ran exactly once
  system.assertResolverCalled('FETCH_USER', 1);

  // 3. Resolver wrote the expected fact
  system.assertFactSet('name', 'John');

  // 4. The triggering fact was only set once
  system.assertFactChanges('userId', 1);
});

Next Steps

Previous
Fake Timers

We care about your data. We'll never share your email.

Powered by Directive. This signup uses a Directive module with facts, derivations, constraints, and resolvers – zero useState, zero useEffect. Read how it works

Directive - Constraint-Driven State Management for TypeScript