Library for stubbing and setting expectations on HTTP requests in Delphi with DUnitX.
* 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.
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.
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.
- Download and extract the latest version 3.2.1.
- In "Tools > Options" under the "Language / Delphi / Library" add the
extracted
Source
directory to the "Library path" and "Browsing path".
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.
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.
In your test unit file a couple of simple steps are required.
- Add
WebMock
to your interfaceuses
. - In your
TestFixture
class useSetup
andTearDown
to create/destroy an instance ofTWebMock
.
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.
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.
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;
HTTP request can be matched by content like:
WebMock.StubRequest('*', '*').WithBody('String content.');
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 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.
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+'));
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.
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
);
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);
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;
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');
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
.
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
);
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.
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.
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.
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;
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.
- 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.
This project follows Semantic Versioning.
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.