การทดสอบ BLoC ด้วย bloc_test และ Mocktail
เขียนการทดสอบหน่วยที่ให้ผลแน่นอนสำหรับบล็อกโดยใช้การคาดหวังของ bloc_test และสิ่งจำลองการพึ่งพา
การทดสอบ BLoC ด้วย bloc_test และ Mocktail เป็นบทเรียน Flutter Mobile Development ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Flutter Mobile Development และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Flutter Mobile Development มีบทเรียนทั้งหมด 4 บทเรียน
บางส่วนของบทเรียนนี้ยังไม่ได้รับการแปล และแสดงเป็นภาษาอังกฤษ
Why BLoCs Need Deterministic Tests
A BLoC is a pure state machine: it takes events in and emits a predictable sequence of states out. That makes it the most testable layer of a Flutter app.
A good bloc test answers one question: given this starting state and these events, exactly which states get emitted, in what order?
- Deterministic means the same input always produces the same output sequence.
- Sources of non-determinism to eliminate: real network calls,
DateTime.now(), random values, and timers.
We solve this with two packages: bloc_test for the test harness and mocktail for faking dependencies.
Setting Up the Test Dependencies
Add the test tooling to the dev_dependencies section of pubspec.yaml. None of these ship in your production bundle.
bloc_testprovides theblocTesthelper.mocktailcreates mocks with no code generation.testgives yougroup,setUp, andexpect.
Mocktail is preferred over Mockito here because it needs no build_runner step and works cleanly with null safety.
dev_dependencies:
bloc_test: ^9.1.7
mocktail: ^1.0.4
test: ^1.25.0The BLoC Under Test
Here is the BLoC we will test. A WeatherBloc depends on a WeatherRepository abstraction. On a WeatherRequested event it emits a loading state, then either a success or failure state.
Notice the dependency is injected through the constructor. This is the key design choice that makes the BLoC testable: we can pass in a fake repository instead of one that hits the network.
class WeatherBloc extends Bloc<WeatherEvent, WeatherState> {
WeatherBloc(this._repository) : super(WeatherInitial()) {
on<WeatherRequested>(_onRequested);
}
final WeatherRepository _repository;
Future<void> _onRequested(
WeatherRequested event,
Emitter<WeatherState> emit,
) async {
emit(WeatherLoading());
try {
final weather = await _repository.fetch(event.city);
emit(WeatherSuccess(weather));
} catch (_) {
emit(WeatherFailure());
}
}
}Creating a Mock with Mocktail
To isolate the BLoC, we replace the real repository with a mock. With Mocktail you simply extend Mock and implement the abstraction.
- No annotations, no generated
.mocks.dartfiles. - The mock starts with every method stubbed to return
nulluntil you tell it otherwise.
You then control its behaviour per test using when(...).thenAnswer(...).
import 'package:mocktail/mocktail.dart';
class MockWeatherRepository extends Mock
implements WeatherRepository {}
class FakeWeather extends Fake implements Weather {}Stubbing Return Values
Inside a test, configure the mock before acting. Use thenAnswer for asynchronous methods that return a Future, and thenThrow to simulate an error path.
thenAnswer((_) async => value)returns a resolved Future.thenThrow(Exception())makes the call fail, exercising your catch block.
This is how you force the exact branch you want without any real I/O.
// Success path stub
when(() => repository.fetch('London'))
.thenAnswer((_) async => const Weather(temp: 14, city: 'London'));
// Failure path stub
when(() => repository.fetch('London'))
.thenThrow(Exception('network down'));The blocTest Helper Anatomy
The blocTest function from bloc_test wires up the whole flow. Its core parameters are:
- build: returns a fresh BLoC instance for each run.
- act: dispatches one or more events into that BLoC.
- expect: the ordered list of states that should be emitted, excluding the initial state.
The build callback runs anew every test, guaranteeing a clean slate and no shared state between cases.
blocTest<WeatherBloc, WeatherState>(
'emits [Loading, Success] when fetch succeeds',
build: () {
when(() => repository.fetch(any()))
.thenAnswer((_) async => const Weather(temp: 14, city: 'London'));
return WeatherBloc(repository);
},
act: (bloc) => bloc.add(const WeatherRequested('London')),
expect: () => const [
WeatherLoading(),
WeatherSuccess(Weather(temp: 14, city: 'London')),
],
);Equatable Makes expect Work
The expect list compares emitted states to your expected states using ==. Plain Dart classes compare by identity, so two different WeatherSuccess instances would never be equal and your test would fail confusingly.
Override value equality with the equatable package. List the fields in props and instances with the same values compare as equal.
abstract class WeatherState extends Equatable {
const WeatherState();
@override
List<Object?> get props => [];
}
class WeatherSuccess extends WeatherState {
const WeatherSuccess(this.weather);
final Weather weather;
@override
List<Object?> get props => [weather];
}Testing the Failure Path
Robust tests cover errors, not just the happy path. Stub the dependency to throw, then assert the BLoC emits the failure state instead of crashing.
This proves your try/catch in the handler actually converts exceptions into a user-facing error state.
blocTest<WeatherBloc, WeatherState>(
'emits [Loading, Failure] when fetch throws',
build: () {
when(() => repository.fetch(any()))
.thenThrow(Exception('network down'));
return WeatherBloc(repository);
},
act: (bloc) => bloc.add(const WeatherRequested('London')),
expect: () => const [
WeatherLoading(),
WeatherFailure(),
],
);registerFallbackValue for any()
Mocktail's any() matcher lets you stub a call regardless of the argument. But for custom types Mocktail cannot construct a placeholder, so it throws at registration time.
Fix this by registering a Fake instance once in setUpAll. Built-in types like String and int never need this; only your own classes do.
setUpAll(() {
// Required before using any<Weather>() in stubs
registerFallbackValue(FakeWeather());
});Verifying Interactions
Beyond asserting emitted states, you often want to confirm the BLoC actually called its dependency the right number of times. Use verify in the verify callback of blocTest, which runs after expect.
verify(() => mock.method()).called(1)asserts exactly one call.verifyNever(...)asserts a call never happened.
This catches bugs where the right state is emitted but for the wrong reason.
blocTest<WeatherBloc, WeatherState>(
'calls repository exactly once',
build: () {
when(() => repository.fetch(any()))
.thenAnswer((_) async => const Weather(temp: 14, city: 'London'));
return WeatherBloc(repository);
},
act: (bloc) => bloc.add(const WeatherRequested('London')),
verify: (_) {
verify(() => repository.fetch('London')).called(1);
},
);Controlling Time with seed and skip
Two more blocTest parameters give you fine control:
- seed: provides a starting state, so you can test a transition from any point, not just the initial state.
- skip: ignores the first N emitted states, useful when you only care about the final one.
- wait: a
Durationto let debounced or delayed events settle before assertions run.
Use seed to test event handlers in isolation without replaying the entire history.
blocTest<CounterBloc, int>(
'emits [11] from a seeded state of 10',
build: () => CounterBloc(),
seed: () => 10,
act: (bloc) => bloc.add(Increment()),
expect: () => const [11],
);Quick Check
A test stubs repository.fetch(any()) where fetch takes a custom Weather argument, and the test crashes at setup with a fallback-value error before any state is emitted. What is the correct fix?
Recap: Deterministic BLoC Testing
You now have a repeatable recipe for testing BLoCs:
- Inject dependencies through the constructor so they can be swapped for mocks.
- Create mocks by extending
Mock implementswith Mocktail; no code generation needed. - Stub behaviour with
thenAnswerfor success andthenThrowfor failure paths. - Use
blocTestwith build / act / expect to assert the exact ordered states. - Make
expectreliable with Equatable value equality. - Register fallbacks with
registerFallbackValuewhen matching custom types viaany(). - Confirm intent with
verify, and control flow withseed,skip, andwait.
Cover both the happy and failure paths, and your BLoC layer becomes fully deterministic and regression-proof.
คำถามที่พบบ่อย
บทเรียน “การทดสอบ BLoC ด้วย bloc_test และ Mocktail” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การทดสอบ BLoC ด้วย bloc_test และ Mocktail” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Flutter Mobile Development ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Flutter Mobile Development มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การทดสอบ BLoC ด้วย bloc_test และ Mocktail”
เขียนการทดสอบหน่วยที่ให้ผลแน่นอนสำหรับบล็อกโดยใช้การคาดหวังของ bloc_test และสิ่งจำลองการพึ่งพา คุณปฏิบัติ Flutter Mobile Development ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Flutter Mobile Development หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Flutter Mobile Development บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน
บทเรียน “การทดสอบ BLoC ด้วย bloc_test และ Mocktail” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Flutter Mobile Development นี้ได้ไหม
ได้ บทเรียน Flutter Mobile Development ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- เหตุการณ์ สถานะ และการตัดสินใจระหว่าง Cubit กับ Bloc
- ตัวแปลงสตรีมและการหน่วงเหตุการณ์ใน BLoC
- การคงอยู่ของสถานะด้วย HydratedBloc
- การทดสอบ BLoC ด้วย bloc_test และ Mocktail