-
Notifications
You must be signed in to change notification settings - Fork 13
Fluent API
- Structure
- Configuring Request Matchers
- Configuring Response Builders
- Logging of the Request Matching Process
- Exception Handling
- Known Limitations
⚠️
The Fluent API allows you to configure responses for requests that are sent to the mock HTTP server, using an intuitive and readable API. The fluent API should be used to configure responses unless you need to match requests or create responses in a way that is not supported by the fluent API. In this case, you should use the delegate function to create the responses.
A good way to understand how to use the fluent API is to look at the tests for the fluent-workflow
workflow, these are located in the LogicAppUnit.Samples.LogicApps.Tests/FluentWorkflowTest
folder in the code repo.
The fluent API allows you to configure a Request Matcher and a corresponding Response Builder. A Request Matcher is used to evaluate a HTTP request against a set of criteria and if the criteria match, the Response Builder is used to create the HTTP response that is returned to the workflow being tested.
There is a one-to-one relationship between a Request Matcher and a Response Builder and any number of Request Matchers and Response Builders can be configured.
Request Matchers can be configured in two places:
- In a test case using the
ITestRunner.AddMockResponse()
method. - In a test class Initialization method using the
WorkflowTestBase.AddMockResponse()
method.
This is useful when you have many test cases in a test class and the you want to configure the same Request Matcher for all tests in the class. In this case, just define the Request Matcher once, in the test class Initialization method.
Request Matchers that are configured in a test case have a higher priority than those configured in the test class. All Fluent Request Matchers have a higher priority than a delegate function configured in the test case.
A Response Builder is configured for a Request Matcher using the IMockResponse.RespondWith()
method. This is an example of a simple Request Matcher and Response Builder:
testRunner
.AddMockResponse("PutMethod",
MockRequestMatcher.Create()
.UsingPut())
.RespondWith(
MockResponseBuilder.Create()
.WithUnauthorized());
This example will match against any request using a PUT
method and then return a response with a status code of HTTP 401 (Unauthorized). The first parameter of the ITestRunner.AddMockResponse()
method is optional and is the name of the request matcher. Request Matcher names must be unique within an instance of a test runner. The name is used when logging of the request matching process is enabled.
This is another example that configures two Request Matchers for a test:
testRunner
.AddMockResponse("Path-EndsWithMatched",
MockRequestMatcher.Create()
.UsingPost()
.WithPath(PathMatchType.EndsWith, "/api/v1/service"))
.RespondWith(
MockResponseBuilder.Create()
.WithSuccess());
testRunner
.AddMockResponse("Default-Error",
MockRequestMatcher.Create()) // Matches all requests
.RespondWith(
MockResponseBuilder.Create()
.WithInternalServerError());
Each Request Matcher is evaluated in sequence, starting with the first one that is defined. When a request is matched, the subsequent matchers are ignored. In this example, any request using a POST
method with an absolute path ending in /api/v1/service
will return a HTTP 200 (OK) response. All other requests will be matched by the Default-Error
matcher (because it does not specify any match criteria) and a HTTP 500 (Internal Server Error) response will be returned.
Request Matchers can be configured to evaluate a request using a range of criteria types:
- HTTP method
- Workflow action name
- Absolute path
- Headers
- Query parameters
- Content Type
- Content
- Match count
When multiple criteria types are defined, the testing framework uses AND logic to combine the criteria types, i.e. the request must match all of the criteria types. Criteria can be configured in a Request Matcher any order, and the order does not dictate the order of evaluation at run-time.
Some of the criteria types are described below, followed by a table that summarises all of the criteria that can be configured for a Request Matcher.
Request Matchers can be configured to check the name of the workflow action that sent the request using the FromAction(params string[] actionNames)
method.
This method uses the x-ms-workflow-operation-name
HTTP header in the request to identify the action name. The header is added by the workflow runtime to all HTTP actions, and actions using a managed API connection, unless the Suppress workflow headers setting has been enabled for the action. If your workflow contains actions where the Suppress workflow headers setting is enabled, do not use the FromAction
matcher, otherwise you will see this message in the test execution log:
The action name header 'x-ms-workflow-operation-name' does not exist in the request so matching has failed - the header is only generated by the Logic Apps runtime when the 'Suppress workflow headers' setting for the action is off"
When the testing framework replaces actions using a built-in connector with a HTTP action, the Suppress workflow headers setting is not enabled.
This example configures a Request Matcher to check for POST requests sent from a workflow action called Call_Service_One
or Call_Service_One_Again
:
testRunner
.AddMockResponse(
MockRequestMatcher.Create()
.UsingPost()
.FromAction("Call_Service_One", "Call_Service_One_Again"))
.RespondWithDefault();
Request Matchers can be configured to check for HTTP headers using these methods:
WithHeader(string name, string value)
WithHeader(string name)
The first method checks that the header exists and has a specific value; the second method just checks that the header exists. This example configures a Request Matcher to check for requests with a UserAgent
header with a value of LogicAppWorkflow
, an Expect
header with a value of application/json
and an Accept
header that can have any value:
testRunner
.AddMockResponse( // No request matcher name
MockRequestMatcher.Create()
.WithHeader("UserAgent", "LogicAppWorkflow")
.WithHeader("Expect", "application/json")
.WithHeader("Accept")
.WithHeader("MyCustomHeader", "HeaderValueThatIsNotMatched"))
.RespondWith(
MockResponseBuilder.Create()
.WithSuccess());
When multiple WithHeader()
methods are used, the testing framework uses AND logic to combine the criteria, i.e. the request must match all of the header criteria.
Request Matchers can be configured to check JSON and string request content using these methods:
WithContentAsJson(Func<JToken, bool> requestContentMatch)
WithContentAsString(Func<string, bool> requestContentMatch)
This example configures a Request Matcher to check for JSON content where the manufacturer
field is set to Aston Martin
:
testRunner
.AddMockResponse("Content-AstonMartin",
MockRequestMatcher.Create()
.WithContentAsJson((requestContent) => { return (string)requestContent.SelectToken("manufacturer") == "Aston Martin"; }))
.RespondWith(
MockResponseBuilder.Create()
.WithSuccess());
The methods use delegate functions to validate the request content so you can implement any logic that returns a boolean value.
It doesn't make sense to configure a Request Matcher using both WithContentAsJson()
and WithContentAsString()
, but if you did, the request would need to match both for a successful match.
Request Matchers can be configured to match on a specific match count number using these methods:
WithMatchCount(params int[] matchCounts)
WithNotMatchCount(params int[] matchCounts)
This is useful if for example you want the first two matches to return HTTP 200 (OK), the third and fifth matches to return HTTP 401 (Unauthorized) and all other matches to return HTTP 500 (Internal Server Error):
testRunner
.AddMockResponse("Match-One-Two",
MockRequestMatcher.Create()
.WithMatchCount(1, 2))
.RespondWith(
MockResponseBuilder.Create()
.WithSuccess());
testRunner
.AddMockResponse("Match-Three-Five",
MockRequestMatcher.Create()
.WithMatchCount(3, 5))
.RespondWith(
MockResponseBuilder.Create()
.WithUnauthorized());
testRunner
.AddMockResponse("Default-Error",
MockRequestMatcher.Create()) // Matches all requests
.RespondWith(
MockResponseBuilder.Create()
.WithInternalServerError());
Remember that Request Matchers are evaluated in order, so the Default-Error
matcher is matched last, when all previous matchers have not been matched.
The match count for a Request Matcher is incremented when all criteria are matched, but before the WithMatchCount()
and WithNotMatchCount()
methods are evaluated.
This table summarises all of the criteria that can be configured for a Request Matcher.
Criteria type | Method name | Matches when HTTP request... |
---|---|---|
Method | UsingGet() |
Uses the HTTP GET method. |
Method | UsingPost() |
Uses the HTTP POST method. |
Method | UsingPut() |
Uses the HTTP PUT method. |
Method | UsingPatch() |
Uses the HTTP PATCH method. |
Method | UsingDelete() |
Uses the HTTP DELETE method. |
Method | UsingMethod(params HttpMethod[] methods) |
Uses one of the HTTP methods defined in methods . |
Method | UsingAnyMethod() |
Uses any HTTP method. Matching any method is the default behaviour for a Request Matcher. This method is only used to provide clarity of intent. |
Action | FromAction(params string[] actionNames) |
Was sent from a workflow action whose name matches one of the names defined in actionNames . |
Absolute Path | WithPath(PathMatchType matchType, params string[] paths) |
Has a path that matches one of the paths defined in paths . The matchType parameter defines the type of match, either Exact , Contains or EndsWith . |
Header | WithHeader(string name) |
Includes a header matching name . The value of the header is not checked. |
Header | WithHeader(string name, string value) |
Includes a header matching name and with a value matching value . |
Query Parameter | WithQueryParam(string name) |
Has a query parameter matching name . The value of the parameter is not checked. |
Query Parameter | WithQueryParam(string name, string value) |
Has a query parameter matching name and with a value matching value . |
Content Type | WithContentType(params string[] contentTypes) |
Includes a Content-Type header that matches one of the types defined in contentTypes . |
Content | WithContentAsString(Func<string, bool> requestContentMatch) |
Has string content that matches the logic implemented in the requestContentMatch delegate function. The delegate function must return a boolean value. |
Content | WithContentAsJson(Func<string, bool> requestContentMatch) |
Has JSON content that matches the logic implemented in the requestContentMatch delegate function. The delegate function must return a boolean value. |
Match Count | WithMatchCount(params int[] matchCounts) |
Is matched the number of times as defined in the matchCounts parameter. |
Match Count | WithNotMatchCount(params int[] matchCounts) |
Is not matched the number of times as defined in the matchCounts parameter. This is the logical opposite of WithMatchCount(params int[] matchCounts) . |
Criteria of different types are combined using AND logic. If multiple criteria of the same type are configured, they are combined as follows:
Criteria Type | Evaluation Logic | Description |
---|---|---|
Method | OR | The request must match one of the method criteria. |
Action | OR | The request must match one of the workflow action name criteria. |
Absolute Path | OR | The request must match one of the path criteria. |
Header | AND | The request must match all of the header criteria. |
Query Parameter | AND | The request must match all of the query parameter criteria. |
Content Type | OR | The request must match one of the content type criteria. |
Content | AND | The request must match all of the content criteria. |
Match Count | OR | The request must match one of the match count criteria. |
A HTTP request can only have one method, one absolute path and one content type, so AND logic wouldn't be appropriate if multiple criteria of the same type were configured.
Consider this example:
testRunner
.AddMockResponse(
MockRequestMatcher.Create()
.UsingPost()
.UsingMethod(HttpMethod.Patch, HttpMethod.Put))
.WithHeader("UserAgent")
.WithHeader("Expect", "application/json")
.WithContentType("application/xml", "plain/xml")
.WithContentType("atom/xml")
A request will only be matched when:
(
HTTP Method is Post
OR
HTTP Method is Patch
OR
HTTP Method is Put
)
AND
(
Header `User-Agent` exists
AND
Header `Expect` has a value of `application/json`
)
AND
(
Content Type is `application/xml`
OR
Content Type is `plain/xml`
OR
Content Type is `atom/xml`
)
A Response Builder creates a HTTP response for a request that has been matched by the related Request Matcher. The response is created using the IMockResponse.RespondWith()
method, followed by methods that configure the response as required. Methods to configure the response can be used in any order.
This example will return a response with a HTTP 200 (OK) status code and plain text content Hello workflow
:
testRunner
.AddMockResponse(
MockRequestMatcher.Create())
.RespondWith(
MockResponseBuilder.Create()
.WithSuccess()
.WithContentAsPlainText("Hello workflow"));
If a HTTP 200 (OK) response is required with no content and no headers, the IMockResponse.RespondWithDefault()
method can be used as a shortcut:
testRunner
.AddMockResponse(
MockRequestMatcher.Create())
.RespondWithDefault();
Some of the response configurations are described below, followed by a table that summarises all of the configurations for a Response Builder.
Use the AfterDelay()
methods when you want the mock HTTP server to implement a delay before sending the response back to the workflow being tested. There are methods that implement either a fixed delay duration or a random delay duration between a minimum and maximum period.
This example configures a response that is delayed by a random number of seconds between 1 and 5 seconds:
testRunner
.AddMockResponse(
MockRequestMatcher.Create()
.UsingPost()
.WithPath(PathMatchType.Exact, "/api/v1/UpdateCustomer"))
.RespondWith(
MockResponseBuilder.Create()
.WithSuccess()
.AfterDelay(1, 5));
The delay feature is useful when you want to test a workflow for a scenario where a call to an external service takes longer than expected and the workflow times out.
Use the ThrowsException(Exception exceptionToThrow)
method to throw an exception in the mock response builder. This could be used to simulate an exception in a local .NET Framework function that is called by a workflow, using the Call a local function action.
This example configures a response to throw an InvalidOperationException
exception:
testRunner
.AddMockResponse(
MockRequestMatcher.Create()
.UsingPost()
.WithPath(PathMatchType.Exact, "/api/v1/UpdateCustomer"))
.RespondWith(
MockResponseBuilder.Create()
.ThrowsException(new InvalidOperationException("The customer could not be updated")));
The text execution logs will show that the exception was thrown in the response builder:
Checking mock request matcher #1:
Matched
EXCEPTION: The customer could not be updated
The Call a local function workflow action does not record the exception type or the exception message that was configured in the thrown exception. The action only records that the action failed, via the action status.
Use the WithContent()
, WithContentAsJson()
and WithContentAsPlainText
methods to configure the content for the response. Configuration of a response using these methods is optional because a response does not need content.
The WithContentAsJson()
methods are used when the response contains JSON content. The content can be configured using a string value, an instance of a Stream, an embedded resource in an assembly, or an object that can be serialised into JSON. The content type is automatically set to application/json
. This example uses a dynamic object that is serialised into JSON:
testRunner
.AddMockResponse(
MockRequestMatcher.Create())
.RespondWith(
MockResponseBuilder.Create()
.WithSuccess()
.WithContentAsJson(new { name = "Falcon 9", manufacturer = "SpaceX", diameter = 3.7, height = 70, massToLeo = 22.8 }));
The WithContentAsPlainText()
methods are used when the response contains plain text content. The content can be configured using a string value, an instance of a Stream or an embedded resource in an assembly. The content type is automatically set to text/plain
. This example uses content from an embedded resource in the current executing assembly (i.e. the assembly containing the test class):
testRunner
.AddMockResponse(
MockRequestMatcher.Create())
.RespondWith(
MockResponseBuilder.Create()
.WithSuccess()
.WithContentAsPlainText(
$"{System.Type.GetType().Namespace}.MyResponseContent.txt",
System.Reflection.Assembly.GetExecutingAssembly()));
The WithContent()
methods allow you to specify a content type, so use these when configuring content that is not JSON or plain text. The content can be configured using a string value, an instance of a Stream or an embedded resource in an assembly.
The WithContent(Func<HttpContent> content)
method allows you to configure a delegate function to create the content as an instance of HttpContent. Use this method when you need to create content that can't be created using the WithContent()
, WithContentAsJson()
and WithContentAsPlainText
methods. This example configures FormUrlEncodedContent content:
testRunner
.AddMockResponse(
MockRequestMatcher.Create()
.UsingPost()
.WithPath(PathMatchType.Exact, "/api/v1/UpdateOrder"))
.RespondWith(
MockResponseBuilder.Create()
.WithSuccess()
.WithContent(() => new FormUrlEncodedContent(listOfKeyValuePairs))
This table summarises all of the configurations for a Response Builder.
Builder type | Method name | Sets the HTTP response... |
---|---|---|
Status code | WithSuccess() |
Status code to 200 (OK). |
Status code | WithAccepted() |
Status code to 202 (Accepted). |
Status code | WithNoContent() |
Status code to 204 (No Content). |
Status code | WithUnauthorized() |
Status code to 401 (Unauthorized). |
Status code | WithNotFound() |
Status code to 404 (Not Found). |
Status code | WithInternalServerError() |
Status code to 500 (Internal Server Error). |
Status code | WithStatusCode(HttpStatusCode statusCode) |
Status code to statusCode . |
Header | WithHeader(string name, string value) |
Header called name to the value of value . |
Delay | AfterDelay(int secondsDelay) |
After a delay (in seconds) of secondsDelay . |
Delay | AfterDelay(TimeSpan delay) |
After a delay of delay . |
Delay | AfterDelay(int secondsMin, int secondsMax) |
After a random delay (in seconds) between secondsMin and secondsMax . |
Delay | AfterDelay(TimeSpan min, TimeSpan max) |
After a random delay (in milliseconds) between min and max . |
Exception | ThrowsException(Exception exceptionToThrow) |
Status code to 500 (Internal Server Error) as a result of an exception being thrown in the response builder. |
Content | WithContent(Func<HttpContent> content) |
Content based on the HttpContent output of the delegate function content . |
Content | WithContentAsJson(Stream jsonStream) |
Content from the stream jsonStream with a content type of application/json . |
Content | WithContentAsJson(string jsonString) |
Content from the string jsonString with a content type of application/json . |
Content | WithContentAsJson(object body) |
Content serialised from the object body with a content type of application/json . |
Content | WithContentAsJson(string resourceName, Assembly containingAssembly) |
Content from the embedded resource called resourceName in assembly containingAssembly , with a content type of application/json . |
Content | WithContentAsPlainText(string value) |
Content from the string value with a content type of text/plain . |
Content | WithContentAsPlainText(Stream stream) |
Content from the stream stream with a content type of text/plain . |
Content | WithContentAsPlainText(string resourceName, Assembly containingAssembly) |
Content from the embedded resource called resourceName in assembly containingAssembly , with a content type of text/plain . |
Content | WithContent(string value, string contentType, Encoding encoding) |
Content from the string value with a content type of contentType and encoding of encoding . |
Content | WithContent(Stream stream, string contentType) |
Content from the stream stream with a content type of contentType . |
Content | WithContent(string resourceName, Assembly containingAssembly, string contentType) |
Content from the embedded resource called resourceName in assembly containingAssembly , with a content type of contentType . |
The testing framework supports logging of the evaluation of Request Matchers and Response Builders. This makes it easier to debug tests, for example to understand why the expected response is not being created for a request.
The request matching logs are disabled by default and can be enabled by adding the logging.writeMockRequestMatchingLogs
option to the testConfiguration.json
file with a value of true
:
"logging": {
"writeMockRequestMatchingLogs": true
}
The setting is optional. If it is not included, the default value is false
.
When logs are enabled, the test log will include something like this:
Mocked requests:
10:55:19.564: GET http://server:7075/api/v1/customers/54617
Mocked request matching logs:
Checking 2 mock request matchers:
Checking mock request matcher #1:
Matched
10:55:19.862: PUT http://server:7075/api/v1.1/membership/customers/54617
Mocked request matching logs:
Checking 2 mock request matchers:
Checking mock request matcher #1:
Not matched - The request method 'PUT' is not matched with GET
Checking mock request matcher #2 (Update Customer):
Matched
Delay for 3000 milliseconds
In this example, the first request is matched by the first Request Matcher and the second request is matched by the second Request Matcher. A few other things to note:
-
The second Request Matcher includes a
WithDelay(1, 5)
configuration and the actual delay duration is recorded in the log (Delay for 3000 milliseconds
). -
The second Request Matcher has a name (
Update Customer
), whereas the first Matcher does not. The name is included in the log when the Request Matcher is created using theAddMockResponse(string name, IMockRequestMatcher mockRequestMatcher)
method - the name is set using the first parameter. Giving a name to each Request Matcher makes it easier to understand the logs. -
If a request is not matched with a Request Matcher, the reason is recorded in the log. In the previous example, the reason indicates that the request method does not match (
The request method 'PUT' is not matched with GET
). All of the Request Matcher methods will write the reason for a match failure to the log.
If the evaluation of a request by a Request Matcher fails, or the execution of a Response Builder fails, the exception will be recorded in the log when logging of the request matching process is enabled.
This example shows the log when the WithContentAsJson(string resourceName, Assembly containingAssembly)
method has not been configured correctly:
Mocked request matching logs:
Checking 1 mock request matchers:
Checking mock request matcher #1:
Matched
EXCEPTION: The resource 'MyTests.ThisResourceDoesNotExist.json' could not be found in assembly 'MyTests'.
When an exception occurs, a HTTP 500 (Internal Server Error) is returned to the workflow being tested.
If you configure a Request Matcher using WithContentAsJson()
and also configure a delegate function that reads the request content as JSON (using request.Content.ReadAsAsync<JToken>().Result
), the delegate function will fail with an Object reference not set to an instance of an object
error.
Enable logging to see this error in the delegate function:
Mocked request matching logs:
Checking 2 mock request matchers:
Checking mock request matcher #1:
Not matched - The JSON request content is not matched
Checking mock request matcher #2:
Not matched - The JSON request content is not matched
Running mock response delegate because no requests were matched
EXCEPTION: Object reference not set to an instance of an object.
This error occurs because the HTTP request content can only be read once. Multiple calls to WithContentAsJson()
in a test share a cached copy of the JSON content, but the delegate function does not use this shared copy.
- Home
- Using the Testing Framework
- Test Configuration
- Azurite
- Local Settings File
- Test Execution Logs
- Stateless Workflows
- Handling Workflow Dependencies
- Fluent API
- Automated testing using a DevOps pipeline
- Summary of Test Configuration Options
-
Example Mock Requests and Responses
- Call a Local Function action
- Invoke Workflow action
- Built-In Connectors:
- Service Bus
- SMTP
- Storage Account
- SQL Server