Functional Testing for ASP.NET Core API
Tags: dotnet, aspnet, testing, api
Published: 2022-02-13 09:54:17


Functional testing is the next level above the unit testing and gives us confidence that the code we wrote works as intended. If we are talking about functional testing in the scope of ASP.NET, we mean how we test our API.


![Functional testing](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s9xyh1ezia8k6kwtonyb.png)

## What actually functional testing is?

> Functional testing is the process through which we determine if a piece of software is acting following pre-determined requirements. It uses black-box testing techniques, in which the tester does not know the internal system logic.

In the scope of API, we need to prepare possible requests that we expect to get and be sure that endpoints return a proper response for any payload.

## Should we even spend time on it?

**Definitely!** If you don`t write integrational tests, then functional testing is a solution to ensure that your code has fewer bugs than it could have. When you can write integrational tests as well, it should be simple to reuse a shared codebase for functional and integrational tests. One difference will be in the proper environmental setup.

## Let`s define a simple API

Our controller contains only single action to create some entity for test purposes. Our business rules may require any entity, which does not matter for our test example. Besides, we should remember about authentication nowadays. Security compliance requires protecting API with any authentication. Let`s assume that the current API requires an Authorization header with a Bearer token.

```cs
[ApiController]
public class MyController : ControllerBase
{
    private readonly IService _service;
    
    public MyController(IService service)
    {
        _service = service;
    }
   
    [HttpPost("entities")]
    [ProducesResponseType(typeof(CreateResponse), (int)HttpStatusCode.OK)]
    [ProducesResponseType(typeof(ExceptionResponse), (int)HttpStatusCode.BadRequest)]
    public Task<CreateResponse> CreateAsync([FromBody] CreateRequest request)
    {
        return _service.CreateAsync(request);
    }
}
```

## What will the functional test look like for this controller?

In our test project, I used NUnit as a test framework, but it is up to you to choose which better suits your needs. NUnit has all the required functionality to write good tests. Also, it`s pretty popular on the market.

Let`s add the following packages to the test project:

```cs
dotnet add package NUnit
dotnet add package NUnit3TestAdapter
```

Take a time to read about [test frameworks](/2021/11/02/xunit-vs-nunit-what-to-choose/) in .NET:


```cs
[TestFixture]
public class ApiControllerTests
{
    private const string CreateUrl = "/entities";

    private readonly TokenBuilder _tokenBuilder = new();

    private TestServer? _server;
    private HttpClient? _client;

    [SetUp]
    public void Setup()
    {

        var settings = new Settings
        {
            SymmetricFuncTestKey = "your key for functional tests"
        };

        var builder = new WebHostBuilder()
            .UseStartup<Startup>()
            .ConfigureTestServices(services =>
            {
                // Mock your external services here
            })
            .ConfigureServices(services =>
            {
                // Change authentication to use bearer token which signed by symmetric key
                services.PostConfigure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
                {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        IssuerSigningKey =
                            new SymmetricSecurityKey(Encoding.UTF8.GetBytes(settings.SymmetricFuncTestKey)),
                        ValidateIssuerSigningKey = true,
                        ValidateIssuer = true,
                        ValidateAudience = true,
                        ValidateLifetime = true,
                        ValidIssuer = TestConstants.Issuer,
                        ValidAudience = TestConstants.Audience,
                    };
                });
            });
        _server = new TestServer(builder);
        _client = _server.CreateClient();
    }

    [Test]
    public async Task CreateAsync_Should_ReturnNotAuthenticated_When_RequestWithoutAuthToken()
    {
        // Arrange
        var request = new CreateRequest
        {
            PropertyName = "propertyValue"
        };
        var json = JsonConvert.SerializeObject(request);
        using var data = new StringContent(json, Encoding.UTF8, "application/json");

        // Act
        var response = await _client?.PostAsync(CreateUrl, data)!;

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
    }

    [Test]
    public async Task CreateAsync_Should_Return400_When_RequestValidationFailed()
    {
        // Arrange
        var request = new CreateRequest();
        var json = JsonConvert.SerializeObject(request);
        using var data = new StringContent(json, Encoding.UTF8, "application/json");
        var token = await _tokenBuilder.BuildJWTToken();
        _client?.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);

        // Act
        var response = await _client?.PostAsync(CreateUrl, data)!;

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
    }

    [Test]
    public async Task CreateAsync_Should_ReturnCreateResponse()
    {
        // Arrange
        var request = new CreateRequest
        {
            PropertyName = "propertyValue"
        };
        var json = JsonConvert.SerializeObject(request);
        using var data = new StringContent(json, Encoding.UTF8, "application/json");
        var token = await _tokenBuilder.BuildJWTToken();
        _client?.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);

        var expected = new CreateResponse
        {
            PropertyName = "propertyValue"
        };

        // Act
        var response = await _client?.PostAsync(CreateUrl, data)!;

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);
        var responseBody = await response.Content.ReadAsStringAsync();
        var responseObject = JsonConvert.DeserializeObject<CreateResponse>(responseBody);
        responseObject.Should().BeEquivalentTo(expected);
    }
}
```

We should cover that API endpoint protected by authentication, and clients can not request it without a proper token. To do this, we need to prepare a test server with a proper authentication setup. To do it in Startup, we need to override **JwtBearerOptions** with our test authentication, which relies on auth token signed by **SymmetricSecurityKey**. To comply with security rules, we should avoid storing any secrets in code, and I don`t suggest you violate this rule even for test projects.

When the authentication configuration is ready, we can generate the proper token to execute business logic in test cases. We can separate token generation into **TokenBuilder** and use it to get auth token.

## Conclusion

I hope you feel that functional tests are pretty simple and allow you to verify your API each time something changes. We often include functional tests as a step in your build pipeline, and it gives you confidence that everything works well before deploying to the production environment.

Any questions or comments? Ping me on LinkedIn or comment below. And if you liked this post, please give it a clap and share it with all of your friends.

Twitter: https://twitter.com/KyliaBoy
Linkedin: https://www.linkedin.com/in/andrew-kulta/

More articles you can find at:
https://blog.akyltech.com/