Skip to content

Build1 publisher2 min readPublished

One new interface method sends a Dart TDD loop back through build_runner

A dev.to post puts the regeneration wait at anywhere from 15 seconds to two minutes, and traces the cause to sound null safety leaving mockito with compile-time code generation as its only available route.

The Engineer · Build desk

Illustration accompanying One new interface method sends a Dart TDD loop back through build_runner

What happened

  • A dev.to post argues against mockito in modern Dart, starting from the moment a new method added to a UserRepository interface leaves the generated mock stale and the test file full of IDE errors.
  • Clearing that state means running dart run build_runner build --delete-conflicting-outputs and waiting 15 or 30 seconds, or maybe two full minutes on what the author calls a large enterprise Flutter codebase.
  • mockito once needed no generation at all, relying on noSuchMethod until Dart 2.12's sound null safety made returning null for a non-nullable return type throw before any stub could register.
  • mocktail, created by Felix Angelov, keeps the same API without generated files by using Dart's implicit interfaces and deferring the mock invocation inside a closure.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • cost The bill falls on whoever runs tests most often, so the tighter a developer's red-green-refactor loop, the more times a day the wait is paid.
  • constraint While Flutter keeps runtime reflection off for tree-shaking, no Dart mocking library can return to the pre-2.12 approach; the choice is generation or deferral.
  • decision A team now picks where a mock's method list gets checked: in a generated file at build time, or at the call site when the closure runs.
  • capability Dropping the generated siblings removes a class of merge conflict that only appears when two branches touch overlapping service interfaces.

The closure is doing all the work here. Under sound null safety, `when(mock.getUser())` evaluates the call first, so `noSuchMethod` has to hand back a `User` before any stub exists to supply one, and returning null throws before the stubbing logic runs [5]. mocktail writes the same intent as `() => mock.getUser()` [12]. The library takes the closure, puts its interception in place, then invokes it, so by the time the call happens there is a registered stub or a fallback value to return [12]. No generated sibling file is needed, because in Dart every class implicitly defines an interface and a mock can implement one without inheriting its implementation [11].

Dart deliberately avoids heavy runtime reflection, and `dart:mirrors` is disabled in Flutter for performance and tree-shaking [6]. With reflection unavailable, mockito took the route that was left: compile-time generation through build_runner [6].

The timings in the post are one developer's experience, not a measurement anyone can replay. He gives 15 seconds, 30 seconds, and maybe two full minutes on a large enterprise Flutter codebase [3]. The top of that range is eight times the bottom [14]. For the two-minute figure to describe your repository, you would need a comparable count of annotated classes and a comparable set of builders in the pipeline; the post does not say whether those runs were cold or incremental. The 15-second figure is the safer one to plan around.

What makes the tax recurring is the trigger. It fires on the interface edit, so how often you pay is set by how you work [1]. Ten regenerations in a day at 30 seconds each is five minutes of waiting, and ten is a normal day for anyone running red-green-refactor against a service layer [15].

Switching costs something too. Every `when` and `verify` call site changes shape, since the invocation has to become a closure [12]. The `@GenerateMocks` lists come out, and the imports of the generated symbols go with them [8][10].

In my view a Flutter app with a fast-moving service layer is better off with the closure. Typing `() =>` costs a keystroke and some unfamiliarity at review; reviewing a few thousand lines of machine-generated boilerplate is nobody's favourite part of a pull request [7]. Where interfaces are stable and the mock list rarely changes, the build cost amortises and this argument gets much weaker. The post's own framing is a flow-state one: it says feedback loops that stretch into tens of seconds lead developers to batch their changes and commit without verifying, with tests run as an afterthought [13].

What to watch

  • Whether a codemod or lint arrives that makes converting an existing mockito suite to mocktail's closure call sites mechanical.
  • Any published build_runner timing on a named Dart repository, cold versus incremental, against which the 15-second-to-two-minute range can be checked.
  • Any move to re-enable runtime reflection in Flutter, which would reopen the pre-2.12 mocking route the post says null safety closed.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories