Bob Swart (aka Dr.Bob)
Debuggen, Tracen en Testen van Win32 Web Services

SOAP en Web Services zijn in mijn ogen een van de (mogelijke) bouwstenen voor een SOA. In dit artikel duik ik in de Delphi Win32 Web Service archtectuur (beschikbaar vanaf Delphi 6), en laat zien hoe we Delphi Web Services kunnen testen, tracen en debuggen.
Debuggen kan onder andere met behulp van de Web App Debugger, die echter vanaf Delphi 2005 niet meer zonder meer werkt (vanwege de nieuwere versie van Indy), maar die wel weer werkbaar te krijgen is.
Voor het tracen zal ik laten zien hoe we zowel binnenkomende SOAP requests als uitgaande SOAP responses kunnen "opvangen" en weergeven in een logfile (of debug window). Het is zelfs mogelijk om SOAP berichten op die manier aan te passen indien gewenst, en ik zal hier tijdens de sessie een voorbeeld (uit de praktijk!) van geven.
Voor het testen van Web Service "engines" kunnen we sinds Delphi 2005 gebruik maken van het ingebouwde Unit Testing framework. Met behulp van DUnit zal ik laten zien hoe we een Delphi SOAP Server op eenvoudige - en herbruikbare - wijze kunnen testen en valideren.

Delphi 2007 SOAP Server Application
Een groot deel van de code in dit artikel werkt met alle Win32 versies van Delphi sinds Delphi 6. Toch zal ik van de meest recente versie Delphi 2007 gebruik maken, en hier en daar melding maken van nieuwe features en mogelijkheden. Allereerst wil ik een Win32 Web Service toepassing bouwen, en daarvoor kunnen we in Delphi gebruik maken van de SOAP Server Application wizard in de WebServices categorie van de Object Repository (zie Figuur 1).

New SOAP Server Application Icon

In de danvolgende Wizard kunnen we kiezen voor ISAPI, CGI of de Web App Debugger Executable als web server applicatie type.
Omdat ik wil demonstreren hoe we SOAP toepassingen kunnen tracen en debuggen, ligt de keuze voor de Web App Debugger Executable voor de hand. Specificeer een Class Name, zoals SDE (zie ook Figuur 2).

New SOAP Server Application Wizard

Er volgt nu een vraag of we een interface voor de SOAP module willen maken. Dat kunnen we ook later doen (met de SOAP Interface wizard uit de Object Repository), maar in beide gevallen krijgen we dezelfde dialoog van Figuur 3 te zien.
Ik heb hier als Service de naam “MyService” ingevuld. De unit identifier wordt dan automatisch ook op MyService gezet, wat leidt tot een MyServiceIntf.pas unit voor de interface definitie en een MyServiceImpl.pas unit voor de implementatie, zoals we zo zullen zien. Commentaar en voorbeeld methodes zijn altijd handig, want die kunnen we makkelijk uitbreiden met onze eigen wensen.

SOAP Server Interface Wizard

Voor we verder gaan wil ik eerst de bestanden van het project wat zinvolle(re) namen geven. Behalve de gegenereerde MyServiceIntf.pas en MyServiceImpl.pas bevat ons project nu namelijk ook een Unit1.pas en Unit2.pas. Unit1.pas is een leeg form dat gebruikt wordt als “main window” van onze toepassing als Web App Debugger client. Ik bewaar dat meestal in WADForm.pas.
Unit2.pas bevat de web module, met daarop al een drietal componenten: HTTPSoapDispatcher, HTTPSoapPascalInvoker en WSDLHTMLPublish. Deze unit bewaar ik onder de naam SWebMod.pas.
Het project zelf bewaar ik als SDESoapServerDemo.dpr.

WAD en Indy
Voordat we verdere wijzigingen gaan aanbrengen aan onze SOAP Server, moeten we eerst even (proberen te) compileren. Met Delphi 6 of 7 gaat dat zonder problemen, maar vanaf Delphi 2005 krijg je hier een foutmelding: Unit SockApp was compiled with a different version of IdTCPServer.TIdTCPServer.
Overigens is het mogelijk dat gebruikers van Delphi 2005 en 2006 deze foutmelding niet krijgen, maar dan alleen als zij bij de installatie van Delphi gekozen hebben om Indy 9 in plaats van Indy 10 te installeren. Het probleem wordt namelijk veroorzaakt doordat de Web App Debugger units in de tijd van Delphi 7 met Indy 9 zijn gebouwd. En helaas is Indy versie 10 een “breaking release”, waardoor de Web App Debugger het niet meer doet onder Indy 10. Er zijn aanpassingen mogelijk om de Web App Debugger ook met Indy 10 te laten werken, maar dan moet je zelf aanpassingen in de source code gaan maken (zie ook de readme bij Delphi 2007 voor details). Een snellere oplossing is om Delphi te instrueren om niet Indy 10 maar Indy 9 te gebruiken voor dit ene project. Het is namelijk gelukkig nog wel zo dat Indy 9 naast Indy 10 in je Delphi directory wordt geïnstalleerd (overigens ook bij Delphi 2007, ondanks het feit dat deze niet langer vraagt of je Indy 9 of Indy 10 wilt hebben, maar standaard gewoon Indy 10 neerzet).
In alle gevallen moeten we de Project Options dialog openen, en dan in de Directories/Conditionals pagina het Search path aanpassen, zodat $(BDS)\Lib\Indy9 in het path staat.

Project Options – Search Path

Merk op dat $(BDS) nog steeds gebruikt wordt ook al heet Delphi 2007 officieel de CodeGear RAD Studio en niet meer de Borland Developer Studio.

Met deze kleine aanpassing compileert het project weer, en kunnen we de SDESoapServerDemo als Web App Debugger client draaien. Dit levert een leeg form op – op zich niet zo heel erg nuttig, maar achter de schermen is ook het MyService SOAP object te benaderen. En te debuggen.
Dit kunnen we demonstreren door uit het Tools menu in Delphi, de Web App Debugger zelf te starten. Dit is de server die alle requests doorsluist naar onze Web App Debugger client. Hierdoor kunnen we breakpoints zetten in het SDESoapServerDemo project en de Delphi debugger gebruiken indien noodzakelijk.

Nog een opmerking over de Web App Debugger: in de SWebMod.pas unit is een extra regel code opgenomen in de initialization sectie die de WebModuleClass van de WebRequestHandler een waarde geeft. Dit is alleen zinnig bij een Web App Debugger client, en deze regel code moeten we dan ook niet vergeten te verwijderen als we de SWebMod unit later in een CGI of ISAPI SOAP Server applicatie hergebruiken.
Een leuke truck hiervoor is gebasseerd op het feit dat een Web App Debugger client een zgn. GUI toepassing is, maar een CGI of ISAPI web server toepassing niet. En tijdens runtime kunnen we met de IsConsole variabele bekijken of we een console of GUI toepassing zijn. Oftewel, de code in de initialization sectie van de SWebMod.pas kunnen we als volgt aanpassen:

  initialization
    if not IsConsole then
      WebRequestHandler.WebModuleClass := TWebModule2;
  end.

Win32 SOAP Servers Tracen
Met een Web App Debugger client kunnen we breakpoints zetten in de Win32 SOAP Web Service. Dat is nuttig, maar vaak is het nuttiger om te zien welke SOAP berichten er heen en weer worden gestuurd. Daar kun je speciale tools voor inzetten, die het berichtenverkeer tussen twee of meer toepassingen gaan besnuffelen, maar het is niet al te moeilijk om dat in je eigen Win32 SOAP Server toepassing te doen (dat biedt je straks meteen de kans om de binnenkomende of uitgaande SOAP berichten hier en daar nog een beetje aan te passen indien nodig).

Als we even teruggaan naar de SOAP Web Module in SWebMod.pas, dan zien we daar de drie componenten van het Delphi Win32 SOAP Framework. De HTTPSoapDispatcher gebruikt de HTTPSoapPascalInvoker om het binnenkomende SOAP bericht te verwerken een SOAP antwoord te genereren. De HTTPSoapPascalInvoker heeft een drietal events die we kunnen gebruiken om in dit proces mee te kijken (en eventueel in te grijpen).
Het OnBeforeDispatchEvent event zal worden gevuurd als het bericht is ontvangen maar nog voordat de bijbehorende methode van het gewenste SOAP object wordt uitgevoerd. Het event heeft een tweetal parameters: MethodName en Request.

  procedure TWebModule2.HTTPSoapPascalInvoker1BeforeDispatchEvent(
    const MethodName: string; const Request: TStream);
  var
    StrStream: TStringList;
  begin
    StrStream := TStringList.Create;
    try
      Request.Position := 0;
      StrStream.LoadFromStream(Request);
      {$IFDEF DEBUG}
      Log('Before Dispatch:', StrStream.Text)
      {$ENDIF}
    finally
      StrStream.Free;
      Request.Position := 0
    end
  end;
Merk op dat er ook een OnBeforeDispatchEvent2 event is, met meer argumenten voor wie ze nodig heeft:
  procedure TWebModule2.HTTPSoapPascalInvoker1BeforeDispatchEvent2(
    const MethodName: string; const Request: TStream; Response: TStream;
    var BindingType: TWebServiceBindingType; var Handled: Boolean);
  begin

  end;
De OnBeforeDispatchEvent2 kunnen we gebruiken in situaties waarin we zelf onze SOAP afhandeling willen implementeren (vandaar de Handled: Boolean parameter). Dat is niet iets dat ik in dit artikel wil doen, maar wel nuttig om te weten.
Na afloop van het uitvoeren van de SOAP method, wordt het antwoord samengesteld, en kunnen we in de OnAfterDispatchEvent kijken wat er in het response staat.
  procedure TWebModule2.HTTPSoapPascalInvoker1AfterDispatchEvent(
    const MethodName: string; SOAPResponse: TStream);
  var
    StrStream: TStringList;
  begin
    StrStream := TStringList.Create;
    try
      SOAPResponse.Position := 0;
      StrStream.LoadFromStream(SOAPResponse);
      {$IFDEF DEBUG}
      Log('After Dispatch:',StrStream.Text)
      {$ENDIF}
    finally
      StrStream.Free;
      SOAPResponse.Position := 0
    end
  end;

Win32 Echo Server
Tijd om wat code aan onze eigen MyService toe te voegen. Als we voor de example methods hebben gekozen, krijgen we al enkele “echo” methoden. Die zijn uitermate geschikt om de interoperabiliteit tussen onze Win32 SOAP Server en andere talen en/of platforms te testen.
Om een aantal kleine problemen met interoperabiliteit tussen Win32 en .NET te demonstreren, moeten we een paar kleine uitbreidingen doen aan de MyServiceIntf.pas en MyServiceImpl.pas units.
In MyServiceIntf.pas wil ik de TMyEmployee uitbreiden met een Birthdate veld van type TXSDateTime. De definitie komt er hierdoor als volgt uit te zien:

  TMyEmployee = class(TRemotable)
  private
    FLastName: AnsiString;
    FFirstName: AnsiString;
    FSalary: Double;
    FBirthdate: TXSDateTime;
  published
    property LastName: AnsiString read FLastName write FLastName;
    property FirstName: AnsiString read FFirstName write FFirstName;
    property Salary: Double read FSalary write FSalary;
    property Birthdate: TXSDateTime read FBirthdate write FBirthdate;
  end;
Daarnaast wil ik de verzameling echoXXX methods van de IMyService uitbreiden met een echoDateTime, zodat het IMyService interface er als volg uit komt te zien:
  { Invokable interfaces must derive from IInvokable }
  IMyService = interface(IInvokable)
  ['{721D51D6-BEA6-4D00-A152-507D7CC42F86}']

    { Methods of Invokable interface must not use the default }
    { calling convention; stdcall is recommended }
    function echoEnum(const Value: TEnumTest): TEnumTest; stdcall;
    function echoDoubleArray(const Value:TDoubleArray): TDoubleArray; stdcall;
    function echoMyEmployee(const Value: TMyEmployee): TMyEmployee; stdcall;
    function echoDouble(const Value: Double): Double; stdcall;
    function echoDateTime(const Value: TXSDateTime): TXSDateTime; stdcall;
  end;
In de MyServiceImpl.pas moeten we nu de definitie van TMyService aanpassen, en de implementatie van de echoMyEmployee en echoDateTime completeren. De twee methods zijn als volgt geïmplementeerd:
  function TMyService.echoMyEmployee(const Value: TMyEmployee): TMyEmployee; stdcall;
  begin
    Result := TMyEmployee.Create;
    Result.LastName := Value.LastName;
    Result.FirstName := Value.FirstName;
    Result.Salary := Value.Salary;
    Result.Birthdate := Value.Birthdate.Clone
  end;

  function TMyService.echoDateTime(const Value: TXSDateTime): TXSDateTime; stdcall;
  begin
    Result := Value.Clone
  end;
Een Delphi Win32 client aan deze Delphi Win32 server koppelen heeft weinig zin, want dan zal alles naar wens werken. Het is juist spannend om er eens een andere client aan te hangen, zoals een C# of Delphi for .NET client.
C# zal ik straks nog even laten zien (bij een .NET 2.0 server), dus gaan we nu nog even verder met Delphi for .NET.

Delphi for .NET Client
Start een nieuw Delphi for .NET toepassing (dit kan nog niet met Delphi 2007, dus hier zullen we Delphi 2006 voor moeten gebruiken bijvoorbeeld), zoals een VCL for .NET toepassing. Via Add Web Reference in the Project Manager kun je een link naar de WSDL van een SOAP Server laten importeren. Voor onze Win32 Web Service is deze URL http://localhost:8081/SDESoapServerDemo.SDE/wsdl/IMyService en die kunnen we invoeren in de Add Web Reference dialoog:

Add Web Reference dialoog

Nadat de web reference is toegevoegd aan ons project, kunnen we de gegenereerde unit gebruiken om een instantie van de IMyServiceservice Web Servce te gebruiken. Er zijn twee dingen die ik wil testen (de rest gaat gewoon goed): echoDouble en echoMyEmployee, en van de laatste dan vooral het Birthdate veld.
Beginnend met de echoDouble, kan ik de volgende code gebruiken om te testen of wat er in gaat ook gelijk is aan wat eruit komt. Met “normale” floating point getallen gaat dit goed, maar met een breuk zoals 42/9 komen we een beetje in de problemen.

  procedure TForm2.btnEchoDoubleClick(Sender: TObject);
  const
    Value = 42/9;
  var
    WS: IMyServiceservice;
    X: Double;
  begin
    WS := IMyServiceservice.Create;
    X := WS.echoDouble(Value);
    if X <> Value then ShowMessage('Niet OK')
  end;
De messagebox met “Niet OK” wordt vertoond als we 42/9 proberen te echo’en. Als we hier willen zien wat er mis is, moeten we in de trace kijken en het binnenkomende SOAP request vergelijken met de uitgaande SOAP response. Ik heb alles even weggehaald behalve de values, en dan zien we dat er het volgende in komt:
  13:59:39.869 Before Dispatch:
  13:59:39.869 <Value xsi:type="xsd:double">4.666666666666667</Value>
En het volgende als echo antwoord weer uitgaat:
  13:59:39.879 After Dispatch:
  13:59:39.879 <return xsi:type="xsd:double">4.66666666666667</return>
Het is moeilijk te zien, maar in het request staan er 14 6-jes voor de 7, en in de response staan er maar 13 6-jes. Het probleem is niet echt een probleem, maar wordt dus veroorzaakt door het feit dat er op verschillende manieren van een double waarde naar een SOAP XML string wordt ge-marshalled. In .NET is een Double een type dat kennelijk 16 significante cijfers heeft. Voor Win32 geeft de online help aan dat een Double 15-16 significante decimalen heeft. 15 dus in ons voorbeeld.
Het gekke is bovendien dat als we het verschil tussen X en Value willen laten zien, dat we dan 0 krijgen. In praktijk zal het probleem tussen 15 of 16 significante decimalen nauwelijks een echt issue zijn, maar dat is mijn persoonlijke mening.
Interessanter is de test van de TXSDateTime data – zowel als losse parameter en als veld van de TMyEmployee class.
  procedure TForm2.btnechoMyEmpClick(Sender: TObject);
  var
    WS: IMyServiceservice;
    Emp, Emp2: TMyEmployee;
  begin
    WS := IMyServiceservice.Create;
    Emp := TMyEmployee.Create;
    Emp.FirstName := 'Bob';
    Emp.LastName := 'Swart';
    Emp.Salary := 42;
    Emp.Birthdate := DateTime.Now;
    Emp2 := WS.echoMyEmployee(Emp);
    if Emp2.FirstName <> Emp.FirstName then ShowMessage('FirstName differs');
    if Emp2.LastName <> Emp.LastName then ShowMessage('LastName differs');
    if Emp2.Salary <> Emp.Salary then ShowMessage('Salary differs');
    if Emp2.Birthdate.Date <> Emp.Birthdate.Date then
      ShowMessage(Emp.Birthdate.ToString + ' <> ' + Emp2.Birthdate.ToString)
    else
      ShowMessage(Emp.Birthdate.ToString + ' = ' + Emp2.Birthdate.ToString);
    // now just echo the Birthdate value
    Emp2.Birthdate := WS.echoDateTime(Emp.Birthdate);
    if Emp2.Birthdate.Date <> Emp.Birthdate.Date then
      ShowMessage(Emp.Birthdate.ToString + ' <> ' + Emp2.Birthdate.ToString)
  end;
Deze interoperability test gaat goed als de Win32 SOAP Server is gecompileerd met Delphi 2007, maar bij oudere versies van Delphi krijgen we een foutmelding, en is het Birthdate datum veld dat we terugkrijgen in de MyEmployee helemaal leeg!
Om dat te illustreren kunnen we het SDESoapServerDemo project eens sluiten in Delphi 2007 en openen in Delphi 2006 om het daar opnieuw te compileren en te runnen in de Web App Debugger.
We krijgen dan een foutmelding, die wordt veroorzaakt ergens diep in de VCL SOAP source code.
De logfile laat het volgende zien:
  <Birthdate xsi:type="xsd:dateTime">2007-05-31T13:59:10.3170672+02:00</Birthdate>
Delphi 2007 for Win32 geeft het volgende – goede – antwoord:
  <Birthdate xsi:type="xsd:dateTime">2007-05-31T14:25:23.9798832+02:00</Birthdate>
Maar Delphi 6 t/m 2006 geven het volgende – foute – antwoord:
  <xsd:Birthdate xsi:type="xsd:dateTime">2007-05-31T13:59:10.3170672+02:00</xsd:Birthdate>
Het verschil zit hem in de xsd: prefix van de Birthdate. Met deze prefix erbij gaat het fout, en zonder de prefix gaat het goed. Dit kunnen we op twee manieren oplossen. Enerzijds kunnen we diep in de VCL SOAP units kijken waar het probleem optreedt. Om een lange zoektocht en bijbehorende verhaal kort te maken: dat blijkt regel 2765 van de unit OPToSOAPDomConv.pas te zijn. Hier zouden we de lege [] moeten veranderen in [ocoDontPrefixNode] om de prefix te onderdrukken en een goed gedrag te krijgen in Delphi 2006. Merk op dat het in Delphi 2007 meteen al goed werkt, dus daar zijn geen verdere aanpassingen nodig.
Een alternatieve manier voor Delphi 2006 bestaat uit het – achteraf – aanpassen van de SOAP response in de OnAfterDispatchEvent, als volgt:
  procedure TWebModule2.HTTPSoapPascalInvoker1AfterDispatchEvent(
    const MethodName: string; SOAPResponse: TStream);
  var
    StrStream: TStringList;
    Str: String;
    Modified: Boolean;

  begin
    StrStream := TStringList.Create;
    try
      SOAPResponse.Position := 0;
      StrStream.LoadFromStream(SOAPResponse);
      Str := StrStream.Text;
      {$IFDEF DEBUG}
      Log('After Dispatch:',Str);
      {$ENDIF}

      Modified := False;
      while Pos('xsd:Birthdate',Str) > 0 do
      begin
        Modified := True;
        Delete(Str,Pos('xsd:Birthdate',Str),4)
      end;
      {$IFDEF DEBUG}
      if Modified then Log('Modified SOAP Response:',Str);
      {$ENDIF}

      SOAPResponse.Size := 0;
      SOAPResponse.Position := 0;
      StrStream.Text := Str;
      StrStream.SaveToStream(SOAPResponse)

    finally
      StrStream.Free;
      SOAPResponse.Position := 0
    end
  end;
Deze aanpassing werkt voor Delphi 2006, en is natuurlijk geheel voor eigen risico, maar het laat wel zien hoe we de SOAPResponse nog net kunnen aanpassen voor de Win32 SOAP Web Service het antwoord terugstuurt naar de client.

.NET SOAP Server
Nu we hebben gezien dat er soms wat mis kan gaan tussen een Win32 SOAP Server en een .NET SOAP Client, is het tijd om het verhaal om te draaien. Als kleine demonstratie heb ik een C# Web Service geschreven in EchoNET2.asmx, die we onder ASP.NET 2.0 kunnen laten draaien.

  <%@ WebService Language="C#" Class="ASPNET2WS.Echo" %>

  using System;
  using System.Web.Services;

  namespace ASPNET2WS
  {
    [WebService(Namespace="http://eBob42.org")]
    public class Echo: System.Web.Services.WebService
    {
      [WebMethod(Description="Echo a value of type double")]
      public double echoDouble(double doubleValue)
      {
        return doubleValue;
      }

      [WebMethod(Description="Echo a value of type DateTime")]
      public DateTime echoDateTime(DateTime DateTimeValue)
      {
        return DateTimeValue;
      }

      public struct MyEmployee
      {
        public String FirstName;
        public DateTime BirthDate;
      }
      [WebMethod(Description="Echo a value of type MyEmployee")]
      public MyEmployee EchoMyEmployee(MyEmployee emp)
      {
        return emp;
      }
    }
  }
Dit voorbeeld is op het internet te vinden als http://www.ebob42.com/cgi-bin/EchoNET2.asmx?WSDL maar kan ook lokaal getest worden. In dat geval kunnen we de URL http://localhost/cgi-bin/EchoNET2.asmx?WSDL in Delphi importeren met de WSDL Importer.

WSDL Importer dialoog

Zowel met Delphi 2006 als Delphi 2007 kunnen we dan een client toepassing schrijven:

  procedure TForm2.btnEchoClick(Sender: TObject);
  var
    WS: EchoSoap;
    Emp, Emp2: MyEmployee;
  begin
    WS := GetEchoSoap;
    Emp := MyEmployee.Create;
    Emp.Birthdate := TXSDateTime.Create;
    Emp.FirstName := 'Robert';
    Emp.Birthdate.AsDateTime := Now;
    Emp2 := WS.echoMyEmployee(Emp);
    ShowMessage(DateTimeToStr(Emp2.Birthdate.AsDateTime));
    Emp2.Birthdate := WS.echoDateTime(Emp.Birthdate);
    ShowMessage(DateTimeToStr(Emp2.Birthdate.AsDateTime))
  end;
Dit zal laten zien dat het in Delphi 2007 wel werkt, maar voor eerdere versies van Delphi niet. Overigens alleen als de C# Web Service onder ASP.NET 2.0 draait. Bij het configureren van de virtual directory als een ASP.NET 1.1 Web Service draait het zonder problemen. De oorzaak van het interoperability probleem moet dan ook gezocht worden in de verschillen tussen ASP.NET 1.1 en ASP.NET 2.0, en blijken het gevolg te zijn van het op een andere manier aangeven van de document|literal binding die ASP.NET web services gebruiken. De Delphi WSDL Importer – met uitzondering van die van Delphi 2007 – is niet in staat om de nieuwe manier waarop dit wordt aangegeven te herkennen. Met als gevolg dat een cruciale declaratie achterwege wordt gelaten: het feit dat het SOAP interface als een zgn. ioDocument interface geregistreerd met worden.
In praktijk is dat makkelijk op te lossen, door één regel code aan het eind van de SOAP import unit toe te voegen. In ons voorbeeld is die regel als volgt:
  InvRegistry.RegisterInvokeOptions(TypeInfo(EchoSoap), ioDocument);
En daarmee draait het ook onder oudere versies van Delphi weer als een trein.

HTTPRio SOAP Clients Tracen
Los van deze snelle oplossing, is het soms toch handig om te zien wat er achter de schermen gebeurt. Om bij een Delphi Win32 SOAP Client bij te houden wat er in komt er wat er uit gaat, moeten we een eigen instantie van de HTTPRio maken en daar enige events van invullen. We hebben zowel een OnBeforeExecute als een OnAfterExecute event waar we iets kunnen doen. Om het vorige Log voorbeeld te herhalen, zou dat als volgt kunnen gaan:

  procedure TForm7.HTTPRIO1BeforeExecute(const MethodName: string;
    var SOAPRequest: WideString);
  begin
    {$IFDEF DEBUG}
    Log('DoBeforeRequest:',SOAPRequest)
    {$ENDIF}
  end;

  procedure TForm7.HTTPRIO1AfterExecute(const MethodName: string;
    SOAPResponse: TStream);
  var
    StrList: TStringList;
  begin
    StrList := TStringList.Create;
    try
      SOAPResponse.Position := 0;
      StrList.LoadFromStream(SOAPResponse);
      {$IFDEF DEBUG}
      Log('DoBeforeRequest:',StrList.Text)
      {$ENDIF}
    finally
      SOAPResponse.Position := 0;
      StrList.Free
    end
  end;
Het enige jammere is dat in Delphi 2006 (en eerder) de SOAPRequest WideString parameter van de OnBeforeExecute helemaal niet gewijzigd kan worden. Het kan wel, maar het heeft geen effect. Met ingang van Delphi 2007 is dat weer hersteld, en kunnen we vrolijk wijzigingen maken in de SOAPRequest – bijvoorbeeld de ‘o’ in Robert veranderen in een ‘u’ om zo tot Rubert te komen.
  procedure TForm2.HTTPRIO1BeforeExecute(const MethodName: string;
    var SOAPRequest: WideString);
  begin
    while Pos('Robert', SOAPRequest) > 0 do
    begin
      SOAPRequest[Pos('Robert', SOAPREquest)+1] := 'u'
    end;
    ShowMessage(SOAPRequest)
  end;
Dit werkt dus pas in Delphi 2007 (en niet in eerdere versies). Om het ook in oudere versies van Delphi te laten werken heb ik een eigen THTTPRio class gemaakt die als volgt geïmplementeerd is, en zelf het SOAPRequest aanpast nadat de OnBeforeExecute event handler is aangeroepen. De source code van deze nieuwe class – handig voor wie nog Delphi 2006 gebruikt bijvoorbeeld – is als volgt:
  type
    THTTPRIO2 = class(THTTPRIO)
      constructor Create(Owner: TComponent); override;
      procedure BeforeExecuteHandler(const MethodName: String;
        var SOAPRequest: WideString);
      procedure AfterExecuteHandler(const MethodName: String;
        SOAPResponse: TStream);
    protected
      procedure DoBeforeExecute(const MethodName: string;
         Request: TStream); override;
    end;

  { THTTPRIO2 }

  constructor THTTPRIO2.Create(Owner: TComponent);
  begin
    inherited;
    OnBeforeExecute := BeforeExecuteHandler;
    OnAfterExecute := AfterExecuteHandler
  end;

  procedure THTTPRIO2.DoBeforeExecute(const MethodName: string;
    Request: TStream);
  var
    StrList: TStringList;
    Str: WideString;
  begin
    if Assigned(OnBeforeExecute) then
    begin
      StrList := TStringList.Create;
      try
        Request.Position := 0;
        StrList.LoadFromStream(Request);
        Str := StrList.Text;
        OnBeforeExecute(MethodName, Str);
        {$IFDEF DEBUG}
        Log('DoBeforeRequest:',Str);
        {$ENDIF}
        Request.Size := 0;
        Request.Position := 0;
        StrList.Text := Str;
        StrList.SaveToStream(Request)
      finally
        Request.Position := 0;
        StrList.Free
      end
    end
  end;

  procedure THTTPRIO2.BeforeExecuteHandler(const MethodName: String;
    var SOAPRequest: WideString);
  begin
    {$IFDEF DEBUG}
    Log('Before Execute:', SOAPRequest)
    {$ENDIF}
  end;

  procedure THTTPRIO2.AfterExecuteHandler(const MethodName: String;
    SOAPResponse: TStream);
  var
    StrStream: TStringList;
    Str: String;
  begin
    StrStream := TStringList.Create;
    try
      SOAPResponse.Position := 0;
      StrStream.LoadFromStream(SOAPResponse);
      Str := StrStream.Text;
      {$IFDEF DEBUG}
      Log('After Execute:', Str);
      {$ENDIF}
      SOAPResponse.Position := 0;
      StrStream.Text := Str;
      StrStream.SaveToStream(SOAPResponse)
    finally
      StrStream.Free;
      SOAPResponse.Position := 0
    end
  end;
Wie meer informatie of ondersteuning zou willen hebben bij de toepassing van deze nieuwe THTTPRIO2 class kan me natuurlijk altijd een e-mailtje sturen.

SOAP Interfaces Testen met DUnit
Nu we het tracen van SOAP en het debuggen van de SOAP server en client toepassingen zelf hebben gezien, blijft er nog één onderwerp over: het daadwerkelijk testen van de SOAP methods met behulp van DUnit. Waar we in de voorgaande voorbeelden o.a. de SOAP berichten heen en weer bekeken om te zien wat er precies over de lijn ging, en waar het mogelijk fout ging, daar hebben we soms behoefte aan het testen van de methods van het SOAP object zelf, zonder daarbij de SOAP laag zelf te gebruiken. Oftewel: het lokaal aanroepen en testen van de methods; als een soort van droogzwem simulatie om te zien of de onderliggende berekeningen en implementaties van de methods goed zijn. Bij echoXXX methods is dat natuurlijk minder zinvol (de kans is niet zo groot dat een echo niet werkt), maar vooral bij ingewikkeldere methods kan het zinvol zijn om daar een aantal tests van de implementeren en bij de hand te houden. DUnit kan daarbij helpen.
Vanuit de Delphi IDE kunnen we binnen de projectgroep van het bestaande project een nieuw project toevoegen, en daarbij moeten we dan kiezen voor een Test Project. In de Test Project Wizard die dan volgt zal als project automatisch de naam van het actieve project worden ingevuld (SDESoapServerDemo) met “Tests” erachter. Als locatie wordt voorgesteld om een Test subdirectory te maken onder de huidige project directory (dat werkt erg prettig, want hierdoor staat het test project los van het daadwerkelijke project, maar kan het toch eenvoudig van alle source files gebruik maken door .. in het Search Path op te nemen).
Tot slot is het belangrijk om als personlijkheid “Delphi” op te geven (en niet bijvoorbeeld Delphi for .NET), en het test project toe te voegen aan de project groep, zodat er eenvoudig heen-en-weer geswitched kan worden.

Test Project Wizard

Het test project is nog leeg, maar daar kunnen we nieuwe test cases aan toevoegen. In de Unit Test categorie van de Object Repository vinden hiervoor de betreffende icon die voor ons de Test Case Wizard start. Hier kunnen we uit de project direcotry units selecteren waar de source code van gescanned zal worden, op zoek naar de public methods van classes. In het geval van onze SDESoapServerDemo is er eigenlijk maar één unit die relevant is, en dat is de MyServiceImpl.pas unit, want die bevat als enige het TMyService object en de bijbehorende echoXXX methods. De MyServiceIntf.pas unit bevat alleen de interface definitie, maar niet de implementatie (en die willen we juist testen!).
Kies dus voor de Impl.pas unit in de Test Case Wizard, en selecteer vervolgens alle SOAP classes en binnen deze classes de methods die we willen testen (zie Figuur 8).

Test Case Wizard

Er zal nu een speciale TestMyServiceImpl.pas unit gegenereerd worden, met daarin een TestTMyService class die behalve een Setup en TearDown method ook voor iedere geselecteerde methode uit de TMyService class (zie Figuur 8) een test methode heeft toegevoegd aan de TestTMyService class.
In ons geval, leidt dat tot de volgende definitie:

  type
    // Test methods for class TMyService
    TestTMyService = class(TTestCase)
    strict private
      FMyService: TMyService;
    public
      procedure SetUp; override;
      procedure TearDown; override;
    published
      procedure TestechoEnum;
      procedure TestechoDoubleArray;
      procedure TestechoMyEmployee;
      procedure TestechoDouble;
      procedure TestechoDateTime;
    end;
In de Setup wordt al een instantie van TMyService aangemaakt, en die wordt in de TearDown weer vrijgegeven. In de verschillende TestechoXXX methods kunnen we gebruikmaken van deze instantie om de echo methode te testen zonder daarbij “gestoord” te worden door de SOAP laag eromheen.
Voor de TestechoMyEmplyee kan de implementatie er bijvoorbeeld als volgt uit komen te zien (waarbij we de waarde van Birthdate testen):
  procedure TestTMyService.TestechoMyEmployee;
  var
    ReturnValue: TMyEmployee;
    Value: TMyEmployee;
  begin
    Value := TMyEmployee.Create;
    Value.Birthdate := TXSDateTime.Create; // anders AVS
    try
      Value.FirstName := 'Bob';
      ReturnValue := FMyService.echoMyEmployee(Value);
      CheckEqualsString(Value.FirstName, ReturnValue.FirstName, 'FirstName')
    finally
      Value.Birthdate.Free;
      Value.Free
    end
  end;
Let op dat de verschillende CheckXXX routines die beschikbaar zijn binnen DUnit Test Cases als eerste argument het te verwachten antwoord hebben, en als tweede argument het daadwerkelijk berekende antwoord (op die manier wordt ook de foutmelding opgebouwd: expected XXX but received YYY).
Er is één probleem met het gebruik van DUnit in Delphi 2007: en dat is het feit dat Delphi 2007 niet de volledige source code van DUnit installeert op disk (wat wel zou moeten), maar wel de volledige verzameling .dcu files. Op zich geen probleem, ware het niet dat alle nieuwe DUnit projecten in Delphi 2007 in het Search Path de directory $(BDS)\Source\DUnit\src met de DUnit source code hebben staan. En dus krijg je een compiler error, dat de unit FastMMMemLeakMonitor niet gevonden kan worden. Dit is op te lossen door in de Project Options het Search Path aan te passen (en er alleen .. in te zetten, zie Figuur 9).

Delphi 2007 Project Options

Dit lost het probleem weer op. Helaas komt het probleem weer terug zodra je een nieuwe Test Case toevoegt (zie Figuur 8), waardoor je steeds opnieuw het Search Path zal moeten aanpassen. Gelukkig is CodeGear op de hoogte van het probleem, en komt er binnenkort een fix (of misschien is die er inmiddels al), waarbij de volledige source code van DUnit zit, waaronder ook de FastMMMemLeakMonitor unit dus.
Tijdens de SDE sessie op vrijdag 1 juni kreeg ik overigens nog een mooie Access Violation (die de mensen in de zaal zich vast nog kunnen herinneren), die veroorzaakt werd door het feit dat ik in de TestechoMyEmployee method het Birthdate veld niet had gecreeerd. Dat lijkt niet zo’n probleem, maar als je de implementatie van de echoMyEmployee erbij haalt zie je dat ik de Clone method aanroep van de inkomende BIrthdate, en dat is natuurlijk geen goed idee als de Birthdate nil is.
Los daarvan, levert een unit testing een verzameling test routines die gecompileerd en uitgevoerd kunnen worden, om daarmee de correctheid van de SOAP methods te verifiëren. En het mooie is dat we alleen het test project nog een keer hoeven te compileren en te runnen als de onderliggende code ooit mocht wijzigen. We hoeven de tests dus maar één keer te schrijven, maar kunnen ze tot in de verre toekomst blijven gebruiken.
Bij positief resultaat zien we allemaal groene lichtjes (zie Figuur 10), en bij fouten verschijnen de details van de CheckXXX routine in beeld voor meer informatie.

Unit Test dialoog

En een laatste gevolg van het op deze manier testen van SOAP methods is dat we nu ook zeker(der) zijn van het feit dat de signature van de SOAP methods niet zomaar mag wijzigen. Dat zou namelijk alle client toepassingen “in het wild” om zeep kunnen helpen, doordat die dan via SOAP requests versturen voor methods die niet meer in de oude vorm bestaan (te vergelijken met het wijzigen van de Windows API waardoor toepassingen niet meer draaien op nieuwe versies van Windows). Indien de SOAP methods zijn aangepast is dat meteen te merken als het test project niet meer compileert. En omdat ik één keer daadwerkelijk heb meegemaakt dat een “ontwikkelaar” een aantal SOAP methods grondig had gewijzigd terwijl er al verschillende client toepassingen in gebruik waren die afhankelijk waren van deze methods. Doodzonde nummer één in de Web Service wereld als je het mij vraagt. En Unit Testing helpt mij in ieder geval bij het opsporen ervan.

Conclusie
In dit artikel heb ik laten zien hoe we Win32 Web Services kunnen debuggen, de SOAP berichten kunnen tracen, en de SOAP methods kunnen testen met behulp van Delphi (en het laatste ook met DUnit). Deze technieken zijn met name van belang bij het gebruik van SOAP in heterogene omgevingen, waarbij alleen de server (of de client) in Delphi for Win32 is geschreven, maar er aan de andere kant met andere omgevingen gewerkt wordt.
Wie nog vragen of opmerkingen heeft kan me altijd per e-mail bereiken of een bezoek brengen aan mijn nieuwe trainingsruimte in Helmond Brandevoort (zie www.eBob42.com voor details).

Referenties
Consuming ASP.NET 2.0 Web Services in Delphi for Win32


This webpage © 2006-2008 by webmaster drs. Robert E. Swart (aka - www.drbob42.com). All Rights Reserved.