Skip to content

Effect Testing ​

The reacton_test package provides two tools for testing side effects and tracking reacton interactions: EffectTracker and MockReacton.

EffectTracker ​

EffectTracker records invocations of effects during tests. Use it to verify that effects ran, how many times they ran, and in what order.

API ​

Property / MethodTypeDescription
record(name, [metadata])voidRecord an invocation with an optional metadata map
invocationsList<EffectInvocation>All recorded invocations (unmodifiable)
totalCallCountintTotal number of invocations across all effects
callCount(name)intNumber of times a specific effect was called
wasCalled(name)boolWhether a specific effect was ever called
wasAnyCalledboolWhether any effect was called
invocationsOf(name)List<EffectInvocation>All invocations for a specific effect
reset()voidClear all recorded invocations

EffectInvocation ​

Each recorded invocation is an EffectInvocation:

FieldTypeDescription
nameStringThe name passed to record()
timestampDateTimeWhen the invocation was recorded
metadataMap<String, dynamic>?Optional metadata

Basic Usage ​

dart
import 'package:flutter_test/flutter_test.dart';
import 'package:reacton_test/reacton_test.dart';

final counterReacton = reacton(0, name: 'counter');

void main() {
  test('effect runs when dependency changes', () {
    final store = TestReactonStore();
    final tracker = EffectTracker();

    final dispose = store.registerEffect(createEffect((read) {
      tracker.record('logger');
      final count = read(counterReacton);
      // Side effect: log the count
      return null;
    }));

    // Effect runs once on registration (initial)
    expect(tracker.callCount('logger'), 1);

    // Change the dependency
    store.set(counterReacton, 1);
    expect(tracker.callCount('logger'), 2);

    store.set(counterReacton, 2);
    expect(tracker.callCount('logger'), 3);

    dispose();
  });
}

Tracking with Metadata ​

Pass metadata to capture additional context about each invocation:

dart
test('effect records metadata', () {
  final store = TestReactonStore();
  final tracker = EffectTracker();

  store.registerEffect(createEffect((read) {
    final count = read(counterReacton);
    tracker.record('counter_effect', {'value': count});
    return null;
  }));

  store.set(counterReacton, 5);

  final invocations = tracker.invocationsOf('counter_effect');
  expect(invocations.length, 2); // initial + update
  expect(invocations.last.metadata?['value'], 5);
});

Verifying Effect Not Called ​

dart
test('effect does not run for unrelated changes', () {
  final store = TestReactonStore();
  final tracker = EffectTracker();

  final nameReacton = reacton('', name: 'name');

  store.registerEffect(createEffect((read) {
    tracker.record('name_effect');
    read(nameReacton); // Only depends on nameReacton
    return null;
  }));

  tracker.reset(); // Clear initial invocation

  // Change a different reacton
  store.set(counterReacton, 42);

  expect(tracker.wasCalled('name_effect'), isFalse);
});

Testing Effect Cleanup ​

Effects can return a cleanup function that runs before the next execution or on disposal:

dart
test('effect cleanup runs', () {
  final store = TestReactonStore();
  final tracker = EffectTracker();

  final dispose = store.registerEffect(createEffect((read) {
    tracker.record('setup');
    read(counterReacton);

    return () {
      tracker.record('cleanup');
    };
  }));

  // Setup runs on registration
  expect(tracker.callCount('setup'), 1);
  expect(tracker.callCount('cleanup'), 0);

  // Change triggers cleanup then re-setup
  store.set(counterReacton, 1);
  expect(tracker.callCount('cleanup'), 1);
  expect(tracker.callCount('setup'), 2);

  // Disposal triggers cleanup
  dispose();
  expect(tracker.callCount('cleanup'), 2);
});

Resetting Between Tests ​

Call tracker.reset() to clear all invocations:

dart
late EffectTracker tracker;

setUp(() {
  tracker = EffectTracker();
});

// Or reset within a test:
test('multi-phase test', () {
  // ... phase 1 ...
  tracker.reset();
  // ... phase 2 starts fresh ...
});

MockReacton ​

MockReacton wraps a real reacton and tracks how many times it was read and written. It also keeps a history of all values.

API ​

Property / MethodTypeDescription
reactonReactonBase<T>The underlying reacton being mocked
initialValueTThe initial value
readCountintNumber of recorded reads
writeCountintNumber of recorded writes
valueHistoryList<T>All values in order (starts with initialValue)
lastValueTThe most recent value
recordRead()voidIncrement the read counter
recordWrite(value)voidIncrement the write counter and add to history
reset()voidReset all counters and history

Basic Usage ​

dart
test('tracks read and write counts', () {
  final mock = MockReacton(counterReacton, initialValue: 0);
  final store = TestReactonStore();

  // Simulate writes
  store.forceSet(mock.reacton, 10);
  mock.recordWrite(10);

  store.forceSet(mock.reacton, 20);
  mock.recordWrite(20);

  expect(mock.writeCount, 2);
  expect(mock.valueHistory, [0, 10, 20]);
  expect(mock.lastValue, 20);
});

Tracking Value History ​

dart
test('value history captures all changes', () {
  final mock = MockReacton(counterReacton, initialValue: 0);

  mock.recordWrite(1);
  mock.recordWrite(2);
  mock.recordWrite(3);

  expect(mock.valueHistory, [0, 1, 2, 3]);
  expect(mock.lastValue, 3);
  expect(mock.writeCount, 3);
});

Reset ​

dart
test('reset clears all tracking', () {
  final mock = MockReacton(counterReacton, initialValue: 0);

  mock.recordRead();
  mock.recordRead();
  mock.recordWrite(5);

  expect(mock.readCount, 2);
  expect(mock.writeCount, 1);

  mock.reset();

  expect(mock.readCount, 0);
  expect(mock.writeCount, 0);
  expect(mock.valueHistory, [0]); // Reset to initial value
});

Combining EffectTracker and MockReacton ​

Use both together for comprehensive side-effect verification:

dart
test('effect reads counter and logs', () {
  final store = TestReactonStore();
  final tracker = EffectTracker();
  final mock = MockReacton(counterReacton, initialValue: 0);

  store.registerEffect(createEffect((read) {
    mock.recordRead();
    final count = read(counterReacton);
    tracker.record('log', {'count': count});
    return null;
  }));

  store.set(counterReacton, 42);
  mock.recordWrite(42);

  expect(tracker.callCount('log'), 2); // initial + update
  expect(mock.readCount, 2);           // read during each run
  expect(mock.writeCount, 1);          // one explicit write

  final lastInvocation = tracker.invocationsOf('log').last;
  expect(lastInvocation.metadata?['count'], 42);
});

What's Next ​

Released under the MIT License.