Skip to content

Library for stubbing and setting expectations on HTTP requests in Delphi with DUnitX

License

Notifications You must be signed in to change notification settings

appercept/Delphi-WebMocks

Repository files navigation

Delphi compatibility Platform compatibility License Lines of Code

WebMocks GitHub release (latest by date) GitHub commits since latest release (by SemVer) GitHub Release Date

Library for stubbing and setting expectations on HTTP requests in Delphi with DUnitX.

Requirements

* WebMocks was developed in Delphi 10.3 (Rio) and 10.4 (Sydney) and until version 3.0 was compatible back to XE8. As WebMocks makes use of the System.Net library introduced with XE8 it will not be compatible with earlier versions. Should you require installing on Delphi versions prior to 10.3 you should install version 2.0.0.

Installation: GetIt

WebMocks 3.2.0 is available through Embarcadero's package manager for Delphi GetIt. If you have a recent version of Delphi including GetIt then this should be the preferred installation method.

Installation: Delphinus-Support

WebMocks should now be listed in Delphinus package manager.

Be sure to restart Delphi after installing via Delphinus otherwise the units may not be found in your test projects.

Installation: Manual

  1. Download and extract the latest version 3.2.1.
  2. In "Tools > Options" under the "Language / Delphi / Library" add the extracted Source directory to the "Library path" and "Browsing path".

Getting Started

If you'd like a gentle introduction to WebMocks for Delphi, there is a series of articles published on DEV starting with Testing HTTP clients in Delphi with DUnitX and WebMocks.

Delphi-WebMocks-Demos contains a set of demonstrations to accompany the articles.

Upgrading from versions prior to 2.0.0

Version 2 has dropped the Delphi. namespace from all units. Any projects upgrade to version 2 or later will need to drop the Delphi. prefix from any included WebMocks units.

Setup

In your test unit file a couple of simple steps are required.

  1. Add WebMock to your interface uses.
  2. In your TestFixture class use Setup and TearDown to create/destroy an instance of TWebMock.

Example Unit Test with TWebMock

unit MyTestObjectTests;

interface

uses
  DUnitX.TestFramework,
  MyObjectUnit,
  WebMock;

type
  TMyObjectTests = class(TObject)
  private
    WebMock: TWebMock;
    Subject: TMyObject;
  public
    [Setup]
    procedure Setup;
    [TearDown]
    procedure TearDown;
    [Test]
    procedure TestGet;
  end;

implementation

procedure TMyObjectTests.Setup;
begin
  WebMock := TWebMock.Create;
end;

procedure TMyObjectTests.TearDown;
begin
  WebMock.Free;
end;

procedure TMyObjectTests.TestGet;
begin
  // Arrange
  // Stub the request
  WebMock.StubRequest('GET', '/endpoint');

  // Create your subject and point it at the endpoint
  Subject := TMyObject.Create;
  Subject.EndpointURL := WebMock.URLFor('endpoint');

  // Act
  Subject.Get;

  // Assert: check your subject behaved correctly
  Assert.IsTrue(Subject.ReceivedResponse);
end;

initialization
  TDUnitX.RegisterTestFixture(TMyObjectTests);
end.

By default TWebMock will bind to a port dynamically assigned start at 8080. This behaviour can be overridden by specifying a port at creation.

WebMock := TWebMock.Create(8088);

The use of WebMock.URLFor function within your tests is to simplify constructing a valid URL. The Port property contains the current bound port and BaseURL property contains a valid URL for the server root.

Examples

Stubbing

Request matching by HTTP method and document path

The simplest form of request matching and starting point for all request stubs is by HTTP method and document path. For example stubbing the HTTP verb GET to the server root / is achieved by:

WebMock.StubRequest('GET', '/');

The use of a single wild-card character * can be used to match any request. For example, to match all POST requests regardless of document path you can use:

WebMock.StubRequest('POST', '*');

Similarly, to match any HTTP method for a given path you can use:

WebMock.StubRequest('*', '/path');

It is perfectly possible to have a catch-all of * and * for both HTTP method and document path.

Request matching by header value

HTTP request headers can be matched like:

WebMock.StubRequest('*', '*').WithHeader('Name', 'Value');

Matching multiple headers can be achieved in 2 ways. The first is to simply chain WithHeader calls e.g.:

WebMock.StubRequest('*', '*')
  .WithHeader('Header-1', 'Value-1')
  .WithHeader('Header-2', 'Value-2');

Alternatively, WithHeaders accepts a TStringList of key-value pairs e.g.:

var
  Headers: TStringList;

begin
  Headers := TStringList.Create;
  Headers.Values['Header-1'] := 'Value-1';
  Headers.Values['Header-2'] := 'Value-2';

  WebMock.StubRequest('*', '*').WithHeaders(Headers);
end;

Request matching by header value

HTTP request can be matched by content like:

WebMock.StubRequest('*', '*').WithBody('String content.');

Request matching by form-data

HTTP requests can be matched by form-data as submitted with content-type of application/x-www-form-urlencoded. Multiple matching field values can be combined. For example:

WebMock.StubRequest('*', '*')
  .WithFormData('AField', 'A value.')
  .WithFormData('AOtherField', 'Another value.');

To simply match the presence of a field, a wildcard * can be passed for the value.

NOTE: You cannot match form-data (WithFormData) and body content (WithBody) at the same time. Specifying both will result in the latest call overwriting the previous matcher.

Matching request document path, headers, or content by regular-expression

Matching a request by regular-expression can be useful for stubbing dynamic routes for a ReSTful resource involving a resource name and an unknown resource ID such as /resource/999. Such a request could be stubbed by:

WebMock.StubRequest('GET', TRegEx.Create('^/resource/\d+$'));

Matching headers can similarly by achieved by:

WebMock.StubRequest('*', '*')
  .WithHeader('Accept', TRegEx.Create('video/.+'));

Matching content can be performed like:

WebMock.StubRequest('*', '*')
  .WithBody(TRegEx.Create('Hello'));

Matching form-data content can be performed like:

WebMock.StubRequest('*', '*')
  .WithFormData('AField', TRegEx.Create('.*'));

NOTE: Be sure to add System.RegularExpressions to your uses clause.

Request matching by JSON

HTTP requests can be matched by JSON data as submitted with content-type of application/json using WithJSON. Multiple matching field values can be combined. For example:

WebMock.StubRequest('*', '*')
  .WithJSON('ABoolean', True)
  .WithJSON('AFloat', 0.123)
  .WithJSON('AInteger', 1)
  .WithJSON('AString', 'value');

The first argument can be a path. For example, in the following JSON, the path objects[0].key would match value 1.

{
  "objects": [
    { "key": "value 1" },
    { "key": "value 2" }
  ]
}

NOTE: Strings patterns can be matched by passing a regular expression as the second argument. For example:

WebMock.StubRequest('*', '*')
  .WithJSON('objects[0].key', TRegEx.Create('value\s\d+'));

Request matching by XML

HTTP request can be matched by XML data values submitted. For example:

WebMock.StubRequest('*', '*')
  .WithXML('/Object/Attr1', 'Value 1');

The first argument is an XPath expression. The previous example would make a positive match against the following document:

<?xml version="1.0" encoding="UTF-8"?>
<Object>
  <Attr1>Value 1</Attr1>
</Object>

The second argument can be a boolean, floating point, integer, or string value.

Request matching by predicate function

If matching logic is required to be more complex than the simple matching, a predicate function can be provided in the test to allow custom inspection/logic for matching a request. The anonymous predicate function will receive an IWebMockHTTPRequest object for inspecting the request. If the predicate function returns True then the stub will be regarded as a match, if returning False it will not be matched.

Example stub with predicate function:

WebMock.StubRequest(
  function(ARequest: IWebMockHTTPRequest): Boolean
  begin
    Result := True; // Return False to ignore request.
  end
);

Stubbed Response Codes

By default a response status will be 200 OK for a stubbed request. If a request is made to TWebMock without a registered stub it will respond 501 Not Implemented. To specify the response status use ToRespond.

WebMock.StubRequest('GET', '/').ToRespond(TWebMockResponseStatus.NotFound);

Stubbed Response Headers

Headers can be added to a response stub like:

WebMock.StubRequest('*', '*')
  .ToRespond.WithHeader('Header-1', 'Value-1');

As with request header matching multiple headers can be specified either through method chaining or by using the WithHeaders method.

  WebMock.StubRequest('*', '*').ToRespond
    .WithHeader('Header-1', 'Value-1')
    .WithHeader('Header-2', 'Value-2');

/* or */

var
  Headers: TStringList;
begin
  Headers := TStringList.Create;
  Headers.Values['Header-1'] := 'Value-1';
  Headers.Values['Header-2'] := 'Value-2';

  WebMock.StubRequest('*', '*')
    .ToRespond.WithHeaders(Headers);
end;

Stubbed Response Content: String Values

By default a stubbed response returns a zero length body with content-type text/plain. Simple response content that is easily represented as a string can be set with WithBody.

WebMock.StubRequest('GET', '/')
  .ToRespond.WithBody('Text To Return');

If you want to return a specific content-type it can be specified as the second argument e.g.

WebMock.StubRequest('GET', '/')
  .ToRespond.WithBody('{ "status": "ok" }', 'application/json');

Stubbed Response Content: Fixture Files

When stubbing responses with binary or large content it is likely easier to provide the content as a file. This can be achieved using WithBodyFile which has the same signature as WithBody but the first argument is the path to a file.

WebMock.StubRequest('GET', '/').WithBodyFile('image.jpg');

The Delphi-WebMocks will attempt to set the content-type according to the file extension. If the file type is unknown then the content-type will default to application/octet-stream. The content-type can be overridden with the second argument. e.g.

WebMock.StubRequest('GET', '/').WithBodyFile('file.myext', 'application/xml');

NOTE: One "gotcha" accessing files in tests is the location of the file will be relative to the test executable which, by default, using the Windows 32-bit compiler will be output to the Win32\Debug folder. To correctly reference a file named Content.txt in the project folder, the path will be ..\..\Content.txt.

Dynamic Responses

Sometimes it is useful to dynamically respond to a request. For example:

WebMock.StubRequest('*', '*')
  .ToRespondWith(
    procedure (const ARequest: IWebMockHTTPRequest;
               const AResponse: IWebMockResponseBuilder)
    begin
      AReponse
        .WithStatus(202)
        .WithHeader('header-1', 'a-value')
        .WithBody('Some content...');
    end
  );

This enables testing of features that require deeper inspection of the request or to reflect values from the request back in the response. For example:

WebMock.StubRequest('GET', '/echo_header')
  .ToRespondWith(
    procedure (const ARequest: IWebMockHTTPRequest;
               const AResponse: IWebMockHTTPResponseBuilder)
    begin
      AResponse.WithHeader('my-header', ARequest.Headers.Values['my-header']);
    end
  );

It can also be useful for simulating failures for a number of attempts before returning a success. For example:

var LRequestCount := 0;
WebMock.StubRequest('GET', '/busy_endpoint')
  .ToRespondWith(
    procedure (const ARequest: IWebMockHTTPRequest;
               const AResponse: IWebMockHTTPResponseBuilder)
    begin
      Inc(LRequestCount);
      if LRequestCount < 3 then
        AResponse.WithStatus(408, 'Request Timeout')
      else
        AResponse.WithStatus(200, 'OK');
    end
  );

Resetting Registered Stubs

If you need to clear the current registered stubs you can call ResetStubRegistry or Reset on the instance of TWebMock. The general Reset method will return the TWebMock instance to a blank state including emptying the stub registry. The more specific ResetStubRegistry will as suggested clear only the stub registry.

Request History

Each and every request made of the TWebMock instance is recorded in the History property. History entries contain all the key web request information: Method; RequestURI; Headers; and Body.

It is possible to write assertions based upon the request history e.g.:

WebClient.Get(WebMock.URLFor('document'));

Assert.AreEqual('GET', WebMock.History.Last.Method);
Assert.AreEqual('/document', WebMock.History.Last.RequestURI);

NOTE: Should you find yourself writing assertions in this manor you should take a look at Request Assertions which provides a more concise way of defining these assertions.

Resetting Request History

If you need to clear request history you can call ResetHistory or Reset on the instance of TWebMock. The general Reset method will return the TWebMock instance to a blank state including emptying the history. The more specific ResetHistory will as suggested clear only the history.

Request Assertions

In addition to using DUnitX assertions to validate your code behaved as expected you can also use request assertions to check whether requests you expect your code to perform where executed as expected.

A simple request assertion:

WebClient.Get(WebMock.URLFor('/'));

WebMock.Assert.Get('/').WasRequested; // Passes

As with request stubbing you can match requests by HTTP Method, URI, Query Parameters, Headers, and Body content (including WithJSON and WithXML).

WebMock.Assert
  .Patch('/resource`)
  .WithQueryParam('ParamName', 'Value')
  .WithHeader('Content-Type', 'application/json')
  .WithBody('{ "resource": { "propertyA": "Value" } }')
  .WasRequested;

Negative Assertions

Anything that can be asserted positively (WasRequested) can also be asserted negatively with WasNotRequested. This is useful to check your code is not performing extra unwanted requests.

Development Dependencies (Optional)

  • TestInsight is required to run the Delphi-WebMocks test suite, so, if you're considering contributing and need to run the test suite, install it. If you do TDD in Delphi I would recommend installing and using it in your own projects.

Semantic Versioning

This project follows Semantic Versioning.

License

Copyright ©2019-2024 Richard Hatherall richard@appercept.com

WebMocks is distributed under the terms of the Apache License (Version 2.0).

See LICENSE for details.