Skip to main content

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(/* ... */),
});
warning

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 and setProvided 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.