Unit test of repository with Mocktail in Flutter: solved exercise

Unit test of repository with Mocktail in Flutter: solved exercise

Testing only the UI is not enough. Business logic in repositories and use cases needs its own unit tests, isolated from the network and Flutter. mocktail lets you create mocks without code generation, which simplifies setup considerably.

Problem statement

Design a PostRepository contract, a GetPostsUseCase that filters empty posts, and write unit tests that verify correct behavior on success and error by mocking the repository.

Dependencies

1
2
3
4
5
6
7
dependencies:
  flutter: { sdk: flutter }

dev_dependencies:
  flutter_test:
    sdk: flutter
  mocktail: ^1.0.4

Solution: domain classes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// lib/domain/models/post.dart
class Post {
  final int id;
  final String title;
  const Post({required this.id, required this.title});

  @override
  bool operator ==(Object other) =>
      other is Post && other.id == id && other.title == title;

  @override
  int get hashCode => Object.hash(id, title);
}

// lib/domain/repositories/post_repository.dart
abstract interface class PostRepository {
  Future<List<Post>> getPosts();
}

// lib/domain/use_cases/get_posts_use_case.dart
class GetPostsUseCase {
  final PostRepository _repository;

  const GetPostsUseCase(this._repository);

  /// Returns only posts with non-empty titles, sorted by id.
  Future<List<Post>> call() async {
    final posts = await _repository.getPosts();
    return posts
        .where((p) => p.title.trim().isNotEmpty)
        .toList()
      ..sort((a, b) => a.id.compareTo(b.id));
  }
}

Solution: tests with Mocktail

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// test/domain/get_posts_use_case_test.dart
import 'package:mocktail/mocktail.dart';
import 'package:flutter_test/flutter_test.dart';

// Import your domain classes
// import 'package:my_app/domain/...';

// ── Mock ─────────────────────────────────────────────────────────────────────
class MockPostRepository extends Mock implements PostRepository {}

void main() {
  late MockPostRepository repository;
  late GetPostsUseCase useCase;

  setUp(() {
    repository = MockPostRepository();
    useCase = GetPostsUseCase(repository);
  });

  group('GetPostsUseCase', () {
    test('returns filtered and sorted list when repository succeeds', () async {
      when(() => repository.getPosts()).thenAnswer(
        (_) async => [
          const Post(id: 3, title: 'Third'),
          const Post(id: 1, title: 'First'),
          const Post(id: 2, title: ''),        // should be filtered
          const Post(id: 4, title: '   '),     // whitespace → should be filtered
        ],
      );

      final result = await useCase();

      expect(result, [
        const Post(id: 1, title: 'First'),
        const Post(id: 3, title: 'Third'),
      ]);
      verify(() => repository.getPosts()).called(1);
    });

    test('propagates exception when repository fails', () async {
      when(() => repository.getPosts())
          .thenThrow(Exception('Network error'));

      expect(() => useCase(), throwsA(isA<Exception>()));
    });

    test('returns empty list if all posts have empty titles', () async {
      when(() => repository.getPosts()).thenAnswer(
        (_) async => [const Post(id: 1, title: '')],
      );

      final result = await useCase();

      expect(result, isEmpty);
    });
  });
}

How to run the tests

1
2
3
4
5
6
7
8
9
# All tests
flutter test

# This file only
flutter test test/domain/get_posts_use_case_test.dart

# With coverage
flutter test --coverage
genhtml coverage/lcov.info -o coverage/html

Expected result

1
00:01 +3: All tests passed!

Three cases pass: filtered and sorted list, propagated exception, empty list.

Common mistakes

  • Unconfigured mock: forgetting when(...) before calling a mocked method throws MissingStubError. Configure stubs in setUp or at the start of each test.
  • Equality not implemented in the model: if Post does not override ==, expect(result, [Post(...)]) always fails even when values match.
  • Verifying without having set up the mock: verify works on mocktail mocks, not real instances.

Why unit tests for domain classes are worth it

  • They run in milliseconds, with no emulator or network needed.
  • They document the expected behavior of business logic.
  • They catch regressions when you change the use case or repository.
  • They serve as executable specifications for the team.

Practical use

Any use case in a clean architecture app benefits from this type of test: list filtering, data transformation, business rules (discounts, validations, permissions).

Guided practice and next step

FAQ

Why Mocktail instead of Mockito?

Mocktail requires no code generation with build_runner. You write the mock in one line and tests run without any pre-build step.

When to use thenAnswer vs thenReturn?

Use thenReturn for synchronous values. Use thenAnswer when the stub returns a Future or Stream.

Where do repository tests live in the project?

In test/domain/ if you follow clean architecture. The convention is to mirror the lib/ structure inside test/.