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
- Why Write Tests? — Tests catch bugs before users do. They let you refactor confidently -- change code and run tests to verify nothing broke. They document how your code should behave. They prevent regressions (old bugs coming back). team_mvp_kit has tests for BLoCs, repositories, and use cases.
- Unit Test Basics with flutter_test — A unit test verifies that a single function or class works correctly in isolation. Use test() to define a test case with a description and a body. Use expect() to assert that actual values match expected values. Group related tests with group().
- Mocktail for Mocking Dependencies — Mocktail lets you create fake versions of dependencies so you can test a class in isolation. If your BLoC depends on a Repository, create a MockRepository that returns predefined data. This way you test the BLoC logic without making real API calls.
- Testing BLoCs with bloc_test — The bloc_test package provides blocTest() for testing BLoC state transitions. You specify the BLoC to create, the event to add (act), and the expected states (expect). It verifies the exact sequence of states emitted. team_mvp_kit tests every BLoC this way.
- Testing Error States — Test that your BLoC handles failures correctly by making the mock return a failure Result. Verify that the BLoC emits a loading state followed by an error state with the correct message. This ensures your error handling actually works.
- Arrange, Act, Assert Pattern — Every test follows three steps: Arrange (set up mocks and test data), Act (call the code being tested), Assert (verify the results). This pattern keeps tests readable and organized. In bloc_test, build is Arrange, act is Act, and expect is Assert.
- Widget Testing Basics — Widget tests verify that your UI renders correctly and responds to user interactions. Use pumpWidget() to render a widget, find.text() to locate elements, and tester.tap() to simulate taps. Widget tests run faster than integration tests but slower than unit tests.
- Test Setup and Teardown — Use setUp() to run code before EACH test (create fresh mocks). Use tearDown() to clean up after each test. Use setUpAll() and tearDownAll() for one-time setup. This ensures tests are independent and do not affect each other.
- Running Tests and Coverage — Run flutter test to execute all tests. Use --coverage to generate a coverage report showing which lines of code are tested. Aim for high coverage on business logic (BLoCs, repositories, use cases) but do not obsess over 100% coverage on every widget.
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. Import flutter_test for test assertions and structure
- 2. Import bloc_test for BLoC-specific testing utilities
- 3. Import mocktail for creating mock objects
- 4.
- 5. Comment: Create mock classes by extending Mock
- 6. MockUserRepository fakes the UserRepository interface
- 7. Closing the mock class
- 8.
- 9. Comment: Test file for UserBloc
- 10. Main function where all tests live
- 11. Create a late mock repository variable
- 12. Create a late BLoC variable
- 13.
- 14. setUp runs before each test to create fresh instances
- 15. Create a new mock repository for each test
- 16. Closing setUp
- 17.
- 18. tearDown runs after each test to clean up
- 19. Close the BLoC to prevent memory leaks
- 20. Closing tearDown
- 21.
- 22. Group related UserBloc tests together
- 23.
- 24. Comment: Test successful user loading
- 25. blocTest with type parameters for the BLoC and State
- 26. Description of what this test verifies
- 27. build creates the BLoC with configured mocks
- 28. Configure mock to return success when getUser is called with any ID
- 29. Return a successful Result with a test User
- 30. Closing the when configuration
- 31. Return the BLoC instance for testing
- 32. Closing build
- 33. act adds the event that triggers the behavior
- 34. Add LoadUser event with ID 42
- 35. expect lists the exact states that should be emitted
- 36. First: UserLoading state
- 37. Second: UserLoaded state with the expected User data
- 38. Closing expect
- 39. verify checks that the mock was called correctly
- 40. Confirm getUser was called once with ID 42
- 41. Closing the first blocTest
- 42.
- 43. Comment: Test network failure scenario
- 44. blocTest for the failure path
- 45. Description
- 46. build configures mock to return a network failure
- 47. Return a failure Result with network error message
- 48. Closing configuration
- 49. Create BLoC
- 50. Closing build
- 51. act adds the same LoadUser event
- 52. expect verifies Loading then Error states
- 53. UserLoading emitted first
- 54. UserError emitted with the network error message
- 55. Closing the failure blocTest
- 56.
- 57. Comment: Test server error scenario
- 58. blocTest for server errors
- 59. Description
- 60. build configures mock to return server failure
- 61. Return a failure Result with server error
- 62. Closing configuration
- 63. Create BLoC
- 64. Closing build
- 65. act adds LoadUser event
- 66. expect verifies Loading then Error
- 67. Closing the server error blocTest
- 68. Closing the UserBloc group
- 69.
- 70. Comment: Unit tests for the repository implementation
- 71. Group for UserRepositoryImpl tests
- 72. Create a mock ApiService
- 73.
- 74. setUp creates fresh mock API for each test
- 75. Closing setUp
- 76.
- 77. Test: successful API call returns mapped User
- 78. Configure mock API to return user JSON
- 79. Closing the when configuration
- 80.
- 81. Create repository with mock API
- 82. Call getUser and get the Result
- 83.
- 84. Assert the result is a success
- 85. Assert the user name was mapped correctly
- 86. Closing the success test
- 87.
- 88. Test: network error returns Failure
- 89. Configure mock API to throw a DioException
- 90. Connection error type simulates no internet
- 91. Closing the when configuration
- 92.
- 93. Create repository and call getUser
- 94.
- 95. Assert the result is a failure
- 96. Assert the failure message mentions internet
- 97. Closing the failure test and repository group
- 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
- Flutter Testing Documentation (Flutter Official)
- bloc_test Package (pub.dev)
- mocktail Package (pub.dev)