max-arshinov
Posted on March 10, 2023
Web Application Factory dramatically improves the developer experience. However, it can be even better. First of all, let’s check the example from the Microsoft website.
public class BasicTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public BasicTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Fact]
public async Task ReturnsSucces(string url)
{
// Arrange
var client = _factory.CreateClient();
var url = "about";
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode();
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
That’s cool. I can run high-level integration tests before deploying the code to an environment. This option reduces the efforts of detecting and fixing defects because integration tests can be run during the implementation phase, just before submitting a merge request.
Unfortunately, this example is far from being realistic. What we really want to test might look like this:
[Theory]
[ClassData(typeof(SophisticatedDataProvider))]
public async Task ReturnsSucces(
InputParams input, ExpectedOutput expectedOutput)
{
// Arrange
var client = _factory.CreateClient();
var url = "enpoint-with-a-lot-of-code-branches";
// Act
var content =
CreatePostContentFromInputParams(input);
var response = await client.PostAsync(url, content);
// Assert
response.EnsureSuccessStatusCode();
var outputData = response
.Content
.ReadAsJsonAsync<OutputParams>();
ComplexAssertions(expectedOutput, outputData);
}
}
Urls and input parameters are subject to change. Despite we have access to all the required .NET code through the assembly reference, which we need anyway to make the web application factory work, we have to do model binding and routing on our one in the test code. What if we could do something like:
public class MyControllerTest:
ControllerTestBase<MyController>
{
[Theory]
[ClassData(typeof(SophisticatedDataProvider))]
public async Task ReturnsSucces(
InputParams input, ExpectedOutput expectedOutput)
{
// Arrange
var client = _factory.CreateControllerClient();
// Act
var outputData = await client.SendAsync(
(MyController c) => c.Post(input));
// Assert
response.EnsureSuccessStatusCode();
ComplexAssertions(expectedOutput, outputData);
}
}
If When routes or input/output parameters change developers would know about these issues first. This would reduce the efforts of detecting and fixing defects again by introducing compile-time safety for the test code. Luckily, the example above can perfectly work with help of Expression Trees. Moq uses exactly the same technique to configure mocks.
var mock = new Mock<ILoveThisLibrary>();
Mock
.Setup(library =>
library.DownloadExists("2.0.0.0")) // Check lambda type
.Returns(true); // it's expression!
You can find the complete source code of Controller Client on GitHub in case you are interested. Otherwise just install AspNetCore.Testing.Expressions via nuget and become an alpha tester.
Posted on March 10, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.