UnitTestEx provides .NET testing extensions to the most popular testing frameworks: MSTest, NUnit and Xunit.
The scenarios that UnitTestEx looks to address is the end-to-end unit-style testing of the following whereby the capabilities look to adhere to the AAA pattern of unit testing; Arrange, Act and Assert.
- API Controller
- HTTP-triggered Azure Function
- Service Bus-trigger Azure Function
- Generic Azure Function Type
- HTTP Client mocking
The build and packaging status is as follows.
CI | UnitTestEx |
UnitTestEx.MSTest |
UnitTestEx.NUnit |
UnitTestEx.Xunit |
---|---|---|---|---|
The included change log details all key changes per published version.
Leverages the WebApplicationFactory
(WAF) as a means to host a test server in process to invoke APIs directly using HTTP requests. This has the benefit of validating the HTTP pipeline and all Dependency Injection (DI) configuration within. External system interactions can be mocked accordingly.
UnitTestEx encapsulates the WebApplicationFactory
providing a simple means to arrange the input, execute (act), and assert the response. The following is an example.
using var test = ApiTester.Create<Startup>();
test.ReplaceHttpClientFactory(mcf)
.Controller<ProductController>()
.Run(c => c.Get("abc"))
.AssertOK()
.Assert(new { id = "Abc", description = "A blue carrot" });
Unfortunately, at time of writing, there is no WebApplicationFactory
equivalent for Azure functions. UnitTestEx looks to emulate by self-hosting the function, managing Dependency Injection (DI) configuration, and invocation of the specified method. UnitTestEx when invoking verifies usage of HttpTriggerAttribute
and ensures a Task<IActionResult>
result.
The following is an example.
using var test = FunctionTester.Create<Startup>();
test.ReplaceHttpClientFactory(mcf)
.HttpTrigger<ProductFunction>()
.Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "person/abc", null), "abc", test.Logger))
.AssertOK()
.Assert(new { id = "Abc", description = "A blue carrot" });
Both the Isolated worker model and In-process model are supported.
As above, there is currently no easy means to integration (in-process) test Azure functions that rely on the Azure Service Bus. UnitTestEx looks to emulate by self-hosting the function, managing Dependency Injection (DI) configuration, and invocation of the specified method and verifies usage of the ServiceBusTriggerAttribute
.
The following is an example of invoking the function method directly passing in a ServiceBusReceivedMessage
created using test.CreateServiceBusMessageFromValue
(this creates a message as if coming from Azure Service Bus).
using var test = FunctionTester.Create<Startup>();
test.ReplaceHttpClientFactory(mcf)
.ServiceBusTrigger<ServiceBusFunction>()
.Run(f => f.Run2(test.CreateServiceBusMessageFromValue(new Person { FirstName = "Bob", LastName = "Smith" }), test.Logger))
.AssertSuccess();
Both the Isolated worker model and In-process model are supported.
To support testing of any generic Type
within an Azure Fuction, UnitTestEx looks to simulate by self-hosting the function, managing Dependency Injection (DI) configuration, and invocation of the specified method.
The following is an example.
using var test = FunctionTester.Create<Startup>();
test.ReplaceHttpClientFactory(mcf)
.Type<ServiceBusFunction>()
.Run(f => f.Run2(test.CreateServiceBusMessageFromValue(new Person { FirstName = "Bob", LastName = "Smith" }), test.Logger))
.AssertSuccess();
To test a component that relies on Dependency Injection (DI) directly without the runtime expense of instantiating the underlying host (e.g. ASP.NET Core) the GenericTester
enables any Type
to be tested.
using var test = GenericTester.Create().ConfigureServices(services => services.AddSingleton<Gin>());
test.Run<Gin, int>(gin => gin.Pour())
.AssertSuccess()
.AssertValue(1);
Where invoking a down-stream system using an HttpClient
within a unit test context this should generally be mocked. To enable UnitTestEx provides a MockHttpClientFactory
to manage each HttpClient
(one or more), and mock a response based on the configured request. This leverages the Moq framework internally to enable. One or more requests can also be configured per HttpClient
.
The following is an example.
var mcf = MockHttpClientFactory.Create();
mcf.CreateClient("XXX", new Uri("https://somesys"))
.Request(HttpMethod.Get, "products/abc").Respond.WithJson(new { id = "Abc", description = "A blue carrot" });
using var test = ApiTester.Create<Startup>();
test.ReplaceHttpClientFactory(mcf)
.Controller<ProductController>()
.Run(c => c.Get("abc"))
.AssertOK()
.Assert(new { id = "Abc", description = "A blue carrot" });
Any configuration specified as part of the registering the HttpClient
services from a Dependency Injection (DI) perspective is ignored by default when creating an HttpClient
using the MockHttpClientFactory
. This default behavior is intended to potentially minimize any side-effect behavior that may occur that is not intended for the unit testing. For example, a DelegatingHandler
may be configured that requests a token from an identity provider which is not needed for the unit test, or may fail due to lack of access from the unit testing environment.
// Startup service (DI) configuration.
services.AddHttpClient("XXX", hc => hc.BaseAddress = new System.Uri("https://somesys")) // This is HttpClient configuration.
.AddHttpMessageHandler(_ => new MessageProcessingHandler()) // This is HttpMessageHandler configuration.
.ConfigureHttpClient(hc => hc.DefaultRequestVersion = new Version(1, 2)); // This is further HttpClient configuration.
However, where the configuration is required then the MockHttpClient
can be configured explicitly to include the configuration; the following methods enable:
Method | Description |
---|---|
WithConfigurations |
Indicates that the HttpMessageHandler and HttpClient configurations are to be used. * |
WithoutConfigurations |
Indicates that the HttpMessageHandler and HttpClient configurations are not to be used (this is the default state). |
WithHttpMessageHandlers |
Indicates that the HttpMessageHandler configurations are to be used. * |
WithoutHttpMessageHandlers |
Indicates that the HttpMessageHandler configurations are not to be used. |
WithHttpClientConfigurations |
Indicates that the HttpClient configurations are to be used. |
WithoutHttpClientConfigurations |
Indicates that the HttpClient configurations are to be used. |
-- | -- |
WithoutMocking |
Indicates that the underlying HttpClient is not to be mocked; i.e. will result in an actual/real HTTP request to the specified endpoint. This is useful to achieve a level of testing where both mocked and real requests are required. Note that an HttpClient cannot support both, these would need to be tested separately. |
Note: *
above denotes that an array of DelegatingHandler
types to be excluded can be specified; with the remainder being included within the order specified.
// Mock with configurations.
var mcf = MockHttpClientFactory.Create();
mcf.CreateClient("XXX").WithConfigurations()
.Request(HttpMethod.Get, "products/xyz").Respond.With(HttpStatusCode.NotFound);
// No mocking, real request.
var mcf = MockHttpClientFactory.Create();
mcf.CreateClient("XXX").WithoutMocking();
To verify the number of times that a request/response is performed UnitTestEx support MOQ Times
, as follows:
var mcf = MockHttpClientFactory.Create();
var mc = mcf.CreateClient("XXX", new Uri("https://d365test"));
mc.Request(HttpMethod.Post, "products/xyz").Times(Times.Exactly(2)).WithJsonBody(new Person { FirstName = "Bob", LastName = "Jane" })
.Respond.WithJsonResource("MockHttpClientTest-UriAndBody_WithJsonResponse3.json", HttpStatusCode.Accepted);
To support different responses per execution MOQ supports sequences. This capability has been extended for UnitTestEx.
var mcf = MockHttpClientFactory.Create();
var mc = mcf.CreateClient("XXX", new Uri("https://d365test"));
mc.Request(HttpMethod.Get, "products/xyz").Respond.WithSequence(s =>
{
s.Respond().With(HttpStatusCode.NotModified);
s.Respond().With(HttpStatusCode.NotFound);
});
A delay (sleep) can be simulated so a response is not always immediated. This can be specified as a fixed value, or randomly generated using a from and to.
var mcf = MockHttpClientFactory.Create();
var mc = mcf.CreateClient("XXX", new Uri("https://d365test"));
mc.Request(HttpMethod.Get, "products/xyz").Respond.Delay(500).With(HttpStatusCode.NotFound);
mc.Request(HttpMethod.Get, "products/kjl").Respond.WithSequence(s =>
{
s.Respond().Delay(250).With(HttpStatusCode.NotModified);
s.Respond().Delay(100, 200).With(HttpStatusCode.NotFound);
});
The Request/Response configuration can also be specified within an embedded resource using YAML/JSON as required. The mock.unittestex.json
JSON schema defines content; where the file is named *.unittestex.yaml
or *.unittestex.json
then the schema-based intellisense and validation will occur within the likes of Visual Studio.
To reference the YAML/JSON from a unit test the following is required:
var mcf = MockHttpClientFactory.Create();
mcf.CreateClient("XXX", new Uri("https://unit-test")).WithRequestsFromResource("my.mock.unittestex.yaml");
The following represents a YAML example for one-to-one request/responses:
- method: post
uri: products/xyz
body: ^
response:
status: 202
body: |
{"product":"xyz","quantity":1}
- method: get
uri: people/123
response:
body: |
{
"first":"Bob",
"last":"Jane"
}
The following represents a YAML example for a request/response with sequences:
- method: get
uri: people/123
sequence:
- body: |
{
"first":"Bob",
"last":"Jane"
}
- body: |
{
"first":"Sarah",
"last":"Johns"
}
Note: Not all scenarios are currently available using YAML/JSON configuration.
By default UnitTestEx provides out-of-the-box Assert*
capabilities that are applied after execution to verify the test results. However, by adding the UnitTestEx.Expectations
namespace in a test additional Expect*
capabilities will be enabled (where applicable). These allow expectations to be defined prior to the execution which are automatically asserted on execution.
The following is an example.
using var test = ApiTester.Create<Startup>();
test.Controller<PersonController>()
.ExpectStatusCode(System.Net.HttpStatusCode.BadRequest)
.ExpectErrors(
"First name is required.",
"Last name is required.")
.Run(c => c.Update(1, new Person { FirstName = null, LastName = null }));
UnitTestEx supports the addition of a appsettings.unittest.json
within the test project that will get loaded automatically when executing tests. This enables settings to be added or modified specifically for the unit testing external to the referenced projects being tested.
Additionally, this can also be used to change the default JSON Serializer for the tests. Defaults to UnitTestEx.Json.JsonSerializer
(leverages System.Text.Json
). By adding the following setting the default JSON serializer will be updated at first test execution and will essentially override for all tests. To change serializer for a specific test then use the test classes to specify explicitly.
{
"DefaultJsonSerializer": "UnitTestEx.MSTest.Test.NewtonsoftJsonSerializer, UnitTestEx.MSTest.Test"
}
As UnitTestEx is intended for testing, look at the tests for further details on how to leverage:
Note: There may be some slight variations in how the tests are constructed per test capability, this is to account for any differences between the frameworks themselves. For the most part the code should be near identical.
These other Avanade repositories leverage UnitTestEx to provide unit testing capabilities:
- CoreEx - Enriched capabilities for building business services by extending the core capabilities of .NET.
- Beef - Business Entity Execution Framework to enable industralisation of API development.
UnitTestEx is open source under the MIT license and is free for commercial use.
One of the easiest ways to contribute is to participate in discussions on GitHub issues. You can also contribute by submitting pull requests (PR) with code changes. Contributions are welcome. See information on contributing, as well as our code of conduct.
See our security disclosure policy.
Avanade is the leading provider of innovative digital and cloud services, business solutions and design-led experiences on the Microsoft ecosystem, and the power behind the Accenture Microsoft Business Group.