Testing
One of the most important aspects of utopia_hooks
is the testability.
Unit testing
Single hooks can be tested in isolation, without placing them in Widgets. This applies to all kinds of hooks, including local and global state.
Unit testing is done
via SimpleHookContext
.
To test any hook, create a new instance of SimpleHookContext
and pass the hook to it:
final context = SimpleHookContext(useCounterState);
The hook will be initialized at creation. Its current value can be accessed using context()
(which is a shorthand
for context.value
). If the hook triggers rebuild (e.g. by changing a value of useState
), it will happen immediately.
This way, assertions about the hook behavior can be made right after triggering any actions:
expect(context().counterValue, 0);
context().onButtonPressed();
expect(context().counterValue, 1);
Test structure
It's recommended to create a new SimpleHookContext
for each test case, to avoid any interaction between them. This can be
done using setUpAll
function:
void main() {
group("CounterState", () {
late SimpleHookContext<CounterState> context;
setUpAll(() => context = SimpleHookContext(useCounterState));
test("should ...", () {
// ... perform test
});
// ... more tests
});
}
Asynchronous hooks
If the hook does something asynchronously (e.g. waits some time before changing a value), the test can wait for it
to happen using waitUntil
method:
context().onButtonPressed();
await context.waitUntil((it) => it.counterValue == 1);
The provided predicate will be called with the current value after every rebuild of the hook until it returns true
,
after which the waitUntil
method will complete.
Mocking dependencies
If the hook depends on external values via useProvided
, they can be mocked using the provided
parameter
of SimpleHookContext
:
SimpleHookContext(useCounterState, provided: {
StateA: StateA(/* ... */),
StateB: StateB(/* ... */),
});
Entries of the provided
map must have form Type: InstanceOfType
, otherwise a runtime error will be thrown:
// DON'T
provided: {
StateA: StateB(/* ... */),
}
Provided values can be changed during the test using the setProvided
method, immediately triggering a rebuild of the
hook.
It's not recommended to mock the behavior of a fully interactive global state this way. Instead, it's better to use techniques described in the Mocking global states section.
Integration testing
Multiple global and local states can be tested together
using SimpleHookProvidedContainer
,
which allows to register any number of hooks that can communicate with each other, and then assert facts about their
behavior:
final container = SimpleHookProviderContainer({
AuthState: useAuthState,
LocalState: useLocalState,
});
Similarly to SimpleHookContext
, all hooks will be initialized at creation. When a rebuild of a given state is
triggered, it will happen immediately, along with all dependent states. The current value of a state can be retrieved
using container<T>()
(which is a shorthand for container.get<T>()
).
expect(container<LocalState>().isLoggedIn, false);
await container<AuthState>().logIn();
expect(container<LocalState>().isLoggedIn, true);
Additional similarities to SimpleHookContext
include:
-
Handling asynchronous hooks via
waitUntil
method:await container.waitUntil<LocalState>((it) => it.isLoggedIn);
-
Mocking dependencies via the
provided
parameter andsetProvided
method.
Mocking global states
SimpleHookProviderContainer
also allows unit-testing of a single global/local state by mocking the behavior of its
dependencies using a "mock hook":
AuthState _useMockAuthState() {
final isLoggedInState = useState(false);
Future<void> logIn() async {
await Future.delayed(const Duration(seconds: 1));
isLoggedInState.value = true;
}
return AuthState(
isLoggedIn: isLoggedInState.value,
logIn: logIn,
);
}
void main() {
group("LocalState", () {
late SimpleHookProviderContainer container;
setUpAll(() {
container = SimpleHookProviderContainer({
AuthState: _useMockAuthState,
LocalState: useLocalState,
});
});
test("should log in", () {
expect(container<LocalState>().isLoggedIn, false);
container<LocalState>().onLogInPressed();
await container.waitUntil<LocalState>((it) => it.isLoggedIn);
expect(container<AuthState>().isLoggedIn, true);
});
});
}
Widget testing
Since hooks can be used in any Flutter Widget, they can also be tested using the standard Flutter testing framework by
placing them
inside HookWidget
/ HookProviderContainerWidget
.
However, it's recommended to use Flutter-independent techniques described above, as they make testing easier while
providing better isolation.
See also
- Global state - A guide on building global states that can later be tested using the techniques described above.
- Testing Examples - Unit / integration tests written for the Examples.