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:
- Et Excel regneark med data
- En ODBC-forbindelse til regnearksfilen
- Et SoapUI-projekt med et Groovy Script
- 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: