Dynamiske mock services i SoapUI

Værktøjet SoapUI er et godt og gratis open-source-værktøj til test af Soap- eller REST-baserede Web Services. Rigtigt mange udviklere bruger SoapUI til test af Web Services, dvs. at de importerer en WSDL og tester Servicen ved at lade SoapUI afsende fx Soap-requests. Disse tests kan enten køres manuelt eller automatiseres med SoapUIs indbyggede understøttelse af TestCases. SoapUI kan imidlertid bruges til mere end blot kald af Services, og i det følgende kigger vi på hvordan SoapUI også kan bruges til at udstille Web Services – såkaldte Mock-Services.

Mock-Services er relevante, hvis man i større contract-first-projekter har ansvar for at udvikle klientapplikationer. Typisk vil der gå noget tid fra kontrakterne er stabile, til det er muligt at kalde test- eller Mock-Services, hvad der kan være en forudsætning for at komme i gang med udviklingen af en klientapplikation. Her er det så, at SoapUI kommer til undsætning, og gør det muligt for klientudviklerne selv at konstruere Mock-Services hurtigt.

Jeg har i de seneste måneder introduceret denne teknik for en større gruppe udviklere hos én af Lund & Bendsens kunder, som led i et SOA-certificeringsforløb, og det er blevet en meget populær teknik. Desuden har vi i JavaGruppen holdt to meget velbesøgte møde i København og Aarhus om emnet, så det lader til at have en bred interesse. I det følgende beskriver jeg den af teknikkerne til konstruktion af Mock-Services, som umiddelbart har vist sig mest generelt anvendelig. Teknikken går ud på at konstruere en dynamisk Mock-Service som henter data fra en database ud fra værdier i requestet. Med denne teknik er det muligt at lave vilkårligt sofistikerede Services.

En dynamisk Mock-Service med Groovy-scripting

SoapUI benytter sproget Groovy til understøttelse af dynamiske Mock-Services. Jeg gennemgår i det følgende en løsning med denne arkitektur:

Som det ses, benytter vi Javas indbyggede JDBC-ODBC-driver (ODBC-bridge) til at tilgå en ODBC-forbindelse på Windows-OS’et. Vi udnytter her, at der med Microsoft Office følger ODBC-drivere til Excel – hvilket vil sige, at vi kan lave en databaseforbindelse (ODBC) til et Excel-ark og lave SQL-forespørgsler mod det. Set fra Groovy-scriptets synspunkt er der blot tale om en almindelig JDBC-forbindelse, så man vil sagtens kunne erstatte Excel med en rigtig database.

Formålet er at simulere en service til opslag af tekster. Her er vist et eksempel på et request og det tilhørende response:

Request:

<soapenv:Envelope xmlns_soapenv=
"http://schemas.xmlsoap.org/soap/envelope/" 
xmlns_tex="http://example.lundogbendsen.dk/textrepository">
   <soapenv:Body>
      <tex:GetTextRepository>
         <tex:Entity>Customer</tex:Entity>
         <tex:Attribute>Type</tex:Attribute>
      </tex:GetTextRepository>
   </soapenv:Body>
</soapenv:Envelope>

Customer og Type er parameterværdier i operationskaldet.

Response:

<soapenv:Envelope xmlns_soapenv=
"http://schemas.xmlsoap.org/soap/envelope/">
   <soapenv:Body>
      <tex:GetTextRepositoryResponse xmlns_tex=
      "http://example.lundogbendsen.dk/textrepository">
         <tex:TextRepository>
            <tex:Id>3</tex:Id>
            <tex:Text>
               <tex:TextId>TR1002#1</tex:TextId>
               <tex:Value>Regular</tex:Value>
               <tex:IsActive>true</tex:IsActive>
            </tex:Text>
            <tex:Text>
               <tex:TextId>TR1002#2</tex:TextId>
               <tex:Value>Silver</tex:Value>
               <tex:IsActive>true</tex:IsActive>
            </tex:Text>
            <tex:Text>
               <tex:TextId>TR1002#3</tex:TextId>
               <tex:Value>Gold</tex:Value>
               <tex:IsActive>true</tex:IsActive>
            </tex:Text>
...
         </tex:TextRepository>
      </tex:GetTextRepositoryResponse>
   </soapenv:Body>
</soapenv:Envelope>

Et TextRepository er en struktur som består af et Id og et antal Text-objekter.

Hvis der kaldes med ukendte parameterværdier kastes en Soap Fault:

<soapenv:Envelope xmlns_soapenv=
"http://schemas.xmlsoap.org/soap/envelope/">
   <soapenv:Body>
      <soapenv:Fault>
         <faultcode>soapenv:Client</faultcode>
         <faultstring>TextRepository Customer/Type2 not found!
         </faultstring>
         <detail>
            <tex:NoSuchTextRepository xmlns_tex=
            "http://example.lundogbendsen.dk/textrepository">
               <tex:RepositoryId>Customer/Type2</tex:RepositoryId>
               <tex:Message>TextRepository not found!</tex:Message>
            </tex:NoSuchTextRepository>
         </detail>
      </soapenv:Fault>
   </soapenv:Body>
</soapenv:Envelope>

Løsningen består af disse elementer:

  1. Et Excel regneark med data
  2. En ODBC-forbindelse til regnearksfilen
  3. Et SoapUI-projekt med et Groovy Script
  4. Et Response-dokument i SoapUI

Den overordnede dynamik af løsningen er vist på denne figur:

Løsningen baserer sig Groovys Mock-Service-facilitet, som afvikler et Groovy-script når et request modtages (1). Scriptet læser data fra Excel og opbygger en komplet Soap-envelope (2,3). Soap-beskeden bindes i en context-variabel (4). Scriptet returnerer navnet på det response-dokument, der skal sendes til klienten (6) og response-dokumentet ’renderer’/udskriver Soap-beskeden. Løsningen kan i høj grad sammenlignes med den klassiske MVC-arkitektur fra JavaWeb-applikationer med SoapUI i rollen som WebContainer, Groovy-scriptet som Servlet/Controller og Response-dokumentet som JSP/View.

Excel-arket

Excel-arket skal tilgås via JDBC og navnet på Arket (Sheet) kommer derfor til at optræde som tabel-navn og kolonneoverskrifterne (1. række) som kolonner. Vores ark ender med at se således ud:

ODBC-forbindelsen

Du skal have ODBC-drivere til Excel installeret. Disse kommer typisk sammen med MS-Office. Vi tilføjer en ny forbindelse baseret på en Excel-driver og vælger den Excel-fil, der er vist ovenfor.

Groovy Scriptet

Groovy-scriptet skrives direkte i en editor i SoapUI, som kommer frem, når man har valgt SCRIPT som dispatch-method:

Her er ses den Groovy-kode der er nødvendig for at parse requestet og udtrække parameterværdier, slå op i databasen og opbygge en Soap-Envelope:

   import groovy.sql.Sql
   import groovy.xml.MarkupBuilder
 
   //utility klasse for diverse SoapUI/Groovy-stuff
   def groovyUtils = new com.eviware.soapui.support.GroovyUtils( context )
 
   def holder  //XmlHolder, som kan bruges til at parse requestet
   //sæt stub-data 
   (bruges, hvis der ikke er noget request, fordi scriptet køres via 
   Run-knappen i SoapUI)
   def entity = "Customer"
   def attribute = "Country"
 
   //hvis scriptet er invokeret af et SoapRequest
   if (mockRequest.requestContent != null) {
         //XmlHolder giver os mulighed for at lave XPath-forespørgsler 
         på SoapRequestet
         holder = groovyUtils.getXmlHolder( mockRequest.requestContent )
         //registrer namespace
         holder.namespaces["tex"] = 
         "http://example.lundogbendsen.dk/textrepository"
         //læs parametre fra requestet
         entity = holder["//tex:Entity"]
         attribute = holder["//tex:Attribute"]
   }
 
   log.info "GetTextRepository(Entity=${entity}, Attribute=${attribute})"
 
   //lav en JDBC-forbindelse til Excel. TextRepository er navnet på 
   ODBC-forbindelsen
   def sql = Sql.newInstance("jdbc:odbc:TextRepository", 
   "sun.jdbc.odbc.JdbcOdbcDriver")
 
   def found=false; //found er true, hvis der findes et TextRepository som 
   matcher Entity og Attribute
   int textRepositoryId
   //Undersøg om der er matchende repository og hvis ja, så hent dets id
   sql.eachRow("SELECT id FROM [TextRepository$] where Entity=${entity} 
   and Attribute=${attribute}", { row ->
         found =  true
         textRepositoryId = row[0]
   })
 
   writer = new StringWriter()
   def xml = new groovy.xml.MarkupBuilder(writer)
 
   if (found) {  //opbyg Soap-Envelope med det fundne TextRepository
         xml.'soapenv:Envelope'('xmlns:soapenv': 
         'http://schemas.xmlsoap.org/soap/envelope/') {
                'soapenv:Body'{
                      'tex:GetTextRepositoryResponse'('xmlns:tex':
                      'http://example.lundogbendsen.dk/textrepository') {
                            'tex:TextRepository'{
                                   'tex:Id'(textRepositoryId)
                                   sql.eachRow("SELECT * FROM 
                                   [TextRepository$] 
                                   where Entity=${entity} and 
                                   Attribute=${attribute}", { row ->
                                  'tex:Text' {
                                         'tex:TextId'(row.textid)
                                         'tex:Value'(row.value)
                                         'tex:IsActive'(row.isactive)
                                         }
                                   })
                            }
                      }
                }
         }    
   } else { //hvis der ikke er noget match, så opbyg en SoapFault
       xml.'soapenv:Envelope'('xmlns:soapenv': *
       'http://schemas.xmlsoap.org/soap/envelope/') {
           'soapenv:Body'{
                 'soapenv:Fault'{
                     faultcode("soapenv:Client")
                     faultstring("TextRepository ${entity}/
                     ${attribute} not found!")
                     detail {
                       'tex:NoSuchTextRepository'('xmlns:tex':
                       'http://example.lundogbendsen.dk/textrepository'){                                                
                          'tex:RepositoryId'("${entity}/${attribute}")
                          'tex:Message'("TextRepository not found!")
                                 }
                           }                     
                    }
              }          
         }
    }
 
   //opret en attribut i RequestContext, som kan læses fra Response-filen
   requestContext?.response = writer.toString()
   return "DefaultResponse"

Kernen i programmet er Groovys MarkupBuilder. Groovy har en række builders, som alle er implementationer af Builder-designmønstret fra Gamma et al. MarkupBuilder er beregnet til at opbygge en struktur i et markupsprog, fx XHTML eller – som her – XML. MarkupBuilder anvender en af Groovys dynamiske features, nemlig det der hedder ’pretended method calls’, hvor et objekt tillader at man kalder vilkårlige metoder på det uden at metoderne er defineret i forvejen. Princippet er at metodekald bliver til elementer, parametre til attributter og closures bliver til elementets børn. Her er et forsøg på en pædagogisk forklaring af princippet:

På denne vis opbygger vi en Soap-Envelope med en XML-struktur for et TextRepository eller med en SoapFault.

Response-dokumentet

Response-dokumentet er ekstremt simpelt, da det blot indeholder en expression, som refererer den response-attribut, der indeholder den opbyggede Soap-Envelope.

Test

Vi har nu alle elementerne på plads i løsningen og kan starte vores Mock Service. For at teste den bruger vi den velkendte klient-del af SoapUI:

Og kaldes operationen med en ukendt kombination af Entity/Attribute fås en Soap-Fault som forventet:

Konklusion

Efter jeg lærte at lave dynamiske mock services i SoapUI er det blevet min foretrukne teknik, når en service skal simuleres. Jeg har tidligere benyttet JAX-WS til formålet og publiceret enten via en Web Container eller direkte med Endpoint.publish-metoden. Det er klart hurtigere at benytte SoapUI og jeg vil gerne fremhæve følgende lækre features ved SoapUI:

  • Lav turn-around-tid: Når du laver ændringer i Scriptet eller Mock-Servicen i øvrigt, skal du hverken gemme eller redeploye for at ændringerne træder i kraft. I og med at Groovy-scriptet fortolkes pr. request, slår ændringer igennem med det samme. Prisen er dog at man ofte glemmer at gemme projektet – og så er ens fine Mock-Service væk når maskinen eller SoapUI crasher.
  • Kompakt kode, som er let at tilpasse: Groovy-kode fylder generelt mindre end tilsvarende Java-kode. Desuden er Groovys MarkupBuilder reelt et DSL for konstruktion af markup-baserede dokumenter. Groovy-koden ligner ganske enkelt den dokumentstruktur, som koden skal producere. I praksis betyder det, at man kan opbygge Groovy-koden ved at indsætte det XML-response, som skal produceres og dernæst lave en næsten triviel transformation til Groovy-syntaksen. Bemærk fx ligheden mellem XML-strukturen og den Groovy-kode der opbygger den:

Groovy:

xml.'soapenv:Envelope'('xmlns:soapenv': 
 'http://schemas.xmlsoap.org/soap/envelope/') {
      'soapenv:Body'{
         'soapenv:Fault'{
           faultcode("soapenv:Client")
          faultstring("TextRepository ${entity}/${attribute} not found!")
             detail {
             'tex:NoSuchTextRepository'('xmlns:tex':
              'http://example.lundogbendsen.dk/textrepository') {
                  'tex:RepositoryId'("${entity}/${attribute}")
                  'tex:Message'("TextRepository not found!")
                                  }
                               }    
                            }
                      }       
                   }

XML:

<soapenv:Envelope xmlns:
 soapenv="http://schemas.xmlsoap.org/soap/envelope/">
   <soapenv:Body>
      <soapenv:Fault>
        <faultcode>soapenv:Client</faultcode>
        <faultstring>TextRepository Person/Ukendt not found!</faultstring>
        <detail>
            <tex:NoSuchTextRepository xmlns_tex=
             "http://example.lundogbendsen.dk/textrepository">
               <tex:RepositoryId>Person/Ukendt</tex:RepositoryId>
               <tex:Message>TextRepository not found!</tex:Message>
            </tex:NoSuchTextRepository>
         </detail>
      </soapenv:Fault>
   </soapenv:Body>
</soapenv:Envelope>

På den negative side, skal nævnes SoapUIs Groovy-editor, som ikke giver anden hjælp end syntaks-highlight af koden. Skal man skrive større programmer, er det anbefalelsesværdigt at skrive koden i fx Eclipse med Groovy-plugins og til sidst kopiere den over i SoapUI.

Jeg håber, at jeg har vakt din interesse for at lave dynamiske Mock-Services i SoapUI med denne artikel. Hvis du vil læse mere om emnerne, så er følgende links relevante:

Jakob Bendsen Profil

Jakob Bendsen

Chefkonsulent | Partner

Jakob har arbejdet professionelt med it og softwareudvikling siden 1990’erne. Java og JVM frameworks (Java EE, Spring…) er hans hjemmebane, men også cloud, virtualisering og integrationteknologi (fx REST og GraphQL) er i værktøjskassen. Han er specialist indenfor facilitering af effektive udviklingsprocesser og enterprisearkitektur. 20+ års erfaring med undervisning, mentoring og rådgivning indenfor softwareudvikling i mange brancher, og altid med øjet rettet mod samspillet mellem forretning og IT.