Torben Mogensen header

OO dur ikke til unit test

I en artikel i dag, fremgår det, at unit test tilsyneladende ikke er sa populært, som man troede, og slet ikke som al den hype, der har været om metoden giver indtryk af. Jeg tror jeg har en delvis forklaring på dette: Unit test og objektorienteret programmering passer ganske enkelt dårligt sammen.

Groft sagt går unit test ud på at teste enkelte klasser/metoder i isolation, men almindelig OO programmering gør dette vanskeligt:

  1. Klasser har ofte "skjult" tilstand i form af felter, der ikke kan tilgås direkte, men kun indirekte gennem metoder, der ikke direkte sætter eller henter værdien, men måske laver test og beregninger inden. Det betyder dels, at det synlige resultat af et metodekald kan afhænge af usynlig tilstand, og at usynlig tilstand kan blive ændret af metodekald. Det giver en ikke-triviel interaktion mellem metoder og tilstand, der gør det vanskeligt at teste dem i isolation og gør det vanskeligt at afgøre, om den skjulte tilstand er ændret efter hensigten.
  2. På grund af nedarvning kan argumenter til en metode have en anden type end specificeret i metodens erklæring, med deraf følgende ændret opførsel af argumentobjektets metoder. Det betyder, at unit test bør teste en metode med argumenter af alle kombinationer af alle mulige subtyper til de erklærede typer. Det er specielt vanskeligt i situationer, hvor man endnu ikke ved, hvilke typer nedarver fra argumenttypen.

Disse to egenskaber er kendetegnende ved traditionel OO programmering, og både skjult tilstand og nedarvning betragtes som god programmeringsskik i de fleste lærebøger om OO programmering. Men de gør det særdeles vanskeligt at designe unit tests, som både giver en bare nogenlunde dækning, og hvor man rent faktisk kan konkludere nogetsomhelst ud fra testens resultat.

Anderledes forholder det sig med polymorft typede funktionelle sprog. Her er resultatet af et funktionskald kun afhængig af argumenterne, så det er nemt at se, om et testtilfælde giver det ønskede resultat. Samtidig giver parametrisk polymorfi garanti for uafhængighed af instantieringen af de polymorfe typer: Groft sagt kan man sige, at hvis en polymorf funktion virker på en instantiering af de polymorfe typer, så fungerer den på alle instanser. Derfor er det ikke nødvendigt at teste på alle mulige instanser.

Nogle funktionelle sprog (f.eks. SML) tillader sideeffekter, der gør funktioners resultater afhængige af mere end argumenterne, men god kodestil i funktionsprogrammering er at bruge sideeffekter så lidt som muligt (i modsætning til OO programmering), så langt de fleste funktioner lader sig nemt unit teste.

Andre funktionsprogrammeringsprog (f.eks. Haskell) udvider parametrisk polymorfi med bounded polymorphism, som ikke har samme polymorfe uafhængighed som omtalt herover. Men selv her kan typen bruges til automatisk generering af testtilfælde.

Så selv om unit test er blevet hyped i OO miljør, så passer det ekstremt dårligt der, og ekstremt godt i funktionsprogrammeringsmiljøer

Kommentarer (43)
sortSortér kommentarer
  • Ældste først
  • Nyeste først
  • Bedste først
Mogens Heller Grabe

Unit test af en klasse passer da perfekt med at man via tests specificerer den kontrakt, som klassen har med omverdenen. Dette gøres ganske naturligt via klassens public metoder, og man sikrer sig på samme måde via disse at klassen opfører sig som forventet.

Mht. polymorfe argumenter, så passer det jo OGSÅ perfekt, idet man blot fremstiller en mock-implementation til at teste med. Hvis man så ellers kan finde ud af at bruge visitor-mønsteret i tilfælde af at forskellige specialiseringer skal behandles forskelligt, så kører det jo derudaf!

Efter min mening så bliver designet helt automatisk bedre og mere OO af at blive skrevet til test.

  • 0
  • 0
Tania Andersen

Jeg er ikke helt sikker på, at du har ret i udsagnet om, at skjult tilstand og nedarvning betragtes som god latin i de lærebøger om OO.

Eksempelvis Josh Blochs "Effective Java," som anses for lidt af en bibel blandt Java-folket, fraråder både nedarvning og tilstande, og den er immervæk fra 2001.

Med venlig hilsen
Tania Andersen

  • 0
  • 0
Carsten Frigaard

Det er vel et fundamentalt valg om man bare vil teste en klasses ydre (public funktioner) eller også fortage test af indre metoder og tilsande (private).

Klart giver den sidste metode mere arbejde, og bryder ind i en klasses flow på en ikke-intuitiv måde. Det er derfor ret besværligt at gøre konsistent...så jeg holder på almindelig test af ydre; der er simplere, og indmaden i klassen kan ændre uden at det i princippet går ud over test-case'ene.

Et andet spørgsmål er så hvad man mener med OO, når nu Tania skriver at Java guidelines fraråder nedarvning...et argument jeg egentlig godt kan følge.

Er det egentlig ikke OO der er problemet og ikke unittest?

  • 0
  • 0
Torben Mogensen Blogger

Hvis man ikke bruger nedarvning og lokal tilstand i klasser, hvad er der så tilbage af OO? Så programmerer man jo funktionelt.

Jeg forstår såmænd godt dem, der fraråder brug af nedarvning og tilstand i OO-programmeringssprog, men det svarer lidt til at fraråde brugen af pointere og heltal i C -- det er en god ide, men det er ikke det, sproget er designet til.

  • 0
  • 0
Torben Mogensen Blogger

Mogens skriver om kontrakter med omverdenen. Min pointe er netop, at disse kontrakter bliver overordentlig komplicerede at specificere og specielt at teste, når der er lokal tilstand i en klasse, og hvor man ikke kan sikre, at argumenterne har konsistent opførsel (da man altid kan give et argument, der er en underklasse med en helt anderledes opførsel).

Jeg forstår til gengæld ikke helt pointen om mock-implementeringer. Mener du virkelig, at en test med en enkelt mock-implementering af en argumenttype sikrer, at klassen virker med alle mulige nedarvninger af argumenttyperne?

Jeg er til gengæld helt enig i, at design bliver bedre af at være skrevet til test. Jeg mener så ikke, at det medfører, at det bliver mere OO -- tværtimod. :-)

  • 0
  • 0
Mogens Heller Grabe

Mener du virkelig, at en test med en enkelt mock-implementering af en argumenttype sikrer, at klassen virker med alle mulige nedarvninger af argumenttyperne?

Jeg mener at ENTEN er det ikke nødvendigt at bruge arv og specialiseringer, eller OGSÅ er det bare en del af din klasses kontrakt at den kalder noget på din argument-klasse. Og det kan du teste at den gør.

Så er det op til de unit tests du har af hver argument-type at verificere at de overholder deres del af kontrakten.

  • 0
  • 0
Torben Mogensen Blogger

Mogens skriver:

"Så er det op til de unit tests du har af hver argument-type at verificere at de overholder deres del af kontrakten."

Det vil sige, at den, der skal lave unit tests for en klasse skal kende kontrakterne for alle de metoder, hvor denne klasse kan bruges som argument.

Det lyder ikke længere som test af hver klasse/metode i isolation, som ellers er en væsentlig del af filosofien i unit test.

  • 0
  • 0
Mogens Heller Grabe

Det vil sige, at den, der skal lave unit tests for en klasse skal kende kontrakterne for alle de metoder, hvor denne klasse kan bruges som argument.

??? Det forstår jeg ikke. Tag f.eks det her:

1: A, givet en B, lover at kalde foo() derpå.

2: C er en B og har en implementation af foo(), der fungerer.

Hver af disse udsagn kan testes i hver deres unit test.

Hvad de i fællesskab kan udføre når de kobles sammen er et andet spørgsmål. Det kan man teste med integrationstest.

  • 0
  • 0
Kim Dalsgaard

Hele ideen med unit-test og (s|r)spec, er at teste ydersiden (og kun ydersiden) af et eller flere objekt(er). Hvad der sker indeni er et udelukkende et implementationsspørgsmål. Dette falder rigtig godt i hak med test-first filosofien - først laves objektets specifikation (testen), og så laves implementationen (koden der får testen til at gå i grøn).

Hvis ikke man er i stand til at beskrive hvorledes objekterne skal opføre sig i enhver tænkelig situation (yderside situation), så har man meget større problemer end manglende unit-tests.

  • 0
  • 0
Torben Mogensen Blogger

"1: A, givet en B, lover at kalde foo() derpå.

2: C er en B og har en implementation af foo(), der fungerer.

Hver af disse udsagn kan testes i hver deres unit test."

Det forudsætter, at alle klasser (f.eks. A), der bruger B, har samme forventninger til foo(). Det sætter meget strenge begrænsninger på, hvad en nedarvning kan lave om på samt i hvilke kontekster, B kan bruges (da de alle skal forvente præcis det samme af foo()).

Men jeg giver dig ret i, at når dette er tilfældet, så er unit test gennemførligt (forudsat, at hverken A eller B har for meget skjult tilstand).

  • 0
  • 0
Michael Rasmussen

Jeg tror mange forveksler unittest med gammeldags whitebox test, hvor den interne logik i en funktion blev aftestet. Unittest er netop kendetegnet ved, at den ikke tester intern logik, men udelukkende ser på kontekst hvori unit indgår. Det kan derfor bedst sammenlignes med gammel blackbox test, dog på et lavere abstraktionsniveau.

Der testes altså på, hvilket output unit producerer i forhold til det modtagne input. Intern tilstand og polymorph egenskabs påvirkning af output er derfor ikke relevant for en unittest, fsv. dette ikke har indflydelse på output.

Jeg ville mere betragte unittest som en avanceret form for grænseværditest.

  • 0
  • 0
Torben Mogensen Blogger

Ja, det er netop pointen. Du tester kun udefra, og kan derfor ikke direkte observere den interne tilstand -- hverken før eller efter testen.

Derfor kan en test med et givet argument og resultat, der opfylder kontrakten, ikke garantere, at samme argumenter også næste gang giver et resultat, der opfylder kontrakten. Da unit tests som regel starter i en veldefineret initialtilstand, tester de ikke metodernes validitet i et program, der har kørt et stykke tid og derfor ikke længere har denne veldefinerede initialtilstand. Du har ret i, at intern tilstand er irrelevant, hvis den ikke påvirker det returnerede resultat. Men hvordan tester du det i en ren ekstern test, hvor du ikke kan sætte eller observere den skjulte tilstand? Og hvis den skjulte tilstand ikke påvirker det observerbare resultat, hvorfor så have den i det hele taget?

Det er i modsætning til rene funktioner, hvor samme argumenter altid giver samme resultat, så testens validitet er uafhængig af programmets samlede tilstand.

Din kommentar om polymorfe egenskaber forstår jeg kanske enkelt ikke. Pointen er jo netop, at parametrisk polymorfi sikrer, at instantieringen ikke påvirker funktionens virkemåde, så en test, hvor man instantierer den polymorfe argumenttype til heltal, også sikrer validitet for det tilfælde, hvor man instantierer den polymorfe argumenttype til f.eks. funktioner fra strings til lister af strings. Den slags garantier har du ikke ved nedarvning -- du bliver nødt til eksplicit at formulere, hvilke krav du har til argumenttypen og teste disse krav for hver mulig instantiering (underklasse) af argumenttypen.

  • 0
  • 0
Mogens Heller Grabe

Derfor kan en test med et givet argument og resultat, der opfylder kontrakten, ikke garantere, at samme argumenter også næste gang giver et resultat, der opfylder kontrakten.

Som generel betragtning er det her naturligvis korrekt - man kan jo aldrig vide, om klassen "husker" f.eks hvor mange gange getLength() er blevet kaldet, for den 42. gang at returnere noget forkert.

Men som regel vil man jo selv skrive sine unit tests, og dermed vil man formentlig teste koden rimelig fornuftigt - f.eks i noget, som man skønner kunne være vanskelige situationer: grænsetilfælde, uventet input, osv.

Hvis man så en dag finder ud af at klassen opfører sig uventet med en given tilstand, så er det jo bare at genskabe tilstanden og fejlen via en test, og så kode til verden bliver grøn igen.

  • 0
  • 0
Rune Juhl-Petersen

man kan jo selv bestemme hvad man vil bruge sine unit tests til. Jeg er selv meget tilhænger af testdriven development hvor udviklerne laver testene samtidigt med de laver selve koden således at koden bliver testbar.

At objekter kan have en skjult tilstand gør ikke unit tests ubrugelige. Hvis man mener den er relevant for sin unit test bliver man nødt til at åbne op for klassen for at den er testbar.

Nedarvning har ingen relevans. Man bliver nødt til at fokusere på hvad det egentligt er man tester. Man er nødt til at fokusere på det niveau der opereres på og ikke teste for alle mulige nedarvninger af en parametre. Nedarvninger synes jeg man skal teste for sig selv i en anden kontekst.

  • 0
  • 0
Ole Østergaard

"Hvis unit tests bliver skrevet med kendskab til implementeringen, så er det ikke længere black box."

Nej, skal det absolut være det? "Rød-grøn-refactor"-cyklen gør at implementationen udvikler sig i takt med at unit-test-suiten udvikler sig, så jeg synes ikke man kan betragte unit-tests som black-box-tests. Det er med at finde den rigtige balance.

"Ideelt set skal unit tests skrives af en anden end implementatoren"

Det var da en pudsig opfattelse. Det er sundt at få reviewet sine unit-tests af andre (ligesom man også bør få al anden kode reviewet af andre), men man bør skrive sine unit-tests ud fra "rød-grøn-refactor"-devisen. Så giver de ganske enkelt mest værdi, da man så "up front" undgår at skrive en masse fejl som andre (og en selv) kommer til at lide under sekundet efter du har committet koden til dit kode-repository.

Og mht. argumenterne om skjult tilstand osv.: Hvad værdi giver kode som måske er meget pæn i din editor, men hvor du ikke aner om det virker? Har du for megen skjult tilstand i dine objekter, så refaktorér til noget mere gennemskueligt.

  • 0
  • 0
Martin Dybdal

Jeg tror at en del har svært ved at se hvorfor objekt-orientered sprog ikke er så velegnet til unit-testing som funktionelle sprog, fordi ikke kender til alle fordelene ved testing i et funktionelt miljø. Jeg har fundet en video med en af Haskell-"bagmændende" (Simon Peyton-Jones), der omhandler netop dette:

http://www.foomongers.org.uk/videos/spj-typedriventestinginhaskell.html

Den er primært rettet mod folk der ikke kender til funktionsprogrammering.
Det vigtigste at få med er nok hans eksempel med test af en SMS-encoder (der pakker 7-bit tegn ned i 8-bit bytes). Se den :-)

  • 0
  • 0
Rune Juhl-Petersen

I har ikke fået mig overbevist om at jeg skal droppe OO til fordel for funktion orienteret programmering. OO giver en enorm fleksibilitet bla. vha. nedarvning. Det er f.eks. helt åbentlyst at bruge OO til at kunne dele en opgave ud på flere personer der så hver især kan udvikle dele af applikationen, unit teste den og til sidst sætte den sammen uden at skulle ændre noget i de dele der allerede er unit testet.

Det er fair nok at i er glade for funktions sprog, men det gør ikke nødvendigvis OO uegnet til f.eks. at unit teste.

  • 0
  • 0
Torben Mogensen Blogger

Rune skriver:

"Det er f.eks. helt åbentlyst at bruge OO til at kunne dele en opgave ud på flere personer der så hver især kan udvikle dele af applikationen, unit teste den og til sidst sætte den sammen uden at skulle ændre noget i de dele der allerede er unit testet."

Bevares, det kan man godt slippe af sted med i OO, men netop det du nævner, er en af de ting, hvor funktionsprogrammering er en klar vinder: Programmer er generelt mere kompositionelle, fordi de har renere grænseflader (der klart fremgår af typerne).

  • 0
  • 0
Ville Witt

Min opfattelse fra ovenstaaende diskusion er desvaerre at flere folk betragter unittest som en metode til at naa korrekte prgrammer. Men det er logiskt at tests ikke viser om hvorvidt kode er korrekt eller ej, med mindre at alle kombinationer er afproevet. En af de foerste artikler http://www.google.dk/search?q=structure+test+software er http://www.ece.cmu.edu/~koopman/des_s99/sw_testing/
Har hurtigt skimtet denne og mener at der her er tale om et fornuftigt syn paa situationen. Hvis kontrakt orienteret udvikling/design er saa meget i folks bevidsthed, burde flere vaerdsaette Ada's struktur: *.ads og *.adb (Ada specification og Ada body) og her er der tale om aegte kontrakter - ikke C++'s underlige bastard headere. Ada er frit og helt fra begyndelsen var software engineering en del af sproget. Korrekt kode.

Unit test virker fint i OO. Men opfatter man generelt det at teste kode via white- eller blackbox metoder, som en metode til at bevise korrekthed af programmel paa, er man ude i hampen.

Haaber ikke nogle folk har ladet sig overbevise om at der er endnu er fundet en "silver bullet" til software krisen.
"Software crisis" paa Google for mere info om emnet.

  • 0
  • 0
Torben Mogensen Blogger

Man kan ganske rigtigt ikke bevise et (ikke-trivielt) program fejlfrit med test (det er Dijkstra's lov om test).

Men hvis tests ikke kommer bare nogenlunde rundt i rummet af mulige brugsmønstre, så er de intet værd. Så kan du være nok så glad over at have lavet unit tests på alle metoder, men du kan ikke bruge resultatet til noget. Kort sagt har du spildt din tid.

Lad os sige, at du har en klasse, der implementerer sekvenser som enkelthægtede lister, noget i stil med

class List implements Sequence {
private Object head;
private List next;
public Object elem()
{return this.head;}
public List advance()
{if (this.next==NULL) error();
else return(this.next);}
public List last()
{if (this.next==NULL) return this;
else return this.next.last();}
}

Nu vil du gerne udvide med en konkateneringsoperator, så du tilføjer en metode:

public List concat(List y)
{this.last().next = y;}

Dine unit tests prøver concat() på hundrede par af tilfældigt konstruerede lister, og den finder ingen fejl. Men en dag er der en, der laver kaldene

x.concat(x);
...
y = x.last();

Testen afslørede ikke problemet, fordi den aldrig prøvede at konkatenere en liste med sig selv. Problemet er, at det er ret svært at forudsige de mulige brugsmønstre bare nogenlunde præcist, hvis de kan afhænge af tilstand og aliasing.

  • 0
  • 0
Michael Rasmussen

Problemet er, at det er ret svært at forudsige de mulige brugsmønstre bare nogenlunde præcist

Er det nu også formålet med test? I min optik består test af to trin:
1) Identificer den lovlige delmængde af inputværdier fra populationen.
2) Skriv test for afprøvning af denne delmængde.

Overført på dit eksempel mangler der en definition af den lovlige mængde af input. I f.eks. C++ ville en assignment metode i en klasse altid teste for "self assignment":

Foo& Foo::operator= (const Foo& f) {
if (this == &f) return *this; // Håndter self assignment

// Normal håndtering af assignment

return *this;
}

Det der går galt i dit eksempel, er den manglende håndtering af self assignment, hvilket burde have været en del af designet.

  • 0
  • 0
Torben Mogensen Blogger

"1) Identificer den lovlige delmængde af inputværdier fra populationen.
2) Skriv test for afprøvning af denne delmængde."

Det virker ku, hvis du er sikker på, at dine metoder aldrig bliver kaldt med værdier udenfro denne mængde, så hvis ikke typen i sig selv sikrer dette, så skal din kode test for inputtets validitet, og din unit test skal teste, at denne test sker korrekt.

"Det der går galt i dit eksempel, er den manglende håndtering af self assignment, hvilket burde have været en del af designet."

Og hvis det burde have været en del af designet, burde et også have været en del af testen.

Problemet i Java og de fleste andre OO sprog er, at typerne sjældent er stærke nok til at sikre mod invalide argumenter. Specielt kan typerne ikke udtrykke aliasing, og netop aliasing er en hyppig kilde til invalide argumenter. Mange programmører tænker ikek engang over, at aliasing kan være et problem for deres kode.

Gode designere vil teste for det på køretid, men dels er det dyrt og dels bliver den slags test ofte sprunget over -- generelt laves der alt for lidt argumentvalidering, se bare på SQL-injection problematikken.

  • 0
  • 0
Michael Rasmussen

Det virker ku, hvis du er sikker på, at dine metoder aldrig bliver kaldt med værdier udenfro denne mængde, så hvis ikke typen i sig selv sikrer dette, så skal din kode test for inputtets validitet, og din unit test skal teste, at denne test sker korrekt

Det var derfor jeg tidligere anlagde betragtningen, at unit test er en avanceret form for grænseværdi test. Punkt 1 i min to-trins model indeholder den implicitte antagelse, at der foretages inputvalidering; Består input ikke denne test, skal der returneres en exception (om denne så bør være checked eller ej, er en helt anden diskussion).

Er din definition af aliasing en interface type? I så fald tilbyder Java instanceof til at teste objekter på kørselstidpunkt. For en dybere validering tilbyder Java inspektion (reflection).

Da min opdragelse indenfor programmering tager afsæt i Pascal og C, er jeg opflasket med inputvalidering - Pascals var indbygget i sproget, og i C blev man vendet til at lave kontrol af returværdien fra funktioner ( if ((res = foo()) == 0) {....}), hvorfor jeg finder det naturligt, at lave inputvalidering af alle metoder i public scope.

Det er egentligt forbavsende så få Java programmører, der anvender sproget indbyggede hjælp til inputvalidering; jeg tænker her på assert. Jeg vil tro, at over halvdelen af alle køretidsfejl i Java skyldes NullPointerException, hvilket enkelt kunne have været løst med en kombination af inputvalidering og assert.

Et andet typisk eksempel er følgende - java er brugt som eksempel:

public class Foo {
Foo instance;

private Foo() {}

public static Foo getInstance() {  
    if (instance == null)  
        instance = new Foo();  
    return instance;  
}  

}

Kan du gætte fejlen i ovenstående, som jeg er 99% sikker på, de fleste ikke tester for med unit test?

  • 0
  • 0
Torben Mogensen Blogger

Jeg er ikke ekspert i alle detaljer omkring Java, så jeg ved ikke, om det betyder noget, at konstruktøren er erklæret private. Hvis ikke, så ser jeg ingen grund til at erklære den eksplicit, for defaultkonstruktøren gør det samme (initialiserer felter til nul). Hvis "private" betyder, at man ikke kan konstruere instanser af objektet, er det vel fejlen.

Men hvad angår nullPointerExeptions, så foretrækker jeg Spec#s ide, hvor default pointertypen ikke kan være null, og man eksplicit skal erklære pointere til at kunne være det. Dermed kan det statisk valideres, at et argument ikke er null.

Apropos inputvalidering, hvad ville du bruge til concat-metoden i mit eksempel?

  • 0
  • 0
Rune Juhl-Petersen

Fejlen er at dit singleton pattern kun returnerer instansen første gang den bliver kald.

Jeg vil konkludere til mig selv at unit test kun kan bruges til at teste de situationer man kender der vil give problemer. Det er altså ikke hvilken teknologi der er afgørende for om en unit test er effektiv, men udviklerens overblik. Funktionelle sprog har altså i min bog ingen fordel frem for OO til unit test.

Til Torben: Hvad er dine erfaringer med OO siden du er så skræmt af det?

  • 0
  • 0
Torben Mogensen Blogger

Rene skriver

Til Torben: Hvad er dine erfaringer med OO siden du er så skræmt af det?

Jeg er ikke skræmt -- jeg har nok programmeret i mange flere forskellige sprog meg mangle flere forskellige paradigmer end dig, så der skal meget til at skræmme mig. Men jeg har stirret OO-programmering i øjnene, og jeg brød mig ikke om, hvad jeg så. Jeg vil tro, at det er det perspektiv, jeg har fra at have prøvet så mange forskellige sprog, der lader mig se det, hvor folk, der kun har programmeret imperative og OO sprog, ikke kan se noget galt.

Jeg kan jo så til gengæld spørge: Hvad er dine erfaringer med funktionsprogrammering, siden du er så skræmt af det?

  • 0
  • 0
Mogens Heller Grabe

En anden pointe mht. unit test og OO, er, at unit test er en måde (hvis ikke det er mådEN) at fastholde de krav man har til sit system.

Noget af det gode ved OO er jo netop indkapsling af implementationsdetaljer, som er omverdenen uvedkommende - men hvordan sikrer man at koden stadigvæk er "korrekt" (altså ikke i algoritmisk forstand, mere i en bred "det blev vi enige med kunden om"-agtig forstand) efter en stor refaktorering?

Det gør man ved at have unit tests! Og der er - så vidt jeg kan se - ingen anden (praktisk lade-sig-gørlig) måde at gøre dette på.

Hvis man vil bevise at koden er matematisk korrekt, skulle man tage at kigge på nogle af de værktøjer der findes til kontraktbaseret programmering i stedet.

  • 0
  • 0
Michael Rasmussen

[qoute]Fejlen er at den ikke er trådsikker.[/quote]Ja, det var den åbenlyse fejl. Fejlen kan udbedres på følgende måde:

[code=java]
public class Foo {
Foo instance;
private Foo() {}

public static synchronized Foo getInstance() {  
    if (instance == null)  
        instance = new Foo();  
    return instance;  
}  

}
[/code]
Men der er en anden, og endnu værre, fejl, der relaterer sig til Java's objektmodel. Fejlen kunne Torben have brugt som argumentation for at unit test i OO er væsentligt vanskeligere end i funktionsprogrammering.

Årsagen til fejlen skal findes i Java's indbyggede regel om, at alle objekter implicit arver fra Object. En af metoder i object har følgende forskrift:

[code=java]
protected Object clone();
[/code]
Umiddelbart har det derfor ingen konsekvenser for vores klasse, men skulle fremtiden betyde nedenstående, kan vi risikere at stå med en logisk bombe:
[code=java]
class Bar implements Clonable {

public Object clone {  
    Object o = null;  
    try {  
        o = super.clone();  
    }  
    catch (CloneNotSupportedException e) {  
        System.err.println("MyObject can't clone");  
    }  
    return o;  
}  

}

class Foo extends Bar {
Foo instance;

private Foo() {}

public static synchronized Foo getInstance() {  
    if (instance == null)  
        instance = new Foo();  
    return instance;  
}  

}

Foo foo = Foo.getInstance();
Object o = foo.clone() /* Da super implementerer clone er det lovligt. Det var vist ikke meningen med en singleton! */
[/code]
For at udelukke denne situation, bør vores singleton derfor have følgende udvidelse:
[code=java]
class Foo Bar {
Foo instance;

private Foo() {}

public static synchronized Foo getInstance() {  
    if (instance == null)  
        instance = new Foo();  
    return instance;  
}

public Object clone()  
        throws CloneNotSupportedException {  
    throw new CloneNotSupportedException();   
    /* Ny kan man ikke lave en kopi af singleton  
     * grundet nedarvning fra super.  
 */  
}  

}
[/code]

  • 0
  • 0
Torben Mogensen Blogger

[quote]Apropos inputvalidering, hvad ville du bruge til concat-metoden i mit eksempel?

Et forslag kunne være at implementere interfacet Comparable.[/quote]

Jeg kan ikke lige se, hvad det skulle hjælpe. Kan du uddybe?

Og forklar så, hvordan det vil fange problemet i følgende kode:

[code=java]
y = x.advance();
x.concat(y);
z = x.last();
[/code]

  • 0
  • 0
Michael Rasmussen

[code=java]
class List<E> implements Sequence<E>, Comparable<List<E>> {

private E head;  
private List&lt;E&gt; next;

public E elem() {  
    return head;  
}

public List&lt;E&gt; advance() {  
    if (next == null)  
        throw new IndexOutOfBoundsException();  
    return next;  
}

public List&lt;E&gt; last() {  
    if (next == null)  
        return this;  
    else  
        return next.last();  
}

public void concat(Sequence&lt;E&gt; y) {  
    if (this.compareTo((List&lt;E&gt;)y) != 0)  
        last().next = (List&lt;E&gt;) y;  
    else  
        throw new RuntimeException(&quot;cannot concatenate to self&quot;);  
}

public int compareTo(List&lt;E&gt; o) {  
    List&lt;E&gt; y = this;  
    List&lt;E&gt; x;  
    try {  
        while (y.next != null) {  
            x = o;  
            while (x.next != null) {  
                if (y.equals(o))  
                    return 0;  
                x = x.advance();  
            }  
            y = y.advance();  
        }  
    }  
    catch (IndexOutOfBoundsException e) {}  
    return 1;  
}

}
[/code]
Bemærk dog det er en dyr kontrol, da kompleksiteten af metoden er O(n^2). Så der bør nok findes en anden løsning for store lister.

  • 0
  • 0
Michael Rasmussen

Glemte:
Forudsætningen er, at klassen i sin nuværende udformning vil loope uendeligt i last metoden, hvis concat tillader input fra lister, hvor fællesmængden af elementer mellem de to lister ikke er tom. compareTo metoden laver denne kontrol, og såfremt den finder blot et identisk element mellem de to lister, afviser concat med en RuntimeException. Ens i denne sammenhæng vil sige, at elementernes reference er ens, og altså ikke indholdet af elementerne. Hvis et element fra to forskellige lister har samme reference, vil de også have fælles reference til næste element.

  • 0
  • 0
Torben Mogensen Blogger

Din ide er god nok, men som du siger er den O(n^2). En bedre metode, der bruger samme grundlæggende ide er at sammenligne this.last() med y.last().

Det giver dog stadig et væsentligt overhead, for uden testen er tidsforbruget af x.concat(y) O(|x|), mens det med testen er O(|x|+|y|), hvor |x| er længden af x.

  • 0
  • 0
Jens Madsen

"Hvis unit tests bliver skrevet med kendskab til implementeringen, så er det ikke længere black box.

Ideelt set skal unit tests skrives af en anden end implementatoren, kun med kontrakten som udgangspunkt -- ellers vil testen ofte have de samme underforståede (og potentielt forkerte) antagelser som implementeringen."

Som jeg ser det, er det dårligt, hvis unit tests ikke laves af den der implementerer. Årsagen ligger i ansvar: Den der implementerer, tager også ansvaret for, at sine ting fungerer, og derfor skal vedkommende også udføre de nødvendige tests. I princippet bør vedkommende udfylde et underskriftsblad på, at blokken er designet til at fungere, at den er testet, og at udvikleren er fuldt overbevist om den fungerer. Test vektorer vedlægges som dokumentation. Som udvikler, tages ansvar for komponenterne. Kun udvikleren kender indholdet (interne tilstande mv.), og specielt ved OOP er nødvendigt, at udvikleren står inde for resultatet.

Test 2 er, at du aldrig skal stole på udvikleren... Når du anvender et objekt, som en anden har udviklet, skal du have den nødvendige skepsis, og tage det med et vist gran salt. Det betyder, at du starter med at teste komponenten. Derved får du kendskab til komponentens funktion, og misforstår den ikke nemt. I nogle tilfælde, opbygges softwaren måske endog udfra de test du gør.

Enhver komponent testes derfor mindst to gange: Først af udvikleren, der står inde for resultatet. Herefter af brugeren (evt. anden udvikler), der står inde for overbevisning om at komponenterne som bruges er testet og fungerer godt, samt i sidste ende også står inde for sit eget resultat. Er du ikke overbevist om, at de komponenter du bruger fungerer, har du naturligvis problem med at udfylde underskriftsark vedrørende din komponent, da denne indeholder de andres værk (og dermed står du også inde for det).

Komponenter, der anvendes ofte, testes ofte. I princippet af alle der bruger komponenterne. Eventuelt kan vedlægges underskrifter fra alle der har testet komponenterne, og fundet de fungerer. Derved kan undlades at teste den så grundigt - til gengæld er det et problem for dem der har testet, hvis en test viser sig, at fejle.

Måske antages komponenter der bruges ofte, at være grundig testet, og udviklerne der anvender dem, bruger måske mindre tid på test - alligevel skal udvikeren være sikker på komponenten og stå inde for den, samt udføre de nødvendige tests, for at opnå det.

Mange typer af tests kræver kendskab til programmets interne opbygning og variable. Du skal ofte - som minimum sikre dig at alle programlinier er udførte ved testen. Ellers er testen ikke nok. Intet program kan være testet, hvis branch retninger ikke er udført, eller programlinier ikke er udført.

Du skal sikre dig, at ram forbruget er konstant, eller går op og ned, som beskrevet i dokumentationen. Visse sprog, kan have problem med at angive forbrug korrekt. Et sådant sprog, har problemer.

Disse ting, kræver til vis grad kendskab til indmaden, eller som minimum at kompileren kan forklare dette - f.eks. forklare om alle linier er udførte, samt alle branch retninger (alle branch retninger er samme som alle programlinier, hvor alle retninger anvendes, eller compileren internt har sat en "dummy" indstruktion ind, ved ikke brugte retninger, som også skal sættes flag på).

White-box tests, er som jeg ser det noget andet. Her tester du typisk input parametre mv. og at arrays ikke overskrider sine grænser. I de tilfælde, hvor det er muligt, at altid bedst at opnå sådanne tests, ved at have range tjek mv. indbygget i compileren. Men i tilfælde, hvor det har betydning for brugeren, f.eks. validering af indtastningsbokse, skal det naturligvis lægges i software. Programmeringssprog, skal helst håndtere range tjek osv. samt at variable kun må have værdier indenfor bestemte grænser. Det skyldes, at kompileren derved har kendskab til intervaller for variable og indexes, og det betyder meget for software analyse og automatisk parallelisering af software mv. Dertil kommer, at den type tjek ofte kan flyttes til hardware, og det er mere besværligt, hvis det står som kode. Range tjek, og lignende, er derfor tjek der skal gøres af compiler, og principielt er forkert at indbygge i objekter, da compileren så ikke får de nødvendige kendskab til variablers værdiområde og mulighed for at bestemme anvendelse af memory, eller at afsætte det rette antal bits i lageret (typisk vil være afsat for meget, fordi der "bare" siges integer, eller større). Compileren har ansvar for kompilering, og hvor mange bits den skal anvende, for at opnå et hurtigt program. Hvis systemet skal være fleksibelt, vil kompileren oversætte til ens kode, uanset CPU og dens arkitektur, og operativsystemet indeholder en CPU driver, der omsætter effektivt koden til CPU.

Formålet med unit test, er at programmøren kan dokumentere sine ting fungerer i form af dokumenterede test vektorer. Brugeren, laver også unit test af komponenterne. Alt er testet to gange som minimum. I slut enden, har vi hele applikationen, der kun er testet af programmøren. Her er nødvendigt med et test team, der tester programmet, set udfra "brugerens synspunkt", og udfører "brugertesten". Derved skulle alt være testet mindst to gange, inden det når til kunden - og man lader det ikke være op til "den perfekte programmør" om tingene fungere.

Kunden udfører så evt. ekstra tests, for at blive overbevist om, at produktet fungerer. Ellers, vil de ikke købe produktet. Dem der køber produktet hos kunden, skal sædvanligvis også være overbevist om, at tingene fungerer, og vedlægge underskriftsblad, troserklærringer osv. på produktet er ok, og at de har gjort det nødvendige for at sikre sig produktet er ok, herunder oftest udført egne tests.

Hele ovenstående mølle, er omtrent det at alt andet (end software) udsættes for. Såvel søm, som skruer... Dertil, findes masser af dokumentation, i form af tekniske og videnskabelige arbejder, der dokumenterer materialerne og deres levedygtighed, styrker, korrosion osv. Det er ikke simpelt at lave et søm.

  • 0
  • 0
Log ind eller Opret konto for at kommentere