How does mockito's @GenerateNiceMocks or @GenerateMocks code generation differ from mocktail, and what decides which one a Flutter team uses?
answer
- annotation plus a .mocks.dart file
- dart run build_runner build
- generated overrides widen parameters
- nice mocks return legal defaults
- mocktail: closures, no build step
basics
~10 smockito generates mock classes into a .mocks.dart file from @GenerateNiceMocks or @GenerateMocks when build_runner runs; mocktail needs no generation and uses closures plus registerFallbackValue. The choice is mostly build step versus runtime setup.
solid answer
~30 sWith mockito 5 you annotate the test library, for example `@GenerateNiceMocks([MockSpec<WeatherApi>()])`, run `dart run build_runner build`, and import the generated `forecast_view_model_test.mocks.dart` that holds `MockWeatherApi`. The generated overrides make parameters nullable, so matchers are plain getters (`when(api.fetchForecast(any))`, no closure). `@GenerateMocks` mocks throw a `MissingStubError` for unstubbed calls; `@GenerateNiceMocks`, which mockito recommends, returns a simple legal value instead. mocktail skips generation: you write `extends Mock implements` and pay with closures and fallback registration. Teams already running build_runner often keep mockito; teams avoiding generated files pick mocktail.
code
dart · 18 linesimport 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
@GenerateNiceMocks([MockSpec<WeatherApi>()])
import 'forecast_view_model_test.mocks.dart';
void main() {
test('load fetches once', () async {
final api = MockWeatherApi();
when(api.fetchForecast(any))
.thenAnswer((_) async => const Forecast(highC: 14, lowC: 6));
await ForecastViewModel(api).load('Oslo');
verify(api.fetchForecast('Oslo')).called(1);
});
}go deeper
Recall that mockito needs an annotation and a build_runner step producing a .mocks.dart file, while mocktail needs neither.
Explain why mockito's matchers need no closure (widened generated parameters) and how @GenerateMocks and @GenerateNiceMocks treat an unstubbed call.
Talk about operating the choice: regenerating mocks after interface changes, generated files in CI, and consistent missing-stub behaviour across suites.
Frame it as a build-pipeline decision: whether the codebase already pays for code generation, and a single mocking library as a team standard.
## Two libraries, one job Both **mockito** (version 5.x, maintained by the Dart team) and **mocktail** (version 1.x) produce **mocks**: objects that implement a dependency's interface, record calls, and return configured answers. Their APIs look alike — `when`, `thenReturn`, `thenAnswer`, `thenThrow`, `verify`, `verifyNever` — because mocktail was modelled on mockito. What differs is **how the mock class comes into existence** and how each library copes with **sound null safety**. ## mockito: generate the mock class mockito's null-safe mode relies on **code generation**: 1. Add `mockito` and `build_runner` as dev dependencies. 2. Annotate the test library: ```dart @GenerateNiceMocks([MockSpec<WeatherApi>()]) import 'forecast_view_model_test.mocks.dart'; ``` 3. Run `dart run build_runner build`. The builder writes `forecast_view_model_test.mocks.dart`, named after the annotated file, containing `class MockWeatherApi extends Mock implements WeatherApi`. Because the class is generated, each method is a real override, and mockito **widens every non-nullable parameter to nullable** in it. That is why mockito's matchers are getters returning `null`: ```dart when(api.fetchForecast(any)).thenAnswer((_) async => forecast); verify(api.fetchForecast('Oslo')).called(1); ``` No closure is needed; the call happens directly and `null` is a legal argument to the widened override. ## Nice mocks versus classic mocks mockito has two annotations, and the difference is the **missing-stub behaviour**: | Annotation | Unstubbed call on a method returning `Future<Forecast>` | |---|---| | `@GenerateMocks([WeatherApi])` | throws `MissingStubError` naming the member | | `@GenerateNiceMocks([MockSpec<WeatherApi>()])` | returns a "simple" legal value that must not be used | mockito's README states that `@GenerateNiceMocks` is the recommended API. `@GenerateNiceMocks` accepts only `MockSpec` entries; `@GenerateMocks` takes a list of types plus optional `customMocks`. `MockSpec` itself carries options: - `as: #MockHttpClient` to rename the generated class, - `onMissingStub: OnMissingStub.throwException` or `OnMissingStub.returnDefault`, - `fallbackGenerators` and `unsupportedMembers` for members whose return type the generator cannot produce, such as a bare type variable `T`. For types mockito cannot invent a dummy for, `provideDummy<T>(value)` supplies one at runtime. ## mocktail: no generation, closures instead mocktail skips the build step. You write the class yourself — `class MockWeatherApi extends Mock implements WeatherApi {}` — and every member falls into `noSuchMethod`. With no generated override to widen types, mocktail defers calls inside **closures** (`when(() => api.fetchForecast(any()))`) so it can catch the `TypeError` a `null` return would cause, and `any()` must return a real value, which is why custom types need `registerFallbackValue`. An unstubbed call returns `null` unless you opt in with `throwOnMissingStub`. ## What decides the choice - **Build pipeline.** A project already running `build_runner` for `json_serializable` or `freezed` adds mockito at almost no cost. A project with no generation avoids a step that has to be re-run whenever a mocked interface changes. - **Stale generated files.** A `.mocks.dart` file out of date with the interface either fails to compile (a changed method signature no longer matches the generated override) or silently lacks overrides for newly added members, which then fall through to `noSuchMethod`; teams either commit the files and regenerate in CI or generate them before every test run. - **Ergonomics.** mockito reads slightly shorter (`when(api.x(any))`); mocktail needs closures and a `setUpAll` of fallback registrations for custom types. - **Ecosystem.** `bloc_test` pairs with mocktail in its documentation, and the Flutter docs' architecture case study uses mocktail for a router mock, while the Flutter cookbook's HTTP example uses mockito with `@GenerateMocks`. - **Mixed codebases.** Both libraries define `Mock`, `when` and `verify`, so a single test file should import one of them only. Neither is more capable for everyday stubbing and verification. The decision is about where the cost sits: a build step with generated source, or runtime setup inside each test file. ## Side by side | Concern | mockito 5 | mocktail 1 | |---|---|---| | Mock class | generated into `*.mocks.dart` | one hand-written line | | Build step | `dart run build_runner build` | none | | Stub syntax | `when(api.fetchForecast(any))` | `when(() => api.fetchForecast(any()))` | | Custom-type matchers | work through widened parameters | need `registerFallbackValue` | | Default for an unstubbed call | throws (`@GenerateMocks`) or legal default (`@GenerateNiceMocks`) | returns `null` | | Interface change | regenerate; a stale file may not compile | nothing to regenerate | ## Migrating between them Because the verbs match, moving a suite from mockito to mocktail is mostly mechanical: replace the annotation and generated import with `extends Mock implements`, wrap each `when` and `verify` argument in a closure, change `any` to `any()` and `argThat(m)` to `any(that: m)`, and add fallback registrations for custom types. The reverse is equally mechanical. Doing it file by file is safe as long as no single file imports both libraries.
- Why can mockito's any be a getter returning null when mocktail's any() cannot?mockito's generated override of each method declares its parameters nullable, so passing `null` during `when(...)` is type-correct. mocktail has no generated override; the call hits your interface's non-nullable signature, so `any()` must return a real instance of the type.
- fetchForecast's parameter changes from String to ForecastRequest and the mockito tests stop compiling. What happened and what is the fix?The generated `.mocks.dart` file still overrides `fetchForecast` with the old parameter type, which is no longer a valid override. Re-run `dart run build_runner build` to regenerate it, and have CI regenerate or check the generated files so drift is caught at once.
saying these in an interview costs you the question
- Believes mockito still works on null-safe code without code generation
- Thinks @GenerateMocks mocks return null for unstubbed calls
- Writes when(() => api.fetchForecast(any)) with mockito's generated mocks
- Claims mocktail is more powerful for verification than mockito
- Imports both mockito and mocktail into the same test file