This post describes how to use Mountebank imposters as Mock Objects/Test Spies via HTTP with F#.
Mountebank is a tool (and currently the only one) which provides multi-protocol, multi-language, on-demand, Test Doubles over the wire, named imposters.
Imposters are normally created during the fixture setup phase and disposed in the fixture teardown phase.
According to the official website, Mountebank currently supports imposters for:
Imposters can act as Mocks, as well as Stubs. For Mocks, the Mountebank website contains a mocking example for the STMP protocol.
The Mountebank server runs at 192.168.1.3 on port 2525 in a Unix-like OS. During the setup phase an imposter is created via HTTP POST specifying the SMTP protocol and port (4547) in the request body:
POST http://192.168.1.3:2525/imposters/ HTTP/1.1
Host: 192.168.1.3:2525
Accept: application/json
Content-Type: application/json
{
"port": 4547,
"protocol": "smtp"
}
In F# this can be written using the FSharp.Data Http module as:
let create protocol host port =
Http.Request(
"http://" + host + ":2525/imposters/",
headers =
["Content-Type", HttpContentTypes.Json;
"Accept" , HttpContentTypes.Json],
httpMethod = "POST",
body = TextRequest
(sprintf @"{ ""port"": %i, ""protocol"": ""%s"" }"
port protocol))
The response is:
HTTP/1.1 201 Created
Location: http://192.168.1.3:2525/imposters/4547
Content-Type: application/json; charset=utf-8
Content-Length: 167
Date: Mon, 21 Apr 2014 20:42:57 GMT
Connection: keep-alive
{
"protocol": "smtp",
"port": 4547,
"requests": [],
"stubs": [],
"_links": {
"self": {
"href": "http://192.168.1.3:2525/imposters/4547"
}
}
}
Now we can send a mail message via SMTP to the Mountebank imposter using port 4547:
From: "Customer Service" <code@nikosbaxevanis.com>
To: "Customer" <nikos.baxevanis@gmail.com>
Subject: Thank you for your order
Hello Customer,
Thank you for your order from company.com. Your order will
be shipped shortly.
Your friendly customer service department.
In F# this can be written as:
let expectedSubject = "Thank you for your order"
(new SmtpClient("192.168.1.3", 4547)).Send(
new MailMessage(
"code@nikosbaxevanis.com",
"nikos.baxevanis@gmail.com",
expectedSubject,
"Hello Customer, Thank you for your order from company.com."))
To get the captured requests from the imposter we can issue a GET
or a DELETE
request. Normally, this happens during the fixture teardown phase via DELETE
:
DELETE http://192.168.1.3:2525/imposters/4547 HTTP/1.1
Content-Type: application/json
Host: 192.168.1.3:2525
The response is:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 810
Date: Mon, 21 Apr 2014 20:41:10 GMT
Connection: keep-alive
{
"protocol": "smtp",
"port": 4547,
"requests": [
{
"requestFrom": "192.168.1.4",
"envelopeFrom": "code@nikosbaxevanis.com",
"envelopeTo": [
"nikos.baxevanis@gmail.com"
],
"from": {
"address": "code@nikosbaxevanis.com",
"name": ""
},
"to": [
{
"address": "nikos.baxevanis@gmail.com",
"name": ""
}
],
"cc": [],
"bcc": [],
"subject": "Thank you for your order!",
"priority": "normal",
"references": [],
"inReplyTo": [],
"text": "Hello Customer, Thank you for your order from company.com.\n",
"html": "",
"attachments": []
}
],
"stubs": [],
"_links": {
"self": {
"href": "http://192.168.1.3:2525/imposters/4547"
}
}
In F# the requests
JSON property can be decomposed and extracted using the FSharp.Data JSON Parser and Http modules as:
let getCapturedRequests (spy : HttpResponse) =
let getRequests jsonValue =
match jsonValue with
| FSharp.Data.Record properties ->
match properties with
| [| protocol; port; requests; stubs; _links; |] ->
match snd requests with
| FSharp.Data.Array elements ->
match elements |> Seq.toList with
| head :: tail -> Some(elements)
| [] -> None
| _ -> None
| _ -> None
| _ -> None
let response =
Http.Request(
spy.Headers.Item "Location",
headers = [
"Content-Type", HttpContentTypes.Json;
"Accept", HttpContentTypes.Json ],
httpMethod = "DELETE")
match response.Body with
| Text json ->
JsonValue.Parse json
|> getRequests
| _ -> None
The signature of GetCapturedRequests
function is:
spy : HttpResponse -> JsonValue [] option
The value of the subject
property can be similarly decomposed and extracted with Pattern Matching:
match GetCapturedRequest imposter "subject" with
| Some actual -> expectedSubject = actual
| None ->
raise <| InvalidOperationException(
"No property named 'subject' found in captured requests.")
With all the above a test using xUnit.net and composed assertions with Unquote could be written as:
let verify = Swensen.Unquote.Assertions.test
[<Fact>]
let sendMailTransmitsCorrectSubject () =
let expectedSubject = "Thank you for your order!"
let mountebankHost = "192.168.1.3"
let imposterPort = 4547
let spy = Imposter.Create "smtp" mountebankHost imposterPort
(new SmtpClient(mountebankHost, imposterPort)).Send(
new MailMessage(
"code@nikosbaxevanis.com",
"nikos.baxevanis@gmail.com",
expectedSubject,
"Hello Customer, Thank you for your order from company.com."))
verify <@
match SmtpSpy.GetCapturedRequest spy "subject" with
| Some actual -> expectedSubject = actual
| None ->
raise <| InvalidOperationException(
"No property named 'subject' found in captured requests.") @>
In this case, it’s only necessary to verify that the SMTP request on the imposter was made only once:
[<Fact>]
let sendMailTransmitsCorrectNumberOfSmtpRequests () =
let expectedNumberOfRequests = 1
let mountebankHost = "192.168.1.3"
let imposterPort = 4546
let spy = Imposter.Create "smtp" mountebankHost imposterPort
(new SmtpClient(mountebankHost, imposterPort)).Send(
new MailMessage(
"code@nikosbaxevanis.com",
"nikos.baxevanis@gmail.com",
"Thank you for your order!",
"Hello Customer, Thank you for your order from company.com."))
verify <@
match Imposter.GetCapturedRequests spy with
| Some actual -> expectedNumberOfRequests = (actual |> Seq.length)
| None ->
raise <| InvalidOperationException(
sprintf "Expected %i calls but received none."
expectedNumberOfRequests) @>
The complete source code is available on this gist - any comments or suggestions are always welcome.
You may also read the next post, Mountebank stubs with F#.