Slik gjør du kjedelig testdata gøy, med Jsonnet

- Alle er misfornøyd med testdataen sin, mener Jostein Hellerud, som lager digre JSON-filer med Jsonnet.

Jostein Hellerud viser deg hvordan du genererer digre JSON-filer enkelt med Jsonnet. 📸: Privat
Jostein Hellerud viser deg hvordan du genererer digre JSON-filer enkelt med Jsonnet. 📸: Privat Vis mer

Som utleid konsulent har jeg rukket og være i en del forskjellige team i løpet av min så langt ganske korte karriere. Det har vært forskjellige teknologier, størrelser, snittalder, foretrukket arbeidsmetodikk, cloud-preferanser og grad av internasjonalitet.

Det de alle likevel har til felles, er at de har vært misfornøyd med dataen i testmiljøene sine.

For meg virker det som det er tre fremgangsmåter til testdata som rår:

«Ulempen er at det er fryktelig kjedelig å skrive inn mye data i JSON-filen, slik at testdataen ender med å bli ganske begrenset.»
  • Den første, som alle er enige om at de ønsker, men av diverse grunner ikke får til, er produksjonsdata. Her snakker man ofte om å vaske data fra produksjons-databasen slik at den består alle GDPR-krav, for å så flytte den over til testmiljøet. I praksis viser dette seg ofte for vanskelig og så langt har jeg aldri sett dette forlate teoretiske diskusjoner for å bli en del av en utviklers hverdag.
  • Den andre fremgangsmåten er et testmiljø som har eksistert siden tidenes morgen og hvor utviklere legger inn ny data rett i databasen når de trengs for å teste nyutviklet funksjonalitet. Her er det ofte rikelig med testdata, men veldig lite av det kan faktisk stoles på. Tester feiler oftere på grunn av at man brukte harsk data, enn at det faktisk er noe galt med funksjonaliteten. Ryktene i bedriften sier kanskje at hvis man tester med brukeren «Foo Bar» i firmaet «Tester123» får man best resultater, mens andre kan påstå at de fikk det til å funke med brukeren «Test Hest». Ingen er fornøyd i en sånn situasjon. Jobben med å resette databasen og legge inn ny data er ikke i Jira-boardet til noen av teamene, og dessuten er det kjedelig.
  • En tredje måte å løse dette på er med en database som resettes jevnlig og seedes basert på en JSON-fil som er sjekket inn i kildekontrollen. Denne fremgangsmåten har mange fordeler, blant annet at man slipper å miste kontroll på den måten som beskrevet over. Ulempen er at det er fryktelig kjedelig å skrive inn mye data i JSON-filen, slik at testdataen ender med å bli ganske begrenset. En bruker per rettighet, en organisasjon og litt ekstra avhengig av hva man lager er ofte alt noen har tålmodighet til. Det er også et problem at en endring i databasestrukturen vil kreve at noen setter seg ned og skriver om hele JSON-filen, hvilket gjør at den fort blir utdatert uansett.

Dette tar meg til noe jeg snublet over for litt siden, og har både brukt og reklamert mye for i etterkant.

Hva er Jsonnet?

Jsonnet er et såkalt «data templating language». Om det virker litt overkill for testdata så kan det være lettere å bare se på det som JSON på steroider.

Jsonnet er et programmeringsspråk som «kompileres» til JSON-filer. Syntaksen er så lik at all JSON er gyldig Jsonnet, men Jsonnet utvider notasjonen med en del funksjonalitet som gjør produksjon av store og kompliserte JSON-filer til en mye artigere opplevelse.

Rent konkret utvider Jsonnet JSON med «Variables, Conditionals, Arithmetic, Functions, Imports and Error Propagation” (jsonnet.org). Alle disse har sin verdi når man lager litt kompliserte JSON-filer. I tillegg til dette kommer språket med et rikt standard bibliotek, med det meste fra «Flat Maps» til «Assert Equal». Språket er godt dokumentert, er utviklet av Google og ligger åpent på deres Github.

Jsonnet kan altså være det som får den tredje testdata-strategien til å fungere! Ikke bare gjør Jsonnet at det er fint mulig å lage 10 000 brukere i seedingdataen deres, det gjør det også, etter min mening, mye morsommere å drive med. Dette løser også utfordringen som dukker opp hvis databasestrukturen endres. Man vil da enkelt kunne skrive inn endringen i Jsonnet og generere testdaten på nytt.

Slik brukes Jsonnet

La oss gjøre det litt mer konkret med noen eksempler.

En enkel måte å lage 400 brukere kan gjøres på denne måten ved hjelp av Norges 20 vanligste fornavn og etternavn:

local userData = import 'userData.libsonnet';

{
  users: [
    {
      name: userData.generateNameFromIndex(firstNameIndex,lastNameIndex)
    }
    for firstNameIndex in std.range(0,userData.firstNamesLength()-1)
    for lastNameIndex in std.range(0,userData.lastNamesLength()-1)
  ],
}

For å få til dette har jeg laget en lib-fil (.libsonnet) som inneholder navn samt noen funksjoner for å benytte disse navnene.

Denne lille kodesnutten produserer 1205 JSON -injer på dette formatet:

{
   "users": [
      {
         "name": "Emma Hansen"
      },
      {
         "name": "Emma Johansen"
      },
      {
         "name": "Emma Olsen"
      },
...

Fra dette ligger egentlig testverden for dine føtter. Det er enkelt ved hjelp av conditionals og functions å lage variert data som etter beste evne matcher det dere har i produksjon. Vi kan for eksempel legge på noen bedrifter, unik ID, rettigheter, favorittmat, epost og litt mer realistisk oppdeling av navn.

Dette gir oss følgende i Jsonnet:

local userData = import 'userData.libsonnet';
local companyData = import 'companyData.libsonnet';

{
  users: [
    {
      id: companyIndex+''+lastNameIndex+''+firstNameIndex,
      displayName: userData.generateNameFromIndex(firstNameIndex,lastNameIndex),
      firstName: userData.generateFirstNameFromIndex(firstNameIndex),
      lastName: userData.generateLastNameFromIndex(lastNameIndex),
      company: companyData.generateCompanyNameFromIndex(companyIndex),
      accessRights: companyData.generateAccessRights((firstNameIndex+lastNameIndex)*(companyIndex+1)),
      email: std.asciiLower(
        userData.generateFirstNameFromIndex(firstNameIndex)+
        "."+
        userData.generateLastNameFromIndex(lastNameIndex)+
        "@"+
        companyData.generateCompanyEmailFromIndex(companyIndex)),
      lovesToEat: userData.generateLovesToEatFromIndex(firstNameIndex)
    }
    for firstNameIndex in std.range(0, userData.firstNamesLength()-1)
    for lastNameIndex in std.range(0, userData.lastNamesLength()-1)
    for companyIndex in std.range(0, companyData.companiesLength()-1)
  ],
}

Som produserer dette resultatet i JSON:

{
   "users": [
      {
         "accessRights": "super user",
         "company": "The red company",
         "displayName": "Emma Hansen",
         "email": "emma.hansen@theredcompany.com",
         "firstName": "Emma",
         "id": "000",
         "lastName": "Hansen",
         "lovesToEat": "pizza"
      },
...

Vi er nå oppe i over 20.000 JSON-linjer som inneholder 2000 brukere fordelt over fem firma. Fem av disse er superbrukere, 500 liker pizza, 100 heter Emma, men bare en har mailadressen «emma.hansen@theredcompany.com».

For å generere aksessrettighetene har jeg laget en hjelpefil (companyData.libsonnet) for å holde kode relatert til bedriftene. Her inne finner vi blant annet funksjonen generateAccessRights som det kan være interessant å ta en rask kikk på her:

local accessRights = [
  "super user",
  "admin",
  "write",
  "read"
];

local generateAccessRights = (function(number)
  if std.mod(number, 200) < 1 then accessRights[0] else 
  if std.mod(number, 10) < 2  then accessRights[1] else
  if std.mod(number, 10) < 6 then accessRights[2] else
  accessRights[3]
);

Ved litt artig bruk av modulo og Jsonnets if-setning kan vi få en fin spredning på aksessrettighetene til brukerne. Et firma på 400 vil her ha en superbruker, 103 administratorer, 168 brukere med skriverettigheter og 128 med bare leserrettigheter.

Jeg har laget et Github-repo med flere eksempler på hvordan man kan bruke Jsonnet til å lage testdata. Der ligger også filene jeg har brukt til å lage eksemplene i denne artikkelen. Ta gjerne en titt hvis det er av interesse. Videre finner dere mye gode resurser på jsonnet.org.

«Det viktigste for meg er at det er mange alternativer til å skrive en 20.000 linjers JSON-fil for hånd.»

Jsonnet til mer enn testdata

Det er verdt å nevne at Jsonnet selvsagt kan brukes til veldig mye annet enn testdata.

Stort sett alt som er JSON, kjedelig og repetitivt kan forbedres. Jeg har brukt Jsonnet til å lage Grafana-dashboards i det siste. Jeg ser at det brukes til deploy pipelines, konfigurasjonsfiler for f.eks. Kubernetes og metadata for Hasuras graphql løsning. Mulighetene er mange.

Jeg ønsker også å legge til at er det ikke utelukkende gull og grønne skoger med Jsonnet. Språket er laget slik at to kjøringer av den samme filen alltid vil produsere det samme resultatet. Med andre ord er det ikke mulig å få et tilfeldig tall, eller generere en UUID, hvilket kan være kjedelig til tider.

Det er også andre ting med språket som kan gjøre det utfordrende for enkelte bruksområder. Heldigvis finnes det mange alternativer til Jsonnet. På sidene sine ser de på alternativer som kan være verdt å sjekke ut.

De ivrigste av dere vil kanskje si at dette er mulig å lage selv i et annet foretrukket programmeringsspråk og det vil dere selvsagt ha helt rett i. Det viktigste for meg er at det er mange alternativer til å skrive en 20.000 linjers JSON-fil for hånd, som kan gjøre hverdagen som utvikler lettere.