Functional Testing For ASP.NET Core API

What Is Functional Testing, Actually?

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 the 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 doesn’t matter for our test example. Besides, we should remember about authentication nowadays. Security requires compliance protecting API with any authentication. Let’s assume that the current API requires an Authorization header with a Bearer token.

[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’s 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:

dotnet add package NUnitdotnet add package NUnit3TestAdapter
[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’t 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 leave a comment. And if you liked this post, please give it a clap and share it with all of your friends.

.

Leave a Comment