Lessen uit het schrijven van Flutter widget testen

Het schrijven van Flutter widget testen blijkt na jaren ervaring met unit testen niet so eenvoudig als ik vooraf dacht. Tijd om wat lessen op een rijtje te zetten.

Mijn grootste verrassing was dat de Flutter widget testen met een interne synchrone klok werken, en het aan de test code zelf is om de klok vooruit te spoelen. Hierdoor lopen widget testen effectief sneller dan real-time, en kunnen tijdgebonden acties gewoon met synchrone breakpoints worden gedebugged. Een uitzondering hierop blijkt de Future.delayed() functie te zijn, die zich niets van de widget test klok aantrekt en daarom maar beter niet in de timing van widgets gebruikt kan worden.

In ons project hadden we vanaf het begin besloten om de UI (=widgets) van de logica te scheiden, zodat we de logica met traditionele unit testen kunnen dekken en de echte UI door widget testen. Dit blijkt een goede keuze, want hierdoor zijn de testen van de logica veel stabieler bij wijzigende inzichten over de interactie met de gebruiker. Tevens houden de widget testen de focus op wat er allemaal op het scherm zichtbaar zou moeten zijn. Door het mocken van de logica, is het tevens mogelijk om eenvoudig alle condities te produceren die in de UI een bijzondere visualisatie hebben.

Het flutter_test_ui package blijkt een onmisbare tool te zijn om de WidgetTester in setup functies te kunnen gebruiken. Dit voorkomt dat de setup in elke testWidgets() herhaald moet worden, en het is voor mij daarom wonderbaarlijk dat het standaard flutter_test package dit niet ondersteunt.

Omdat WidgetTester de sleutel is naar de interactie met de widgets, hebben we een extensie op deze class geschreven die shortcuts voor herhalende patronen in onze testen bevat. Hierdoor werden de widget testen veel beter leesbaar.

De intelligentie van GestureDetector was bijvoorbeeld een bron voor verschillende extensies op WidgetTester. Doordat bij het registreren van een tap en double-tap listener pas na een timeout besloten kan worden dat er een tap afgehandeld moet worden, is er een expliciete timeout van kDoubleTapTimeout nodig in de test. Dit is een bron van extra regels code die eigenlijk niets met de werkelijke test te maken hebben, en regelmatig blijkt te leiden tot frustratie waarom de juiste events niet worden herkend.

Een andere bron van extensies is de pumpWidget() functie. Regelmatig hebben widgets parent widgets nodig, bijvoorbeeld om een Material als parent te leveren, maar ook om actions te mocken of providers te injecteren. Voor elk van deze situaties hebben we een extensie geschreven, wat de setup van veel widget testen beter leesbaar heeft gemaakt.

De belangrijkste les die ik heb geleerd is dat het achteraf toevoegen van widget testen net zo moeilijk is als het achteraf (dekkend) toevoegen van unit testen. Hoewel ik nog steeds niet denk dat het doenlijk is om test-first een UX te bouwen, is het verstandig om de widget testen er wel zo snel mogelijk bij te schrijven. En ook hier geldt dat het belangrijker is om individuele widgets te testen dan (bijvoorbeeld) de compositie tot een volledig scherm. Voor dit laatste zijn integratietesten veel beter geschikt.