Forside

Automatisk test af PL/1 moduler

Når man går i gang med automatisk test første gang, er det ofte noget man laver oven på et system, der ikke er konstrueret med henblik på automatisk test. Det har nogle svagheder, der hurtigt gør at man begynder at tvivle på om automatisk test er vejen frem.

Man opdager hurtigt at det er svært at lave en stabil testcase, man kan køre igen og igen. Måske ændrer en kollega i et modul man kalder. Måske er der nogen der retter i databasen. Resultatet er, at ens testcase ikke længere giver det resultat man forventer og man skal rette testcasen til.

Det ville være rart hvis man kunne isolere sin kode og kontrollere det miljø, ens test kører i. Formålet er dels at isolere den kode man tester til en overskuelig klump og dels at lave et stabilt grundlag at teste koden på, så man ikke skal vedligeholde sin test, hver gang der er nogen der ændrer et komma i systemet.

En metode til at isolere koden er dependency injection. Man definerer hvilke grænseflader de moduler man arbejder sammen med har, men det er først ved run-time at man fortæller hvilke konkrete implementationer af disse grænseflader, modulet skal arbejde sammen med.

Jeg vil i det følgende illustrere ideen med et eksempel i PL/1. I eksemplet indgår to moduler: AddVAT og GetVATRate. AddVAT kaldes med et beløb og returnerer beløbet inkl. moms. GetVATRate henter momssatsen.

Vi starter med at definere grænsefladen til hhv. AddVAT og GetVATRate rutinerne:

 

  2 Amount fixed dec(15, 2),
  2 AmountWithVAT fixed dec(15, 2)

og

  2 VATRate fixed dec(5, 3)

 

Så laver vi selve AddVAT rutinen:

 AddVAT: proc(resolver, interface);                    
                                                       
 dcl resolver entry(char(100)) returns(entry) variable;
 dcl 1 interface,                                      
 %include IAddVAT;                                     
 ;                                                     
                                                       
 dcl GetVATRate entry(entry, *) variable;              
 dcl 1 IGetVATRate,                                    
 %include IGetVAT;                                     
 ;                                                     
                                                       
   GetVATRate = resolver('GetVATRate');                
   call GetVATRate(resolver, IGetVATRate);             
                                                       
   interface.AmountWithVAT = interface.Amount *        
                            (IGetVATRate.VATRate + 1); 
                                                       
 end AddVAT;                                           

Her er det værd at bemærke resolver parameteren til rutinen. Det er denne rutine, der gør det muligt for os at få den konkrete implementation af GetVATRate. Vi kalder resolver rutinen med navnet på den rutine vi skal bruge ('GetVATRate') og den returnerer en entry, som vi så kan kalde.

Så er vi sådan set klar til at teste. Vi har ikke nogen implementation af GetVATRate endnu, men det er sådan set også lige meget. Når vi tester, laver vi en stub af GetVATRate. På den måde er vi ikke afhængige af den 'rigtige' udgave af GetVATRate og vi kan også konstruere programmerne i den rækkefølge, der giver mest mening.

Vores test-program ser sådan her ud:

 testpgm: proc options(main) reorder;                      
                                                           
   dcl addroutine entry(char(100), entry);                 
   dcl AddVAT entry(entry, *);                             
   dcl resolve entry(char(100)) returns(entry limited);    
                                                           
   dcl 1 IAddVAT,                                          
   %include IAddVAT;                                       
   ;                                                       
                                                           
   call addroutine('GetVATRate', GetVATRateStub);          
                                                           
   IAddVAT.Amount = 100;                                   
   call AddVat(resolve, IAddVat);                          
   if (IAddVAT.AmountWithVAT = 125) then                   
     put skip list('Test passed');                         
   else                                                    
     put skip list('Test failed. Expected 125, but got ' !!
                   IAddVAT.AmountWithVAT);                 
                                                           
   GetVATRateStub: proc(resolver, i);                      
     dcl resolver entry(char(100)) returns(entry);         
     dcl 1 i,                                              
     %include IGetVAT;                                     
     ;                                                     
                                                           
     i.VATRate = 0.25;                                     
   end GetVATRateStub;                                     
 end testpgm;                                              

I dette program optræder addroutine. Denne rutine kæder et logisk navn ('GetVATRate') og en konkret implementation (GetVATRateStub) sammen. Så når vi nede i AddVAT rutinen beder om den konkrete implementation af GetVATRate, så får vi adressen på GetVATRateStub.

Vores GetVATRateStub er hardkodet til at levere en momssats på 25% retur. Og så kan vi kode resten af testen. Vi kalder med 100 i Amount og checker at vi får 125 tilbage i AmountWithVAT.Så er det vist på tide at løfte sløret for hvordan addroutine og resolver fungerer:

 windsor: package exports(*);                  
 dcl root ptr init(null());                    
                                               
 dcl 1 routine_structure based,                
       2 next ptr,                             
       2 name char(100),                       
       2 routine entry limited;                
                                               
 addroutine: proc(name, routine);              
   dcl name char(100);                         
   dcl routine entry limited;                  
                                               
   dcl r ptr;                                  
                                               
   allocate routine_structure set(r);          
   r->routine_structure.name = name;           
   r->routine_structure.routine = routine;     
                                               
   r->routine_structure.next = root;           
   root = r;                                   
 end addroutine;                               
                                               
 resolve: proc(name) returns(entry limited);   
   dcl name char(100);                         
   dcl p ptr;                                  
                                               
   p = root;                                   
   do while(p ^= null());                      
     if (p->routine_structure.name = name) then
       return(p->routine_structure.routine);   
     p = p->routine_structure.next;            
   end;                                        
                                               
   put skip list('Couldn''t resolve ' !! name);
   signal error;                               
 end resolve;                                  
 end windsor;                                  

addroutine lægger det logiske navn og det tilhørende entry ind i en hægtet liste. Og resolve søger den hægtede liste igennem efter et logisk navn og returnerer det tilhørende entry.

Pakken hedder 'Windsor', fordi der findes en pakke til dependency injection i C#, der hedder Castle Windsor, der gør nogenlunde det samme, bare smartere og bedre.

Nu kan vi compilere det hele, linke det sammen og prøve at køre det. Og det virker! Test programmet skriver 'Test passed' som det skal.

Nu kan vi gå videre og lave den rigtige implementation af GetVATRate:

 GetVAT: proc(resolver, interface);              
   dcl resolver entry(char(100)) returns(entry); 
   dcl 1 interface,                              
   %include IGetVAT;                             
   ;                                             
                                                 
   /* Read VAT from database */                  
   interface.VATRate = 0.30;                     
 end GetVAT;                                     

Her har jeg snydt og hardkodet momssatsen. Men for at vise styrken i denne teknik, har jeg sat momssatsen til 30%. Og så laver vi det rigtige program, der kalder AddVAT:

 prodpgm: proc options(main) reorder;                  
   dcl addroutine entry(char(100), entry);             
   dcl AddVAT entry(entry, *);                         
   dcl resolve entry(char(100)) returns(entry limited);
   dcl GetVAT entry(entry, *);                         
                                                       
   dcl 1 IAddVAT,                                      
   %include IAddVAT;                                   
   ;                                                   
                                                       
   call addroutine('GetVATRate', GetVAT);              
                                                       
   IAddVAT.Amount = 100;                               
   call AddVAT(resolve, IAddVAT);                      
   put skip edit(IAddVAT.Amount, ' with VAT is ',      
                 IAddVAT.AmountWithVAT)                
                (p'ZZZ.ZZ9,V99', a, p'ZZZ.ZZ9,V99');   
 end prodpgm;                                          

Det ligner meget testprogrammet, men i produktionsprogrammet kæder vi det logiske navn ('GetVATRate') sammen med den rigtige implementering af GetVATRate, nemlig GetVAT rutinen.

Det kan vi compilere og køre og det skriver "100,00 with VAT is 130,00". Og det er jo korrekt, for vores rigtige implementation af GetVATRate returnerer en momssats på 30%.

Men hvad så med vores testcase? Skal den ikke rettes til den nye momssats? Nej, det skal den ikke. Med dependency injection har vi opnået at vores test af AddVAT er isoleret fra GetVATRate og vores test kører stadig som den skal. I testprogrammet kalder vi nemlig ikke den rigtige implementation af GetVATRate, men i stedet en stub. Og stubben returnerer stadig en momssats på 25%, der passer til vores test-case.

Om kilianconsult

Kilianconsult udvikler IT-systemer af høj kvalitet på .NET platformen, mainframe og mobil-platforme. En meget bred erfaring, skabt igennem mere end 30 år i IT-branchen, sikrer solide løsninger.

Curriculum vitae

quote1.png