Fullstack Hub

[Count: 1]

In previous parts, we implemented the Application unit and Persistence Integration tests, in this last part, let’s implement the API project integration tests. We will follow the same approach we did with the Application and Persistence test e.g. fixture class (with a different name), using a test database, etc.

Let’s Start

In API integration tests, we will call the controller actions (APIs) by using an HTTP client, read the HTTPResponse, and verify the content against successful data or error messages. Create a new test project UserManagement.API.IntegrationTests using MSTest Test Project (.NET Core) template in the tests folder. Delete the default UnitTest1.cs file and add the following dependencies before moving further in order to avoid any compilation error in upcoming steps:

  • Shouldly 3.0.2
  • xunit 2.4.1
  • xunit.runner.visualstudio 2.4.3
  • Microsoft.AspNetCore.Mvc.Testing 3.1.8
  • System.Configuration.ConfigurationManager 5.0.0
  • Add UserManagement.API project reference.
Create Integration Test Helper Class

First, let’s create the static class IntegrationTestHelper with two functions:

  1. GetRequestContent(): This function will take the object type variable (basically our input payload for HTTP Request e.g. user object etc.) and convert it to StringContent so that it can travel with HTTP Request.
  2. GetResponseContent(): This function will help us to read the HTTPResponseMessage content once the API would return the response from the server. E.g. if we will call the GET User API, we can call this function to read the HTTP response as a JSON string. This function is optional since we will be using ReadAsStringAsync() function.

Create a new class IntegrationTestHelper in the root of UserManagement.API.IntegrationTests project and replace its content with the following:

namespace UserManagement.API.IntegrationTests
{
    using System.Net.Http;
    using System.Text;
    using System.Threading.Tasks;
    using Newtonsoft.Json;

    /// <summary>
    /// Generic methods for JSON serialization.
    /// </summary>
    public static class IntegrationTestHelper
    {
        /// <summary>
        /// Serialize the JSON in API response.
        /// </summary>
        /// <param name="obj">The input object to be serialized.</param>
        /// <returns>Return the string conent the JSON.</returns>
        public static StringContent GetRequestContent(object obj)
        {
            return new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, "application/json");
        }

        /// <summary>
        /// Deserialize the JSON.
        /// </summary>
        /// <typeparam name="T">Input genric class object.</typeparam>
        /// <param name="response">The HttpResponseMessage object.</param>
        /// <returns>The result.</returns>
        public static async Task<T> GetResponseContent<T>(HttpResponseMessage response)
        {
            var stringResponse = await response.Content.ReadAsStringAsync();

            var result = JsonConvert.DeserializeObject<T>(stringResponse);

            return result;
        }
    }
}
Create Web Application Factory (Kind of Fixture Class)

Let’s create a generic custom Application Factory extending the WebApplicationFactory. The WebApplicationFactory class is provided by Microsoft specifically for testing. It is used to create the TestServer, HTTP Client to make HTTP calls, etc. We will also override its ConfigureWebHost() function to define our dependencies. To read more about WebApplicationFactory, click here.

Create a new class CustomWebApplicationFactory in the root of UserManagement.API.IntegrationTests project and replace its content with the following:

namespace UserManagement.API.IntegrationTests
{
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Mvc.Testing;
    using Microsoft.Extensions.DependencyInjection;
    using System.Data;
    using System.Data.SqlClient;
    using System.Net.Http;
    using UserManagement.Application.Common.Interfaces;
    using UserManagement.Domain.UnitOfWork;
    using UserManagement.Persistence.Constant;
    using UserManagement.Persistence.UnitOfWork;

    /// <summary>
    /// The APIs test setup.
    /// </summary>
    /// <typeparam name="TStartup">The input object.</typeparam>
    public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup>
        where TStartup : class
    {
        /// <summary>
        /// Get the anonymous client for testing.
        /// </summary>
        /// <returns>The anonymous client.</returns>
        public HttpClient GetAnonymousClient()
        {
            return this.CreateClient();
        }

        /// <summary>
        /// Setup for middleware.
        /// </summary>
        /// <param name="builder">Take the WebHostBuilder object.</param>
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder
               .ConfigureServices(services =>
               {
                   services.AddSingleton<IConfigConstants, ConfigConstants>();
                   services.AddSingleton<IDbConnection>(conn => new SqlConnection(conn.GetService<IConfigConstants>().TestFullStackConnection));
                   services.AddTransient<IUnitOfWork>(uof => new UnitOfWork(uof.GetService<IDbConnection>()));
               });
        }
    }
}

We can see CustomWebApplicationFactory is a generic class taking TStartup datatype where we will send the Startup class type of UserManagement.API project.

In the GetAnonymousClient() function, we are calling the WebApplicationFactory class built-in function CreateClient() that will be used to make HTTP Class.

We are overriding the ConfigureWebHost() function and defining the required dependencies. Check the IDBConnection dependency, we are specifying the test database connection string instead of the development database and then injecting this DBConnection object to UnitOfWork class so that our UserRepository uses the test database for all operations.

Create User Controller Integration Tests

So far, we created the helping classes, let’s create the User APIs tests i.e. GET API to load the users, PUT API to update, POST API to add a new user, and DELETE API to remove the user from the database. We will use the test client defined in CustomWebApplicationFactory class.

Create a new folder User in UserManagement.API.IntegrationTests project and add a class UserAPI in it, replace class content with the following:

namespace UserManagement.API.IntegrationTests.User
{
    using Xunit;
    using System.Threading.Tasks;
    using Shouldly;
    using UserManagement.Application.User.Commands;
    using System.Net;
    using System;

    public class UserAPI : IClassFixture<CustomWebApplicationFactory<Startup>>
    {
        private readonly CustomWebApplicationFactory<Startup> factory;

        public UserAPI(CustomWebApplicationFactory<Startup> factory)
        {
            this.factory = factory;
        }

        [Fact]
        public async Task GivenValidGetAllUserQuery_ReturnSuccessObject()
        {
            await AddNewUser();
            var client = this.factory.GetAnonymousClient();
            var response = await client.GetAsync($"api/User/GetAll");
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task GivenValidGetUserQuery_ReturnSuccessObject()
        {
            var res = await AddNewUser();
            var client = this.factory.GetAnonymousClient();
            var response = await client.GetAsync($"api/User/Get?userID={res}");
            var result = response.Content.ReadAsStringAsync().Result;
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task GivenValidAddUserQuery_ReturnSuccessObject()
        {
            var res = await AddNewUser();
            res.ShouldBeGreaterThan(0);
        }

        [Fact]
        public async Task GivenValidUpdateUserQuery_ReturnSuccessObject()
        {
            var res = await AddNewUser();
            var client = this.factory.GetAnonymousClient();
            var command = new UpdateUserCommand
            {
                UserID = res,
                City = "SpringField",
                Country = "USA",
                State = "VA",
                Zip = "66006",
                PhoneNumber = "888-88-8888",
            };

            var content = IntegrationTestHelper.GetRequestContent(command);
            var response = await client.PutAsync($"api/User/Put", content);
            response.EnsureSuccessStatusCode();

        }

        [Fact]
        public async Task GivenValidDeleteUserQuery_ReturnSuccessObject()
        {
            var res = await AddNewUser();
            var client = this.factory.GetAnonymousClient();
            var response = await client.DeleteAsync($"api/User/Delete?userID={res}");
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task GivenValidGetUserQuery_SendNullUserID_ReturnErrorCode()
        {
            var client = this.factory.GetAnonymousClient();
            var response = await client.GetAsync($"api/User/Get");
            var result = response.Content.ReadAsStringAsync().Result;
            result.ShouldContain("User ID is required");
            response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
        }

        private async Task<int> AddNewUser()
        {
            var client = this.factory.GetAnonymousClient();
            var command = new AddUserCommand
            {
                FirstName = "Test",
                LastName = "Doe",
                City = "Falls Chruch",
                Country = "USA",
                State = "VA",
                Zip = "22044",
                DOB = new DateTime(1980, 01, 01),
                EmailAddress = "jdoe@fullstackhub.io",
                Gender = "M",
                PhoneNumber = "444-443-4444",
            };

            var content = IntegrationTestHelper.GetRequestContent(command);
            var response = await client.PostAsync($"api/User/Post", content);
            return Convert.ToInt32(response.Content.ReadAsStringAsync().Result);
        }
    }
}

The UserAPI class is implementing an IClassFixture interface of the Xunit package sending a CustomWebApplicationFactory<Startup> as a fixture type. The Startup class is referenced from UserManagement.API project so we have all middleware and dependencies are in place except those we override to inject test database connection string in CustomWebApplicationFactory class’s ConfigureWebHost() function. Hopefully, it is making sense now.

Like Persistence test cases, we are explicitly adding the user into a database before each test case through the private method AddNewUser() function. In the UserAPI constructor, the testing engine is taking care of dependency injection and we have a factory variable of CustomWebApplicationFactory type.

In GivenValidGetAllUserQuery_ReturnSuccessObject() test, after adding the user, we are creating an HTTP client through the factory’s GetAnonymousClient() function, making a call to GetAll API (action), and receiving the HTTPResponseMessage object. We are verifying that the API is sending back success HTTP status code 200. The rest of the tests are following the same strategy.

The GivenValidGetUserQuery_SendNullUserID_ReturnErrorCode() is a negative test case where we are calling the GET API but not passing the UserID. We are reading the HTTPResponseMessage content and verifying the error message sent by the corresponding query validator class. Go ahead and create more negative tests for add, update, and delete for practice.

Go to View -> Test Explorer (if not visible on left side pane), and click on the first button (Run All Tests in View) or simply press Ctrl + R keys. Make sure your all tests are passed.

That’s it for ASP.NET Web API & Angular 10 Clean Architecture learning path, hopefully, it helped you to understand a few concepts.

Yaseer Mumtaz

2 Replies to “ASP.NET Core with Angular Application Architecture – Part 11”

  1. Thank you! It’s a great article totally practical. This is the best way to understand design patterns.
    I liked very much the way you implemented the configurations class as constants, it makes it very straightforward to consume the application settings anywhere

    Regards

Leave a Reply

Your email address will not be published. Required fields are marked *