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:
- Microsoft.AspNetCore.Mvc
- Microsoft.AspNetCore.Mvc.Core
- Microsoft.AspNetCore.Diagnostics
- Microsoft.AspNetCore.TestHost
- 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;
{
directoryInfo =
directoryInfo.Parent;
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}.");
}
public TestFixture() : this(Path.Combine(""))
{
}
{
Client.Dispose();
Server.Dispose();
}
{
var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;
{
ApplicationParts = { new AssemblyPart(startupAssembly)},
FeatureProviders =
{ new ControllerFeatureProvider(),
new
ViewComponentFeatureProvider()
}
};
}
{
var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;
var contentRoot = GetProjectPath(relativeTargetProjectParentDir,
startupAssembly);
.SetBasePath(contentRoot)
.AddJsonFile("appsettings.json");
.UseContentRoot(contentRoot)
.ConfigureServices(InitializeServices)
.UseConfiguration(configurationBuilder.Build())
.UseEnvironment("Development")
.UseStartup(typeof(TStartup));
Server = new
TestServer(webHostBuilder);
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;
{ Client = fixture.Client; }
public async Task TestGetStockItemsAsync()
{
//
Arrange
var request = "/api/v1/Storagehouse/StockItem";
var response = await Client.GetAsync(request);
response.EnsureSuccessStatusCode();
}
public async Task TestGetStockItemAsync()
{
//
Arrange
var request = "/api/v1/Storagehouse/StockItem/1";
var response = await Client.GetAsync(request);
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();
response.EnsureSuccessStatusCode();
}
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
}
};
var response = await Client.PutAsync(request.Url,
ContentHelper.GetStringContent(request.Body));
response.EnsureSuccessStatusCode();
}
public async Task TestDeleteStockItemAsync()
{
//
Arrange
{
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)
}
};
var postResponse = await Client.PostAsync(postRequest.Url,
ContentHelper.GetStringContent(postRequest.Body));
var jsonFromPostResponse = await postResponse.Content.ReadAsStringAsync();
postResponse.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:
- The Http client in created in class constructor
- Define the request: url and request model (if applies)
- Send the request
- Get the value from response
- 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:
Post a Comment