Skip to content

UnitTestEx provides .NET testing extensions to the most popular testing frameworks (MSTest, NUnit and Xunit) specifically to improve the testing experience with ASP.NET controller, and Azure Function, execution including underlying HttpClientFactory mocking.

License

Avanade/UnitTestEx

Repository files navigation


Logo


Introduction

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.


Status

The build and packaging status is as follows.

CI UnitTestEx UnitTestEx.MSTest UnitTestEx.NUnit UnitTestEx.Xunit
CI NuGet version NuGet version NuGet version NuGet version

The included change log details all key changes per published version.


API Controller

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" });

HTTP-triggered Azure Function

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.


Service Bus-trigger Azure Function

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.


Generic Azure Function Type

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();

Generic Type

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);

HTTP Client mocking

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" });

HTTP Client configurations

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();

Times

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);

Sequeuce

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);
});

Delay

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);
});

YAML/JSON configuration

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.


Expectations

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 }));

appsettings.unittest.json

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"
}

Examples

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.


Other repos

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.

License

UnitTestEx is open source under the MIT license and is free for commercial use.


Contributing

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.


Security

See our security disclosure policy.


Who is Avanade?

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.

About

UnitTestEx provides .NET testing extensions to the most popular testing frameworks (MSTest, NUnit and Xunit) specifically to improve the testing experience with ASP.NET controller, and Azure Function, execution including underlying HttpClientFactory mocking.

Topics

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks