Writing integration tests for ASP.NET Core app with xUnit, RestEase and TestHost

In my last post, I wrote about great HTTP client for .NET Core called RestEase and I showed how to use it as a simple proxy between API Gateway and microservice. Today I’ll present how this library can also simplify writing part of integration tests.

 

Unit test vs integration test

I assume that for some of you the term „integration test” may sounds odd, therefore I’d like to explain it first to avoid the possible confusion with a unit test later in this article. Let’s start with the second one.

As the name says, a unit test should focus on the one, particular point (unit) in the code. Usually, it’s a (public) method inside the class but there’s no official requirement here. From my experience, the smaller unit you test, the better tests are (since they’re more specific). What’s crucial to unit testing is the isolation. Every dependency on another unit should be replaced with mock or stub. The reason is simple – since we want to test single unit inside our code, we should not rely on any other part of the system. This gives us a certainty if the test does not work, the fault lies on the side of the tested code and not on the dependencies side. Let’s see the simple example:

 


    public class ProductsService : IProductsService
    {
        private readonly IProductsRepository _productsRepository;

        public ProductsService(IProductsRepository productsRepository)
        {
            _productsRepository = productsRepository;
        }

        public ProductEntity Create(string name, decimal price)
        {
            decimal discount;

            if(price > 100) 
                discount  = 50;
            else if (price > 50) 
                discount = 30;
            else 
                discount = 0;

            var product = new ProductEntity 
            {
                Name = name,
                Price = price,
                discount = discount
            };

            var createdProduct = _productsRepository.Create(product);

            return createdProduct;
        }
    }

 

In the example above I tried to illustrate common code in the application. It’s a simple domain service which takes repository as the dependency and uses it to persist data to some database. This service has only one method, which creates a product entity and runs logic on it, so calculates a discount. Consider now, what should we put inside unit tests? Should we care about the persistence layer and checking whether the entity is actually saved into DB? No, because it’s not the part of the unit we’re trying to test. Our test should ONLY check whether domain logic works as expected in the isolated environment. Let’s see the example implementation:

 


    public class ProductsService_Create_Tests
    {

        [Fact]
        public void ProductsService_Create_Returns_New_Product_With_No_Discount_For_Price_Lower_Than_50()
        {
            var createdProduct = _productsService.Create("TV", 30);

            Assert.Equal(0, createdProduct.Discount);
        }

        private readonly IProductsService _productsService;

        public ProductsService_Create_Tests()
        {
            var mockProductsRepository = new MockProductsRepository();
            _productsService = new ProductsService(mockProductsRepository);
        }
    }

 

In our test, instead of the actual implementation of ProductsRepository, I put the mock which could be a proxy object or in-memory store. This gives us mentioned isolation from other parts of the system which could perturb the result of the test. Changing/breaking the implementation of „production” repository would not break the test since we use mock instead. Then we do the actual testing, so checking whether discounts are calculated properly for given data. There’s one more thing. As you see, inside my domain service I used dependency injection pattern. This gave me a lot of flexibility when it came to testing because I could easily switch repository implementation to mock. If you don’t use DI, I’d strongly recommend to give it a try, because it makes our job a lot easier. Don’t try any „quick wins” instead. In most cases, they put you in a trouble.

At this point, someone could say that sometimes it’s impossible to avoid using dependencies inside a tested unit. This can happen if:

  • Tested infrastructure integrates with other library/framework/system
  • Domain logic uses dependency directly (e.g. call to API for current temperature for later calculations)
  • Business process is distributed between plenty different units (common in microservices)
  • You simply want to test some part of your whole system

For such scenarios, besides unit testing, you should also consider writing integration tests. What are they? Once again, the name is self-explained because the main reason for writing integration tests is verifying integration between plenty different parts (units) of the system. We check whether all of them put together work as one, consistent being. The crucial thing here is the fact that integration test should not be treated as some kind of replacement for the unit test. The gif below shows it perfectly:

 

 

Hope you get it. Despite the fact that both windows work as expected (unit tests pass), the integration between them is invalid which makes the whole system invalid. Fine, after this longish introduction to the topic, we can finally write some code!

 

The app

As I wrote before, there are plenty different scenarios when you can write integration tests, but for the simplicity sake, I’ll stick to the last point. So, imagine that you’ve just completed your ASP.NET Core app. All unit tests passed, so you know that your domain logic behaves properly, but you’re still not sure whether all your code works properly. The easiest way to verify that would be to run the app, call the API and do the assertion on the response. That’s exactly what we are going to do! However, before we move further, we should take a look at the app that we are going to test (just the controller part):

 


    [Route("api/Users")]
    public class UsersController : Controller
    {
        private readonly IEnumerable<UserModel> _users = new List<UserModel>
        {
            new UserModel { "John", Age = 13 },
            new UserModel { "Sam", Age = 24 },
            new UserModel { "Pit", Age = 44 },
        };


        [HttpGet("Filtered")]
        public IEnumerable<UserModel> GetFilteredUsers([FromQuery] ushort minAgeFilter)
            => _users.Where(user => user.Age >= minAgeFilter);
    }

 

Well, not very impressive but still we need to test it somehow.

 

Preparing test project

The first thing we need to is creating a test project. Since I’m a big fan of xUnit, I use dotnet CLI for that purpose:

 


dotnet new xunit -n MyApp.IntegrationTests

 

Having the project installed, we need to add three dependencies:

  • Project we want to test (project reference) – obvious
  • Microsoft.AspNetCore.TestHost (package reference) – provides TestHost class which runs our app during the tests
  • RestEase (package reference) – strongly typed HTTP client for .NET Core
  • I’d also recommend using Shouldly for nice assertions but it’s not required

 

Creating integration test

Below I put the code of the example integration test for our API:

 


    public interface IUsersApi
    {
        [Get("/api/Users/Filtered")]
        Task<IEnumerable<UserModel>> GetFilteredUsersAsync(ushort minAgeFilter);
    }

    public class UsersApi_IntegrationTests
    {
        [Fact]
        public async void UsersApi_GetFilteredUsersAsync_Returns_Properly_Filtered_Users()
        {
            ushort minAgeFilter = 13;
            var users = await _usersApi.GetFilteredUsersAsync(minAgeFilter);

            users.ShouldAllBe(user => user.Age > minAgeFilter);
        }


        private readonly IUsersApi _usersApi;
        private readonly TestServer _testServer;

        public UsersApi_IntegrationTests()
        {
            var webHostBuilder = new WebHostBuilder().UseStartup<TestedAppStartup>();

            _testServer = new TestServer(webHostBuilder);
            _usersApi = RestClient.For<IUsersApi>(_testServer.CreateClient());
        }
    }

 

Let’s start with the constructor which in this case is on the bottom (just my habit). The first line is very similar to what you can find inside Program class (in ASP.NET Core 2.0). It creates a new instance of WebHostBuilder class which is then passed to the TestServer’s constructor. This class will use the builder to create a temporary test environment for our app. Using other words, we’ll be able to call the API inside each integration test. Having the environment up and running, we still need to configure RestEase to make HTTP calls. Typically RestClient takes the URI as the parameter, but fortunately, there’s also an overload which accepts .NET HttpClient. We can get it from TestServer by calling a CreateClient method.
The integration test is just one of the possible cases for our API, but I don’t see the point for writing all of them here. I wanted to check whether each item returned in the collection has the property Age greater than or equal to minAgeFilter variable. This would ensure me that the filter mechanism is implemented correctly. The code inside the test is very simple. The API is called using RestEase client and the response from the asynchronous operation is assigned to the users variable. Then, a simple assertion is done using Shouldly, which checks my requirement. That’s it, no more code required.

Of course, in some scenarios, you might also want to check the response object returned from the API to test HTTP status code or something different. Fortunately, this is very simple with RestEase:

 


        [Fact]
        public async void UsersApi_GetFilteredUsersAsync_Returns_Properly_Filtered_Users()
        {
            ushort minAgeFilter = 13;
            var response = await _usersApi.GetFilteredUsersAsync(minAgeFilter);

            response.ResponseMessage.StatusCode.ShouldBe(HttpStatusCode.OK);

            var users = response.GetContent();
            users.ShouldAllBe(user => user.Age > minAgeFilter);
        }

 

As you see, we can verify the Response from API and then easily access the actual data returned.

Hopefully, this article brightened some of you what are integration tests and how to write them for ASP.NET Core app. Unfortunately, from my experience, I see that hardly anyone writes them in their code. It’s sad because thanks to them that we can really say whether what we wrote actually works.

You may also like...