Monday, May 31, 2021

Creating Web API in ASP.NET Core 2.0 - Part 3 - Integration Tests

 Step 08 - Add Integration Tests

In order to add integration tests for API project, follow these steps:

Right click on Solution > Add > New Project

Go to Installed > Visual C# > Test > xUnit Test Project (.NET Core)

Set the name for project as Sample.API.IntegrationTests

Click OK

Manage references for Sample.API.IntegrationTests project:

Now add a reference for Sample.API project:


Once we have created the project, add the following NuGet packages for project:

  1. Microsoft.AspNetCore.Mvc
  2. Microsoft.AspNetCore.Mvc.Core
  3. Microsoft.AspNetCore.Diagnostics
  4. Microsoft.AspNetCore.TestHost
  5. Microsoft.Extensions.Configuration.Json

Remove UnitTest1.cs file.

Save changes and build Sample.API.IntegrationTests project.

What is the difference between unit tests and integration tests? For unit tests, we simulate all dependencies for Web API project and for integration tests, we run a process that simulates Web API execution, this means Http requests.

Now we proceed to add code related for integration tests.

For this project, integration tests will perform Http requests, each Http request will perform operations to an existing database in SQL Server instance. We'll work with a local instance of SQL Server, this can change according to your working environment, I mean the scope for integration tests.

Code for TestFixture.cs file

using System;

using System.IO;

using System.Net.Http;

using System.Net.Http.Headers;

using System.Reflection;

using Microsoft.AspNetCore.Hosting;

using Microsoft.AspNetCore.Mvc.ApplicationParts;

using Microsoft.AspNetCore.Mvc.Controllers;

using Microsoft.AspNetCore.Mvc.ViewComponents;

using Microsoft.AspNetCore.TestHost;

using Microsoft.Extensions.Configuration;

using Microsoft.Extensions.DependencyInjection;

namespace Sample.API.IntegrationTests

{

    public class TestFixture<TStartup> : IDisposable

    {

        public static string GetProjectPath(string projectRelativePath, Assembly startupAssembly)

        {

            var projectName = startupAssembly.GetName().Name;

            var applicationBasePath = AppContext.BaseDirectory;

             var directoryInfo = new DirectoryInfo(applicationBasePath);

             do

            {

                directoryInfo = directoryInfo.Parent;

                var projectDirectoryInfo = new DirectoryInfo(Path.Combine(directoryInfo.FullName, projectRelativePath));

              if (projectDirectoryInfo.Exists)

               if (new FileInfo(Path.Combine(projectDirectoryInfo.FullName, projectName, $"{projectName}.csproj")).Exists)

                        return Path.Combine(projectDirectoryInfo.FullName, projectName);

            }

            while (directoryInfo.Parent != null); 

            throw new Exception($"Project root could not be located using the application root {applicationBasePath}.");

        }

         private TestServer Server; 

        public TestFixture() : this(Path.Combine(""))

        {

        }

         public HttpClient Client { get; }

         public void Dispose()

        {

            Client.Dispose();

            Server.Dispose();

        }

         protected virtual void InitializeServices(IServiceCollection services)

        {

            var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;

             var manager = new ApplicationPartManager

            {

                ApplicationParts = new AssemblyPart(startupAssembly)},

                FeatureProviders =

                {   new ControllerFeatureProvider(),

                    new ViewComponentFeatureProvider()

                }

            };

             services.AddSingleton(manager);

        }

         protected TestFixture(string relativeTargetProjectParentDir)

        {

            var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;

            var contentRoot = GetProjectPath(relativeTargetProjectParentDir, startupAssembly);

             var configurationBuilder = new ConfigurationBuilder()

                .SetBasePath(contentRoot)

                .AddJsonFile("appsettings.json");

             var webHostBuilder = new WebHostBuilder()

                .UseContentRoot(contentRoot)

                .ConfigureServices(InitializeServices)

                .UseConfiguration(configurationBuilder.Build())

                .UseEnvironment("Development")

                .UseStartup(typeof(TStartup));

             // Create instance of test server

            Server = new TestServer(webHostBuilder);

             // Add configuration for client

            Client = Server.CreateClient();

            Client.BaseAddress = new Uri("http://localhost:5001");

            Client.DefaultRequestHeaders.Accept.Clear();

            Client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        }

    }

}

Code for ContentHelper.cs file:

using System.Net.Http;

using System.Text;

using Newtonsoft.Json;

namespace Sample.API.IntegrationTests

{

    public static class ContentHelper

    {

        public static StringContent GetStringContent(object obj)

            => new StringContent(JsonConvert.SerializeObject(obj), Encoding.Default, "application/json");

    }

}

Code for StoragehouseTests.cs file:

using System;

using System.Net.Http;

using System.Threading.Tasks;

using Newtonsoft.Json;

using Sample.API.Models;

using Xunit;

namespace Sample.API.IntegrationTests

{

    public class StoragehouseTests : IClassFixture<TestFixture<Startup>>

    {

        private HttpClient Client;

        public StoragehouseTests(TestFixture<Startup> fixture)

        Client = fixture.Client;  }

         [Fact]

        public async Task TestGetStockItemsAsync()

        {

            // Arrange

            var request = "/api/v1/Storagehouse/StockItem";

             // Act

            var response = await Client.GetAsync(request);

             // Assert

            response.EnsureSuccessStatusCode();

        }

         [Fact]

        public async Task TestGetStockItemAsync()

        {

            // Arrange

            var request = "/api/v1/Storagehouse/StockItem/1";

             // Act

            var response = await Client.GetAsync(request);

             // Assert

            response.EnsureSuccessStatusCode();

        }

        [Fact]

        public async Task TestPostStockItemAsync()

        {

            // Arrange

            var request = new

            {

                Url = "/api/v1/Storagehouse/StockItem",

                Body = new

                {

                    StockItemName = string.Format("USB anime flash drive - Vegeta {0}", Guid.NewGuid()),

                    SupplierID = 12,

                    UnitPackageID = 7,

                    OuterPackageID = 7,

                    LeadTimeDays = 14,

                    QuantityPerOuter = 1,

                    IsChillerStock = false,

                    TaxRate = 15.000m,

                    UnitPrice = 32.00m,

                    RecommendedRetailPrice = 47.84m,

                    TypicalWeightPerUnit = 0.050m,

                    CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"32GB\",\"USB Powered\"] }",

                    Tags = "[\"32GB\",\"USB Powered\"]",

                    SearchDetails = "USB anime flash drive - Vegeta",

                    LastEditedBy = 1,

                    ValidFrom = DateTime.Now,

                    ValidTo = DateTime.Now.AddYears(5)

                }

            };

            // Act

            var response = await Client.PostAsync(request.Url, ContentHelper.GetStringContent(request.Body));

            var value = await response.Content.ReadAsStringAsync();

             // Assert

            response.EnsureSuccessStatusCode();

        }

         [Fact]

        public async Task TestPutStockItemAsync()

        {

            // Arrange

            var request = new

            {

                Url = "/api/v1/Storagehouse/StockItem/1",

                Body = new

                {

                    StockItemName = string.Format("USB anime flash drive - Vegeta {0}", Guid.NewGuid()),

                    SupplierID = 12,

                    Color = 3,

                    UnitPrice = 39.00m

                }

            };

             // Act

            var response = await Client.PutAsync(request.Url, ContentHelper.GetStringContent(request.Body));

             // Assert

            response.EnsureSuccessStatusCode();

        }

         [Fact]

        public async Task TestDeleteStockItemAsync()

        {

            // Arrange

             var postRequest = new

            {

                Url = "/api/v1/Storagehouse/StockItem",

                Body = new

                {

                    StockItemName = string.Format("Product to delete {0}", Guid.NewGuid()),

                    SupplierID = 12,

                    UnitPackageID = 7,

                    OuterPackageID = 7,

                    LeadTimeDays = 14,

                    QuantityPerOuter = 1,

                    IsChillerStock = false,

                    TaxRate = 10.000m,

                    UnitPrice = 10.00m,

                    RecommendedRetailPrice = 47.84m,

                    TypicalWeightPerUnit = 0.050m,

                    CustomFields = "{ \"CountryOfManufacture\": \"USA\", \"Tags\": [\"Sample\"] }",

                    Tags = "[\"Sample\"]",

                    SearchDetails = "Product to delete",

                    LastEditedBy = 1,

                    ValidFrom = DateTime.Now,

                    ValidTo = DateTime.Now.AddYears(5)

                }

            };

             // Act

            var postResponse = await Client.PostAsync(postRequest.Url, ContentHelper.GetStringContent(postRequest.Body));

            var jsonFromPostResponse = await postResponse.Content.ReadAsStringAsync();

             var singleResponse = JsonConvert.DeserializeObject<SingleResponse<StockItem>>(jsonFromPostResponse);

             var deleteResponse = await Client.DeleteAsync(string.Format("/api/v1/Storagehouse/StockItem/{0}", singleResponse.Model.StockItemID));

             // Assert

            postResponse.EnsureSuccessStatusCode();

            Assert.False(singleResponse.DidError);

            deleteResponse.EnsureSuccessStatusCode();

        }

    }

}

As we can see, StoragehouseTests contain all tests for Web API, these are the methods:

METHODS

DESCRIPTION

TestGetStockItemsAsync

Retrieves the stock items

TestGetStockItemAsync

Retrieves an existing stock item by ID

TestPostStockItemAsync

Creates a new stock item

TestPutStockItemAsync

Updates an existing stock item

TestDeleteStockItemAsync

Deletes an existing stock item



















How Integration Tests Work?

TestFixture class provides a Http client for Web API, uses Startup class from project as reference to apply configurations for client.

StoragehouseTests class contains all methods to send Http requests for Web API, the port number for Http client is 1234.

ContentHelper class contains a helper method to create StringContent from request model as JSON, this applies for POST and PUT requests.

The process for integration tests is:

  1. The Http client in created in class constructor
  2. Define the request: url and request model (if applies)
  3. Send the request
  4. Get the value from response
  5. Ensure response has success status

Running Integration Tests

Save all changes and build Sample.API.IntegrationTests project, test explorer will show all tests in project:


Keep in mind: To execute integration tests, you need to have running an instance of SQL Server, the connection string in appsettings.json file will be used to establish connection with SQL Server.

Now run all integration tests, the test explorer looks like the following image:


If you get any error executing integration tests, check the error message, review code and repeat the process.

Code Challenge

At this point, you have skills to extend API, take this as a challenge for you and add the following tests:

TEST

DESCRIPTION

Get stock items by parameters

Make a request for stock items searching by lastEditedBy, colorID, outerPackageID, supplierID, unitPackageID parameters.

Get a non existing stock item

Get a stock item using a non existing ID and check Web API returns NotFound (404) status.

Add a stock item with existing name

Add a stock item with an existing name and check Web API returns BadRequest (400) status.

Add a stock item without required fields

Add a stock item without required fields and check Web API returns BadRequest (400) status.

Update a non existing stock item

Update a stock item using a non existing ID and check Web API returns NotFound (404) status.

Update an existing stock item without required fields

Update an existing stock item without required fields and check Web API returns BadRequest (400) status.

Delete a non existing stock item

Delete a stock item using a non existing ID and check Web API returns NotFound (404) status.

Delete a stock item with orders

Delete a stock item using a non existing ID and check Web API returns NotFound (404) status.


















Follow the convention used in unit and integration tests to complete this challenge.

Good luck!

Related Links

·       Integration tests in ASP.NET Core

 



No comments:

Popular Posts