HomeBlogHoe je je widget testing setup omzet in een widget library met Widgetbook
Widgetbook screenshot
#Strategy
Bas - DeveloperBas de Vaan
8 augustus 2023

Hoe je je widget testing setup omzet in een widget library met Widgetbook

Laatst was ik geïnspireerd door een presentatie van Lucas over Widgetbook. Daardoor ben ik Widgetbook gaan toepassen in onze huidige projecten, en probeerde daarbij de hoeveelheid nieuwe code te minimaliseren en de onderhoudskosten zo laag mogelijk te houden. In deze blog deel ik de aanpak die ik heb gebruikt.

Key takeaways
  • Widgetbook is een gegenereerde UI component library, geïnspireerd op Storybook.js
  • Meestal is het voor kleine projecten tijdrovend om een UI component library bij te houden
  • Widgetbook gebruiken in je tests verlaagt de onderhoudskosten van je UI component library

Widgetbook is een UI component library geïnspireerd op Storybook.js. Het toont UI componenten in een duidelijke folderstructuur. Deze componenten worden gegenereerd uit je daadwerkelijke code, dus je kan precies zien hoe ze er in je applicatie uit zien. De component library is interactief, waardoor je kan testen hoe elementen reageren op user input, hovers en muisklikken. Je kan ook verschillende variaties en opties make voor je component zodat je inzichtelijk kan maken welke varianten van het element er beschikbaar zijn. Je kan de library online hosten om het te delen met klanten, andere developers en designers. Als je benieuwd bent naar de potentie van een widget book, check dan hier hun online demo!

De uitdaging van het onderhouden van een widget library

Ik herinner me dat ik bij een Javascript meetup ooit een talk heb gehoord over Storybook. Ik dacht toen ‘Dit zou fantastisch zijn in Flutter!’ Tot mijn verrassing bleek dit maanden later te bestaan! Lucas van Widgetbook gaf een fantastische presentatie over de tool op een flutter meetup. Het leek makkelijk bruikbaar en veelbelovend, met nog meer potentie in verwachte functionaliteiten. Het enige waar ik mee zat, waren de onderhoudskosten. Voor elke entry in Widgetbook moet je een use case definieren en veranderingen in je widgets bijhouden. Dit is een voorbeeld van hoe je een use case definieert in Widgetbook:

@widgetbook.UseCase(name: 'with no text', type: TestFieldWidget)
Widget noTextTestFieldWidget(BuildContext _) {
  return const TestFieldWidget(text: '');
}

Dus hoewel deze use case vrij compact is, kan je je voorstellen dat een widget met veel opties snel omslachtig kan worden. Hier is een voorbeeld van een grotere widget (maar nog steeds een simpele checkbox):

@UseCase(name: 'basic', type: CheckboxInput)
Widget basicCheckboxInput(BuildContext context, {bool initialValue = false}) {
  return CheckboxInput(
    field: CheckBoxFieldType(
      name: 'checkbox',
      value: context.knobsOrMock.boolean(label: 'checkboxValue', initialValue: initialValue),
      type: FieldType.bulletin,
      label: context.knobsOrMock.string(
        label: 'CheckboxLabel',
        initialValue: 'checkbox',
      ),
    ),
    onChanged: (bool? changed) {
      initialValue = changed ?? false;
    },
  );
}

Je snapt ‘m al. Om elk element in Widgetbook weer te geven, moet je een function met use case annotation maken. Gelukkig doet de Widgetbook code generation tool de rest van de magie voor je 🔮.

Ondanks dat, voelde het maken van een Widgetbook op deze methode als meer werk dan ik bereid was erin te stoppen. In een ideaal scenario, doe ik zo min mogelijk werk. Dat bracht me tot het idee om het in te bouwen in mijn widget testing.

Widgets en testing combineren

In de meeste van mijn apps heb ik al een robuuste widget testing setup. We gebruiken zowel Widget testing en Golden Testing om de interactie en look & feel van onze widgets te testen. Wanneer een wijziging is, komt dat omhoog in onze tests, wat ons meer zekerheid geeft bij het ontwikkelen van nieuwe features. Als een verandering een ongewenste bijwerking heeft op een andere widget, worden onze test rood en geven ze een waarschuwing. Als je je widgets nog niet test, is het een stevige aanrader om dat eens te proberen! Golden Tests zijn simpel te onderhouden en voelen als weinig moeite voor waardevolle output.

Maar wat hebben deze tests overeen met de use cases in Widgetbook? In beide gevallen, moet je de widget bouwen die je wil, het isoleren van de rest van je applicatie en aan het framework koppelen, of dat hou Widgetbook is of je testing library. Hierdoor ben ik gaan zoeken naar een manier om de use cases van Widgetbook en mijn tests te combineren. En ik heb een oplossing gevonden die ‘min of meer’ werkt.

Als je de functies de use case functies gewoon in je test file zet, kan je deze functies als gebruiken in in test en tegelijk annoteren. Een simpele test ziet er dan bijvoorbeeld zo uit:

@widgetbook.UseCase(name: 'with a lot of text', type: TestFieldWidget)
Widget longTextTestFieldWidget(BuildContext _) {
  return const TestFieldWidget(text: 'A very long test to create a certain amount of text to see how it looks like.');
}

@widgetbook.UseCase(name: 'with no text', type: TestFieldWidget)
Widget noTextTestFieldWidget(BuildContext _) {
  return const TestFieldWidget(text: '');
}

void main() {
  testWidgets(
    "test description",
    (WidgetTester tester) async {
      Widget widget = Builder(builder: ((context) => longTextTestFieldWidget(context)));
      await tester.pumpWidget(widget);
      expect(
          find.text('A very long test to create a certain amount of text to see how it looks like.'), findsOneWidget);
    },
  );
}

Het is ook mogelijk om een initial value van een knop in de functie mee te geven als optionele parameter; hierdoor is het mogelijk om de ‘knop’ te gebruiken in je test. Een test voor een checkbox die we gebruiken in een echt project ziet er zo uit:

@UseCase(name: 'basic', type: CheckboxInput)
Widget basicCheckboxInput(BuildContext context, {bool initialValue = false}) {
  return CheckboxInput(
    field: CheckBoxFieldType(
      name: 'checkbox',
      value: context.knobsOrMock.boolean(label: 'checkboxValue', initialValue: initialValue),
      type: FieldType.bulletin,
      label: context.knobsOrMock.string(
        label: 'CheckboxLabel',
        initialValue: 'checkbox',
      ),
    ),
    onChanged: (bool? changed) {
      initialValue = changed ?? false;
    },
  );
}

void main() {
  group('Checkbox Input Golden Test', () {
    GoldenPumpingCompanion companion = defaultPumpingCompanion();

    goldenTest(
      'renders correctly',
      fileName: 'checkbox_input_golden_test',
      pumpWidget: companion.pumpWidget,
      builder: () => companion.buildTestGroup(
        'default widget',
        UseCaseWrapper.wrap((context) => basicCheckboxInput(context, initialValue: false)),
      ),
    );

    goldenTest(
      'renders correctly',
      fileName: 'checkbox_input_checked_golden_test',
      pumpWidget: companion.pumpWidget,
      builder: () => companion.buildTestGroup(
        'default widget',
        UseCaseWrapper.wrap((context) => basicCheckboxInput(context, initialValue: true)),
      ),
    );
  });
}

Houd er wel even rekening mee dat ik hier wat extra test helpers en functies heb die me ondersteunen bij het bouwen van de tests. Zo ziet zelfs een “ingewikkelde” widget test er simpel uit en biedt ons bovendien snapshot tests én een entry in ons Widgetbook! Bekijk mijn GitHub repository als je op zoek bent naar een simpel voorbeeldproject.

Implementeren van de oplossing

Okay, ik moet wel even iets opbiechten. Toen ik eerder zei dat ik een ‘min of meer’ werkende oplossing had, bedoelde ik eigenlijk dat het nog niet werkt. De code generation tool van de Widgetbook library kan nog niet correct omgaan met de imports van de test en lib folders. Dit kan je oplossen door handmatig de imports in de door Widgetbook gegenereerde file aan te passen. Verwijder de imports die starten met asset: en vervang ze met de eigenlijke imports uit je project. Zo kan Widgetbook draaien, maar ik hou er niet van om handmatige aanpassingen te maken aan gegenereerde code. Dat is in het bijzonder een probleem als je Widgetbook automatisch wil deployen in een automatische pipeline. Toen ik dit ontdekte heb ik een issue voor dit probleem geopend in de Widgetbook Github repository.

Ik heb deze blog geschreven terwijl ik op Fluttercon 2023 was (lees daar meer over in mijn vorige blog). Tijdens deze conferentie heb ik een talk over Widgetbook kunnen bijwonen en hun booth bezocht. De mannen van Widgetbook waren er ook. Tijdens de conferentie heb ik ook een oplossing bedacht voor het probleem.

Ik kon mijn oplossing en het issue bespreken met de developers, die heel vriendelijk waren en open stonden voor samenwerking. Al met al, een toffe ervaring 🙂

Bas met het Widgetbook team

En terwijl ik deze blogpost nog aan het schrijven was, heeft Youssef (rechts op de foto) mijn pull request al gebruikt als basis voor een fix voor Widgetbook!

Screenshot van de merge request

Dus binnenkort kan je dit zelf testen in een nieuwe release (or in de git ref)! Awesome! 🚀

EDIT: Op 18 augustus is deze feature gereleased in Widgetbook Generator 3.1.0! Dus je kan nu updaten en het zelf uitproberen!

Wrapping up

En als we het dan nog even hebben over de toekomst van Widgetbook: versie 3 is recent uitgebracht met veel nieuwe features en een volledig uitgeruste cloud omgeving. Daarom is het een goed idee om Widgetbook nu te implementeren, in het bijzonder voor apps met een grote collectie aan UI elementen.

Als je meer wil weten over hoe we Widgetbook hebben geïmplementeerd bij Dutch Coding Company of benieuwd bent hoe ik naar de toekomst van Widgetbook kijk, stuur me dan vooral een berichtje op Twitter (@Bassiuz). Ik praat graag over alles wat met Flutter te maken heeft :)

Dit vind je misschien ook interessant:

Laat je project niet stranden omdat een goede strategie ontbreekt. Kijk of jouw idee klaar is voor ontwikkeling met onze Digital Readiness Scan. Binnen 5 vragen weet je wat je volgende stap naar succes is.

Naar de scan