Lesson 50 of 51 advanced

Testing & Debugging

Proving Your Code Works

Open interactive version (quiz + challenge)

Real-world analogy

Testing is like being a detective who checks their own work. You write a suspect list (test cases), examine each suspect (function), and verify their alibi (expected output). Unit tests check individual suspects. Widget tests check if suspects work together. Mocktail creates fake witnesses so you can test suspects in isolation. If all alibis check out, your case (app) is solid!

What is it?

Testing in Flutter uses flutter_test for assertions, mocktail for creating mock dependencies, and bloc_test for verifying BLoC state transitions. The Arrange-Act-Assert pattern structures every test. Mocks let you test classes in isolation without real APIs or databases. team_mvp_kit tests all BLoCs, use cases, and repositories using these tools.

Real-world relevance

team_mvp_kit has test files for every BLoC and critical use case. The pattern is: create mocks of dependencies, configure them to return specific data or failures, test that the BLoC emits the correct sequence of states. This catches regressions before they reach production and documents the expected behavior of every feature.

Key points

Code example

import 'package:flutter_test/flutter_test.dart';
import 'package:bloc_test/bloc_test.dart';
import 'package:mocktail/mocktail.dart';

// 1. Create mock classes
class MockUserRepository extends Mock
    implements UserRepository {}

// 2. Test file for UserBloc
void main() {
  late MockUserRepository mockRepo;
  late UserBloc bloc;

  setUp(() {
    mockRepo = MockUserRepository();
  });

  tearDown(() {
    bloc.close();
  });

  group('UserBloc', () {
    // Test successful user loading
    blocTest<UserBloc, UserState>(
      'emits [Loading, Loaded] when user loads successfully',
      build: () {
        when(() => mockRepo.getUser(any())).thenAnswer(
          (_) async => Result.success(
            User(id: 42, name: 'Alice', email: 'a@b.com',
                 isVerified: true),
          ),
        );
        return UserBloc(mockRepo);
      },
      act: (bloc) => bloc.add(LoadUser(42)),
      expect: () => [
        UserLoading(),
        UserLoaded(
          User(id: 42, name: 'Alice', email: 'a@b.com',
               isVerified: true),
        ),
      ],
      verify: (_) {
        verify(() => mockRepo.getUser(42)).called(1);
      },
    );

    // Test network failure
    blocTest<UserBloc, UserState>(
      'emits [Loading, Error] on network failure',
      build: () {
        when(() => mockRepo.getUser(any())).thenAnswer(
          (_) async => Result<User>.failure(
            Failure.network('No internet connection'),
          ),
        );
        return UserBloc(mockRepo);
      },
      act: (bloc) => bloc.add(LoadUser(42)),
      expect: () => [
        UserLoading(),
        UserError('No internet connection'),
      ],
    );

    // Test server error
    blocTest<UserBloc, UserState>(
      'emits [Loading, Error] on server error',
      build: () {
        when(() => mockRepo.getUser(any())).thenAnswer(
          (_) async => Result<User>.failure(
            Failure.server('Internal error', 500),
          ),
        );
        return UserBloc(mockRepo);
      },
      act: (bloc) => bloc.add(LoadUser(42)),
      expect: () => [
        UserLoading(),
        UserError('Internal error'),
      ],
    );
  });

  // 3. Unit test for repository
  group('UserRepositoryImpl', () {
    late MockApiService mockApi;

    setUp(() {
      mockApi = MockApiService();
    });

    test('returns Success with mapped User on API success',
        () async {
      when(() => mockApi.get('/users/1')).thenAnswer(
        (_) async => {
          'id': 1,
          'full_name': 'Alice',
          'email': 'alice@example.com',
          'is_verified': true,
        },
      );

      final repo = UserRepositoryImpl(mockApi);
      final result = await repo.getUser(1);

      expect(result.isSuccess, isTrue);
      expect(result.data?.name, equals('Alice'));
    });

    test('returns Failure on network error', () async {
      when(() => mockApi.get('/users/1')).thenThrow(
        DioException(
          type: DioExceptionType.connectionError,
          requestOptions: RequestOptions(path: '/users/1'),
        ),
      );

      final repo = UserRepositoryImpl(mockApi);
      final result = await repo.getUser(1);

      expect(result.isFailure, isTrue);
      expect(result.failure?.message,
          contains('internet'));
    });
  });
}

Line-by-line walkthrough

  1. 1. Import flutter_test for test assertions and structure
  2. 2. Import bloc_test for BLoC-specific testing utilities
  3. 3. Import mocktail for creating mock objects
  4. 4.
  5. 5. Comment: Create mock classes by extending Mock
  6. 6. MockUserRepository fakes the UserRepository interface
  7. 7. Closing the mock class
  8. 8.
  9. 9. Comment: Test file for UserBloc
  10. 10. Main function where all tests live
  11. 11. Create a late mock repository variable
  12. 12. Create a late BLoC variable
  13. 13.
  14. 14. setUp runs before each test to create fresh instances
  15. 15. Create a new mock repository for each test
  16. 16. Closing setUp
  17. 17.
  18. 18. tearDown runs after each test to clean up
  19. 19. Close the BLoC to prevent memory leaks
  20. 20. Closing tearDown
  21. 21.
  22. 22. Group related UserBloc tests together
  23. 23.
  24. 24. Comment: Test successful user loading
  25. 25. blocTest with type parameters for the BLoC and State
  26. 26. Description of what this test verifies
  27. 27. build creates the BLoC with configured mocks
  28. 28. Configure mock to return success when getUser is called with any ID
  29. 29. Return a successful Result with a test User
  30. 30. Closing the when configuration
  31. 31. Return the BLoC instance for testing
  32. 32. Closing build
  33. 33. act adds the event that triggers the behavior
  34. 34. Add LoadUser event with ID 42
  35. 35. expect lists the exact states that should be emitted
  36. 36. First: UserLoading state
  37. 37. Second: UserLoaded state with the expected User data
  38. 38. Closing expect
  39. 39. verify checks that the mock was called correctly
  40. 40. Confirm getUser was called once with ID 42
  41. 41. Closing the first blocTest
  42. 42.
  43. 43. Comment: Test network failure scenario
  44. 44. blocTest for the failure path
  45. 45. Description
  46. 46. build configures mock to return a network failure
  47. 47. Return a failure Result with network error message
  48. 48. Closing configuration
  49. 49. Create BLoC
  50. 50. Closing build
  51. 51. act adds the same LoadUser event
  52. 52. expect verifies Loading then Error states
  53. 53. UserLoading emitted first
  54. 54. UserError emitted with the network error message
  55. 55. Closing the failure blocTest
  56. 56.
  57. 57. Comment: Test server error scenario
  58. 58. blocTest for server errors
  59. 59. Description
  60. 60. build configures mock to return server failure
  61. 61. Return a failure Result with server error
  62. 62. Closing configuration
  63. 63. Create BLoC
  64. 64. Closing build
  65. 65. act adds LoadUser event
  66. 66. expect verifies Loading then Error
  67. 67. Closing the server error blocTest
  68. 68. Closing the UserBloc group
  69. 69.
  70. 70. Comment: Unit tests for the repository implementation
  71. 71. Group for UserRepositoryImpl tests
  72. 72. Create a mock ApiService
  73. 73.
  74. 74. setUp creates fresh mock API for each test
  75. 75. Closing setUp
  76. 76.
  77. 77. Test: successful API call returns mapped User
  78. 78. Configure mock API to return user JSON
  79. 79. Closing the when configuration
  80. 80.
  81. 81. Create repository with mock API
  82. 82. Call getUser and get the Result
  83. 83.
  84. 84. Assert the result is a success
  85. 85. Assert the user name was mapped correctly
  86. 86. Closing the success test
  87. 87.
  88. 88. Test: network error returns Failure
  89. 89. Configure mock API to throw a DioException
  90. 90. Connection error type simulates no internet
  91. 91. Closing the when configuration
  92. 92.
  93. 93. Create repository and call getUser
  94. 94.
  95. 95. Assert the result is a failure
  96. 96. Assert the failure message mentions internet
  97. 97. Closing the failure test and repository group
  98. 98. Closing main

Spot the bug

blocTest<UserBloc, UserState>(
  'loads user successfully',
  build: () {
    when(() => mockRepo.getUser(42)).thenAnswer(
      (_) async => Result.success(User(id: 42, name: 'Alice')),
    );
    return UserBloc(mockRepo);
  },
  act: (bloc) => bloc.add(LoadUser(42)),
  expect: () => [
    UserLoaded(User(id: 42, name: 'Alice')),
  ],
);
Need a hint?
Think about what states the BLoC emits before the data is loaded...
Show answer
The expect list is missing the UserLoading() state. The BLoC emits UserLoading() first, then UserLoaded(). The expect should be: [UserLoading(), UserLoaded(User(id: 42, name: 'Alice'))]. blocTest verifies the EXACT sequence of states, so missing even one state causes the test to fail.

Explain like I'm 5

Imagine you built a robot that sorts colored balls. To test it, you do not wait for real balls to arrive from the factory. Instead, you use pretend balls (mocks) that you control. You put in a red ball and check: did the robot put it in the red bin? You put in a broken ball and check: did the robot handle it without crashing? By using pretend balls, you can test every possible situation quickly and safely. That is what mocktail and testing do for your code!

Fun fact

The term 'unit test' was first used in the 1950s, but automated testing became mainstream with Kent Beck's xUnit framework in the 1990s. The Flutter testing ecosystem is remarkably mature -- bloc_test has over 1 million downloads, and mocktail (created by Felix Angelov, the same person who created the bloc package) provides a simpler API than the older mockito package by not requiring code generation!

Hands-on challenge

Write a complete test file for a LoginBloc. Create MockAuthRepository. Write three blocTest cases: 1) successful login emits [Loading, Success], 2) wrong password emits [Loading, Error], 3) network failure emits [Loading, Error]. Use when() to configure mocks and verify() to check mock calls.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter & Dart