Testing¶
This section describes how to effectively test SimpleAppState and RefAppState.
Overview¶
Testing in SimpleAppState and RefAppState is based on observing state changes.
A dedicated debug listener allows developers to:
Track every state mutation
Observe old and new values
Inspect behavior without modifying application logic
Testing can be done in two modes: value-based testing using StateSlot and DebugListener, and reference-based testing using RefSlot and RefDebugListener.
Debug Listener¶
SimpleAppState provides a debug listener intended for development and testing.
void setDebugListener(DebugListener? listener) {
_debugListener = listener;
}
The debug listener is notified whenever any value changes, including intermediate updates inside batch operations.
DebugListener Signature¶
typedef DebugListener =
void Function(StateSlot key, dynamic oldValue, dynamic newValue);
Each callback provides:
The StateSlot that changed
The previous value
The new value
This makes every state transition observable.
Using StateSlot as a Test Key¶
StateSlot acts as a stable observation key.
Each slot has:
A unique, stable name
A reference to its owning state
Strong equality and hashing guarantees
In Flutter applications, widgets subscribe explicitly to slots. As a result, observing slot changes is often sufficient to reason about UI behavior.
Example¶
// int app_state.dart
final appState = SimpleAppState();
final count = appState.slot<int>('count', initial: 0);
//////////////////
// in main.dart //
if (kDebugMode) {
appState.setDebugListener((slot, oldV, newV) {
debugPrint(
"Changed Slot:${slot.name}, Value changed from:$oldV, to:$newV",
);
});
}
//////////////////
// int runtime code
count.set(1);
count.update((v) => (v ?? 0) + 1);
The collected log provides a complete trace of state transitions.
RefDebugListener (Reference Debugging)¶
When using RefAppState and RefSlot, values are stored and updated by reference, not by deep copy.
In this model, the standard DebugListener is not sufficient, because there is no meaningful old vs new snapshot for mutable objects.
For this purpose, RefAppState provides a dedicated reference debugger.
typedef RefDebugListener = void Function(RefSlot key, dynamic ref);
The listener receives:
The RefSlot that changed
The current reference value
This allows tests and debugging tools to observe which object is currently bound to a slot, even when that object is mutated in place.
Using RefSlot as a Test Key¶
Each RefSlot has:
A stable identity
A reference to the currently bound object
Equality based on slot identity, not object content
Example¶
// in ref_state.dart
final refState = RefAppState();
final obj = refState.slot<MyObject>('obj', initial: MyObject());
//////////////////
// in main.dart
if (kDebugMode) {
refState.setRefDebugListener((slot, ref) {
debugPrint("RefSlot:${slot.name}, now refers to:$ref");
});
}
//////////////////
// in runtime code
obj.set(MyObject());
Deterministic Behavior¶
SimpleAppState is fully deterministic:
State updates are synchronous
No background processing occurs
No hidden side effects are introduced
This allows tests to assert results immediately after updates, without timing control or async handling.
RefAppState is also deterministic at the slot level, but tests must account for mutable reference objects that can change without rebinding the slot.
Testing Perspective¶
Because StateSlot and RefSlot sit at the boundary between state and UI:
Tests can focus on slot changes instead of widget trees
UI refactoring does not invalidate state-level tests
Behavior can be reasoned about from an observer’s perspective
This encourages clear, robust, and maintainable tests.