| name | horse-integration-tests |
| description | Guide for writing automated integration tests for Horse endpoints using DUnit/DUnitX and THTTPClient. |
Horse Integration Testing
Automated integration tests ensure your API routes, request parsing, and error-handling pipelines behave correctly under simulated HTTP traffic.
1. The Dynamic Port Pattern
When running tests locally or in CI/CD pipelines (like GitHub Actions), static ports (e.g., 9000) might be occupied. To avoid port conflicts:
- Generate a dynamic port for each test run.
- Start Horse on that port inside the setup method.
- Perform HTTP requests against
http://localhost:<dynamic_port>.
- Terminate Horse inside the teardown method.
2. Integration Test Example (DUnitX)
Here is a complete integration test suite demonstrating how to bootstrap Horse, perform requests with Delphi's native THTTPClient, and assert responses:
unit Test.UserAPI;
interface
uses
DUnitX.TestFramework,
System.Net.HttpClient,
System.SysUtils;
type
[TestFixture]
TTestUserAPI = class
private
FPort: Integer;
FClient: THTTPClient;
function GetBaseURL: string;
public
[SetupFixture]
procedure SetupFixture; // Start Horse Server
[TearDownFixture]
procedure TearDownFixture; // Terminate Horse Server
[Setup]
procedure Setup;
[TearDown]
procedure TearDown;
[Test]
procedure TestGetPingReturnsPong;
[Test]
procedure TestGetInvalidUserReturns404;
end;
implementation
uses
Horse,
System.JSON,
System.Net.URLClient;
function TTestUserAPI.GetBaseURL: string;
begin
Result := 'http://localhost:' + FPort.ToString;
end;
procedure TTestUserAPI.SetupFixture;
begin
// Choose a random dynamic port
Randomize;
FPort := 10000 + Random(50000);
// Register endpoints to test
THorse.Get('/ping',
procedure(Req: THorseRequest; Res: THorseResponse; Next: TProc)
begin
Res.Send('pong');
end);
THorse.Get('/users/:id',
procedure(Req: THorseRequest; Res: THorseResponse; Next: TProc)
var
LId: Integer;
begin
LId := Req.Params.Field('id').AsInteger;
if LId = 999 then
Res.Status(THTTPStatus.NotFound).Send('User not found')
else
Res.Send('User found');
end);
// Start Horse asynchronously (non-blocking)
THorse.Listen(FPort);
end;
procedure TTestUserAPI.TearDownFixture;
begin
// Terminate the process / stop Horse provider
THorse.Terminate;
end;
procedure TTestUserAPI.Setup;
begin
FClient := THTTPClient.Create;
end;
procedure TTestUserAPI.TearDown;
begin
FClient.Free;
end;
procedure TTestUserAPI.TestGetPingReturnsPong;
var
LResponse: IHTTPResponse;
begin
LResponse := FClient.Get(GetBaseURL + '/ping');
Assert.AreEqual(200, LResponse.StatusCode);
Assert.AreEqual('pong', LResponse.ContentAsString);
end;
procedure TTestUserAPI.TestGetInvalidUserReturns404;
var
LResponse: IHTTPResponse;
begin
LResponse := FClient.Get(GetBaseURL + '/users/999');
Assert.AreEqual(404, LResponse.StatusCode);
Assert.AreEqual('User not found', LResponse.ContentAsString);
end;
initialization
TDUnitX.RegisterTestFixture(TTestUserAPI);
end.
3. Best Practices for Integration Testing
- Isolate Database State: Before running tests, mock database tables or use transactional rollbacks to ensure tests start with clean, predictable data.
- Separate Test Logic: Keep your route handlers in dedicated
Controller units. Do not define routes inline in your test project setup unless you are testing general configuration middleware behavior.
- Always call
THorse.Terminate: Failing to call Terminate in your tear down fixture will leave the TCP socket open, preventing subsequent test executions from binding to the same port.