Тестирование с моками и фейками
В нашем тестовом проекте bloc_example недавно появились тесты. Пока добавил только проверку логики работы блоков. Вместо реальных запросов в сеть или походов в локальную БД мы используем моки и фейки.
Мок (Mock) - это тестовый объект, имитирующий поведение реального объекта и позволяющий проверять взаимодействия с ним (что вызывалось, с какими параметрами, сколько раз).
Фейк (Fake) - это самостоятельная реализация того же интерфейса, который реализует реальный компонент.
В bloc_example эти компоненты реализуются через пакет mocktail - решение от Феликса Ангелова, разработчика bloc и flutter_bloc. С другими плагинами по типу mockito я мало работал, поэтому рассказывать про них и сравнивать с mocktail не буду.
Создать и использовать мок репозитория можно следующим образом:
// Создаем мок репозитория как отдельный класс.
class MockUsersRepository extends Mock implements UsersRepository {}
void main() {
group(
'UsersBloc tests',
() {
late UsersRepository usersRepository;
setUp(
() {
usersRepository = MockUsersRepository();
// Определяем поведение мока при вызове метода
// с определенными параметрами.
when(
() =>
usersRepository.fetchUsers(
limit: 30,
page: 0,
),
).thenAnswer((_) => Future.value((_testUsers, null)));
},
);
blocTest(
'Initialization test',
build: () =>
UsersBloc(
usersRepository: usersRepository,
),
expect: () =>
[
_defaultLoadedState,
],
// После того, как тест завершится, мы можем проверить,
// сколько раз, например, был вызван метод `fetchUsers`.
// Это полезно, когда у блока сложный флоу инициализации,
// и нужно быть на все 100% уверенными в том, что лишних запросов нет.
tearDown: () {
verify(() => usersRepository.fetchUsers(limit: 30, page: 0)).called(1);
},
);
},
)
,
}
Проверку количества запросов метода fetchUsers по требованиям мы делать не будем - это показано для примера. Конечная
цель теста - удостовериться, что состояния и данные в них у блоков такие, какие мы в них ожидаем. Для этого требуется
добавить проверку пагинации. В реализации выше нужно для каждого возможного варианта запроса сделать связку when ->
thenAnswer. Это долго, и есть куда-более простой способ решить данную задачу - написать фейковый репозиторий.
Фейк, выполняющий ту же самую работу, что и мок, будет выглядеть так:
class FakeUsersRepository extends Fake implements UsersRepository {
final List<User> _allUsers;
FakeUsersRepository(this._allUsers);
@override
Future<(List<User>?, String?)> fetchUsers({
int limit = 30,
int page = 0,
}) async {
final start = page * limit;
final end = start + limit;
if (start >= _allUsers.length) {
return (<User>[], null);
}
final pageUsers = _allUsers.sublist(
start,
min(end, _allUsers.length),
);
return (pageUsers, null);
}
}
Для простой логики и проверок вызовов методов лучше использовать моки, для сложных кейсов - фейки. Руководствуйтесь требованиями к вашим тестам, но если на первых порах ничего не понятно, возьмите моки - они легче в реализации.
❗️ Учитывайте, что фейковые репозитории, которые наследуются от Fake из mocktail, лучше использовать только в тестах:
/// **WARNING**:[Fake] uses [noSuchMethod](goo.gl/r3IQUH), which is a _form_ of
/// runtime reflection, and causes sub-standard code to be generated. As such,
/// [Fake] should strictly _not_ be used in any production code, especially if
/// used within the context of Dart for Web (dart2js, DDC) and Dart for Mobile
/// (Flutter).
▶️ Подробнее про mocktail, моки, фейки, и остальные возможности библиотеки можно узнать из документации.