BookinglyTech News
Software

Mockito obliga a pasar por build_runner en cada test de Dart; mocktail no

Desde null safety, mockito no puede resolver los mocks en tiempo de ejecución y arrastra la generación de código con build_runner. mocktail hace lo mismo sin codegen ni ficheros .mocks.dart.

2 min de lecturaDev.to0 vistas

En Dart, si un equipo escribe sus mocks con mockito, cada cambio en una interfaz le devuelve a la terminal con dart run build_runner build --delete-conflicting-outputs. Son 15 o 30 segundos en un proyecto mediano y hasta dos minutos en un repositorio Flutter grande, según los tiempos que maneja el autor de la nota. mocktail hace lo mismo sin generar una sola línea de código.

De dónde sale el peaje de build_runner

Antes de null safety, mockito creaba mocks en tiempo de ejecución: bastaba con class MockRepo extends Mock implements Repo {} y noSuchMethod se encargaba del resto, sin generación de código. Dart 2.12 lo rompió. Si un método de la interfaz devuelve un User no nulable, noSuchMethod no puede devolver null mientras espera al stub y el sistema de tipos lanza el error antes de que se registre nada. dart:mirrors está desactivado en Flutter por rendimiento y tree-shaking, así que la reflexión en tiempo de ejecución no era una salida. La opción que quedaba fue generar los mocks en tiempo de compilación.

El resultado son tres costes concretos. El primero es el bucle de iteración: el TDD vive de respuestas en milisegundos y aquí cada firma nueva cuesta decenas de segundos, con lo que la gente deja de ejecutar los tests a menudo y los agrupa para el final. El segundo son los ficheros .mocks.dart, un hermano generado por cada test con mocks, con miles de líneas que ensucian las revisiones y que revientan en conflictos de merge cuando dos ramas tocan la misma interfaz. El tercero es el acoplamiento al anotar con @GenerateMocks([...]) cada dependencia nueva.

Qué cambia con mocktail

mocktail lo firma Felix Angelov, conocido por Bloc y Very Good Ventures. Mantiene la API de mockito, pero se apoya en dos cosas nativas del lenguaje: las interfaces implícitas de Dart —toda clase define una sin necesidad de heredar su implementación— y los closures para el stubbing, que retrasan la evaluación de la llamada hasta que la librería puede interceptarla y devolver el valor registrado.

La migración es casi un cambio de sintaxis: when(repo.name).thenReturn('x') pasa a when(() => repo.name).thenReturn('x'), anyNamed('id') a any(named: 'id') y verify(repo.login()).called(1) a verify(() => repo.login()).called(1). La clase del mock se declara en el propio fichero de test y se ejecuta.

Ojo con las cifras: los tiempos son del autor, no de un benchmark publicado, y esto es la primera parte de una serie en un blog, no el anuncio de una versión. Aun así, la pregunta que deja planteada es razonable para cualquier equipo con un monorepo Flutter grande: cuánto del ciclo de desarrollo se va en generar código que solo existe para los tests. Medirlo antes de mover nada es lo sensato.