jextest er et såkalt DSL (domenespesifikt språk) for å gjøre det lettere å skrive JUnit-tester.

Mange av oppgavene på denne wiki'n lenker til JExercise-tester, slik at en kan teste koden en skriver undervies og se om den tilfredsstiller kravene i oppgaven. jextest er et eget språk, som er bygget på Java og som kan brukes for å skrive slike tester. Målet med jextest er tredelt:

  • gjøre testene lettere å forstå - logikken bak testene skal kunne leses og forstås godt nok til at en både skjønner hva og hvordan ens kode testes og forstår feilmeldingene en får når testen oppdager feil
  • gjøre det raskere å skrive tester - for å kunne produsere et stort utvalg av oppgaver med tilhørende tester er det viktig at det ikke tar lang tid å skrive en test
  • senke terskelen for å skrive tester - det er fint om læringsassistenter og andre med mindre erfaring kan bidra med oppgaver og tester

jextest er basert på idéen bak objekttilstandsdiagrammer: at objekter forandrer tilstand ved kall på metoder og at oppførselen kan spesifiseres/beskrives med eksempler på sekvenser av slike tilstandsoverganger. Hvis man har tegnet et objekttilstandsdiagram som er dekkende for forventede oppførsel, så skal det å kode en test på at denne oppførselen er korrekt implementert være nokså rett frem.

Oppførselen til Counter-klassen fra Tilstand og oppførsel kan for eksempel beskriveses med følgende diagram og testes med jextest-koden til høyre

1.1:Countercounter = 1end = 31.1:Countercounter = 2end = 31.1:Countercounter = 3end = 3getCounter() => 1count()getCounter() => 2count()getCounter() => 3count()
test stateandbehavior.Counter

instance Counter counter = new Counter(1, 3)

sequence example {
	-->
	state {
		getCounter() == 1
	}
	-- count() -->
	state {
		getCounter() == 2
	}
	-- count() -->
	state #final {
		getCounter() == 3
	}
	-- count() --> #final
}

Diagrammet helt til venstre illustrerer oppførselen til Counter-klassen ved å vise hvordan et Counter-objekt endrer tilstand når count()-metoden kalles, inntil counter-verdien har nådd slutt-verdien (end).

jextest-koden til venstre beskriver hvordan en instans opprettet med new Counter(1, 3) drives gjennom en sekvens med tilstander ved kall til count()-metoden, inntil siste tilstand gjentas. jextest-koden er på en måte en tekstlig formulering av diagram-logikken, som kan oversettes til Java og utføres som en vanlig JUnit-test.

Hver sekvens oversettes til én test-metode som utfører koden i transisjonene og tester tilstanden før og etter. F.eks. vil sekvensen med navn example i eksempet gi opphav til metoden testExample(), som JUnit-rammeverket gjenkjenner som en testmetode. Hver tilstand er en sekvens med (som oftest logiske) uttrykk, som oversettes til kall til en assert-metode (akkurat hvilken avhenger av typen uttrykk).

Referanser til typer og koden for uttrykk og setninger som brukes i transisjoner og tilstander følger syntaksen til Xbase-språket (del av Xtext-rammeverket), som gir kompakt og lettlest kode. Du kan lese om syntaksen til Xbase-uttrykk her: http://www.eclipse.org/Xtext/documentation.html#xbaseLanguageRef_Introduction

 

Hovedstrukturen til jextest-filer

Tabellen under oppsummerer syntaksen til jextext-filer.

test <navn på klasse som testes> [with <navn på testklasse>] [<beskrivelsestekst>] [@ <nettadresse>]

test-deklarasjonen inneholder som regel bare navnet på klassen som testes, men en kan også eksplisitt angi navnet på testklassen som genereres. I tillegg kan en legge inn en beskrivelse/dokumentasjonstekst og en nettadresse, f.eks. til wiki-siden for oppgaven.

test-deklarasjonen etterfølges av en import-del og en eller flere instance-, state-, sequence- og method-deklarasjoner.

import <type>Importerer type(navn), slik at en kan referere til dem uten pakkenavnet som prefiks.
instance ([<type>] <navn på instans>)+instance-deklarasjoner lister opp type og navn til instansene som testes. Hvis typen ikke oppgis, så brukes typen til klassen som testes. Hvis jextest-fila ikke inneholder noen instance-deklarasjoner, så får en automatisk én instans av typen som testes med navn like typenavnet (men med liten forbokstav, slik konvensjonen er).
state method [<type>] <navn> (<argumentliste>) { <uttrykk>* }state method-deklarasjonen definerer en tilstandsmetode, som tester tilstanden til en instans. Denne kan kalles fra state-deklarasjoner inni sequence-blokker, for å gjøre dem kortere. state method-deklarasjonen angir type til instansen (som testes av metoden), navn og argumentliste, som for vanlige metoder. Hvis typen ikke oppgis, så brukes typen til klassen som testes. Uttrykkene er som for state-deklarasjoner (se nedenfor).

sequence <navn> {
   [instance ([<type>] <navn på instans>)+]
   <før-tilstand>
   (
   <transisjon>
   <etter-tilstand>
   )+
}

sequence-blokken definerer en sekvens av tilstander og transisjoner som sammen tester oppførsel til et sett instanser for et gitt brukstilfelle. Inni sequence-blokken kan en ha flere instance-deklarasjoner, for instanser som bare brukes av i denne test-sekvensen.

Test-delen av sequence-blokken starter med en før-tilstand, som etterfølges av ett eller flere par av transisjoner og etter-tilstander. Det finnes også en variant som bare inneholder én tilstand og som er nyttig for å teste konstruktører. I dette tilfellet blir test-delen bare --> <tilstand>

En tilstand er vanligvis en state-deklarasjon, men det kan også være en referanse til en state-deklarasjon som er angitt med # <tilstandsnavn>.

state [#<navn>] [instansnavn]
(
{
   <uttrykk>+
}
)+

state-deklarasjonen angir en sekvens av uttrykk som tester tilstanden til en instans. Instansnavnet kan utelates, når det bare er deklarert én instans for denne test-sekvensen. Hvis en oppgir #<navn> så kan en referere til denne tilstanden et annet sted, istedenfor å måtte ha en ny state-deklarasjon med de samme uttrykkene.

Hvert uttrykk tolkes som en utsagt om instansens tilstand og blir oversatt til bruk av JUnit-rammeverkets assert-metoder.  Det vanligste er varianten <uttrykk1> == <uttrykk2>, som blir oversatt til assertEquals(<uttrykk2>, <uttrykk1>) (merk rekkefølgen). Andre logiske uttrykk blir oversatt til assertTrue(<uttrykk>), mens uttrykk uten type (void), f.eks. kall til en hjelpemetode blir antatt å selv bruke assert-metoder og blir ikke oversatt.

-- <uttrykk> (, <uttrykk>)+ -->
--
<uttrykk> (, <uttrykk>)+ throws <unntaksklasse>

Dette angir en transisjon og er ment å ligne en pil med én eller flere uttrykk (skilt med komma) på/inni, som peker på etter-tilstanden.

Den andre varianten uttrykker at effekten av transisjonen er et unntak av en bestemt klasse og ikke en etter-tilstand. Da må en også utelate den (vanligvis) påfølgende etter-tilstanden.

method <returtype> <navn> (<argumentliste>) { <uttrykk>* }method-deklarasjonen definerer hjelpemetoder, som kan brukes for beregninger eller testkode som blir for komplisert til å ha inni tilstander og transisjoner. method-deklarasjonen angir returtype, navn og argumentliste, som for vanlige metoder.

Eksempler på jextest-kode

Nedenfor følger eksempler på jextext-kode, hentet fra oppgaver på wiki'n.

test stateandbehavior.Account

sequence constructor "Konstruktør" {
	-->
	state {
		balance == 0;
	}
}

sequence deposit "Innskudd" {
	"Setter inn 100 kr." -- deposit(100) -->
	state {
		balance == 100;
	}
}

sequence depositNegative "Negativt innskudd" {
	"Setter inn -50 kr." -- deposit(-50) -->
	state {
		balance == 0;
	}
}

sequence addInterest "Legge til renter" {
	"Setter rentefoten" -- interestRate = 5 -->
	state {
		balance == 0;
		interestRate == 5;
	}
	"Setter inn 100 kr." -- deposit(100) -->
	state {
		balance == 100;
	}
	-- addInterest -->
	state {
		balance == 105;
	}
}

method == (double d1, double d2) {
	val epsilon = 0.000001d
	d1 >= d2 - epsilon && d1 <= d2 + epsilon
}

 

 

Account-oppgaven

Denne jextest-koden tester Account-klassen i pakken stateandbehavior.

Siden det ikke er noen instance-deklarasjon så defineres en instans av Account med account som navn. Denne blir implisitt i state-blokkene i testene.

Nederst defineres en hjelpemetode, som redefinerer ==-operatoren for double-verdier. Dette gjør at alle testene av balance og interestRate, som begge er double-verdier, kaller denne metoden og i praksis tar høyde for avrundingsfeil.

constructor-sekvensen  sjekker bare den initielle tilstanden til den implisitte Account-instansen og vi bruker derfor -->-notasjonen.

deposit-sekvensen sjekker tilstanden etter et kall til deposit(100) på den implisitte Account-instansen.

depositNegative-sekvensen sjekker tilstanden etter et kall til deposit(-50) på den implisitte Account-instansen, som i dette tilfellet skal være uendret.

addInterest-sekvensen setter først rentefoten med en tilordning. I denne oppgaven legger vi opp til at det ikke brukes synlighetsmodifikatorer, så dette er enkel tilordning av interesRate-feltet. Deretter kalles deposit(100) og addInterest. Etter begge disse trinnene sjekkes at tilstanden er korrekt.

 

 

  • No labels