Welcome to today’s post.
In today’s post I will be showing how we can create basic integration tests using the NUnit testing framework using .NET Core.
In a previous post I have shown how to create basic unit tests using .NET Core.
What are the main differences between unit tests and integration tests?
- With unit tests, we make use of mocking frameworks to simulate dependencies of the target resources of our unit test.
- With unit tests, the focus is to test the business logic of our application.
- With unit tests, the dependent resources can be mocked, including databases, external services and so forth.
- With unit tests, the test runs generally are of shorter duration.
- With integration tests, the actual resources in production including databases and services are tested.
- With integration tests, the test runs take significantly longer to run.
- With integration tests, the code required to execute the tests tends to be more complex.
I will cover two different areas of a .NET Core application that can benefit from integration testing:
- Custom data-dependent services.
- Application HTTP REST APIs.
With custom data-dependent services, we will test the following two areas:
- Custom service methods.
- The interaction of the custom service with backend data store.
Our custom service is part of a .NET Core API and the backend data store is a SQL server database accessed through an EF Core data provider from the .NET Core API service. Before we discuss implementation, I will show how to create and configure a unit test project.
Setting up our Unit Test project
When creating a new application, use the NUnit .NET Core Test Project template as shown:
As a suggestion, I would recommend creating the unit test project within its own folder separate from any dependent Visual Studio web application or web API project so that any automated tests from a CI tool can run by opening a dedicated solution.
To reference a dependent project from the unit test solution, select Add Project .. from the solution root and select the project to add as shown:
Next, open the default unit test source file and rename the file and namespace / class names if desired.
To access each test resource target, we will need to provide a configuration file within our integration test project.
We create a JSON file configuration.json in our project folder and populate with various keys and values for our database, API URLs etc. An example is shown below:
{
"ConnectionStrings": {
"AppDbContext": "Server=localhost;Database=aspnet-BookCatalog;User Id=?????;Password=?????;"
},
"AppSettings": {
"UrlTokenAPI": "http://localhost/BookLoan.Identity.API/",
"UrlCatalogAPI": "http://localhost/BookLoan.Catalog.API/"
}
}
We will also need a configuration model class to map our app settings keys and values from the configuration file:
namespace BookLoanIntegrationTest
{
public class AppConfiguration
{
public AppConfiguration() { }
public string UrlCatalogAPI { get; set; }
public string UrlTokenAPI { get; set; }
}
}
In the next section I will show how to structure our integration tests using a familiar test pattern.
Arranging dependent Resources for our Tests
We will use the following test pattern for each test:
Arrange
Act
Assert
Where:
Arrange sets up our test with resource initializations and configuration.
Act runs our target resource (for example: method call, http request, database connection etc.)
Assert verifies the results of the action and provides a result (Pass or Fail).
With arranging each test we can either conduct this within each running method, or we can do this within the Setup() method. The reason why we arrange test resources within the setup method is that it is called before every Test method is called. This can save considerable time during a lengthy integration test run.
In our setup we will configure the following resources as follows:
Loading our configuration file into the application pipeline is done as follows:
config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("configuration.json")
.Build();
To retrieve the database connection and inject the connection to the application pipeline is done as follows:
services = new ServiceCollection();
connection = config.GetConnectionString("AppDbContext");
services.AddDbContext<ApplicationDbContext>(
options => options.UseSqlServer(connection)
);
To configure console and debug logging and inject logging instances into the application pipeline is done as follows:
services.AddLogging(config =>
{
config.AddConsole(opts =>
{
opts.IncludeScopes = true;
});
config.AddDebug();
});
To configure HTTP requests and inject them into the application pipeline is done as follows:
services.AddHttpClient();
Note: Unlike custom services, where we inject IHttpContextAccessor into the pipeline, declare the type in the target service constructor, and retrieve the context within our service using httpContextAccessor.HttpContext, in our test suite we do not need to use it here as we as calling the external web APIs using a non-web hosted test console application.
To add the IOptions configuration instance into our application pipeline and map our application settings keys from the configuration file into our AppConfiguration type is done as follows:
services.AddOptions();
services.Configure<AppConfiguration>(config.GetSection("AppSettings"));
The final part of our setup is to assign the local instance variables for our resources for application configuration, data context, http context, logging and event bus:
serviceProvider = services.BuildServiceProvider();
httpClientFactory = serviceProvider.GetService<IHttpClientFactory>();
appConfiguration = serviceProvider.GetService<IOptions<AppConfiguration>>();
dbctxt = serviceProvider.GetService<ApplicationDbContext>();
logger = serviceProvider.GetService<ILogger<BookService>>();
eventBus = serviceProvider.GetService<IEventBus>();
Our test class variables to hold both shared and unique to individual test methods is shown:
// Variables particular to methods.
private static ILogger<BookService> logger;
private IEventBus eventBus;
private IOptions<AppConfiguration> appConfiguration;
private IHttpClientFactory httpClientFactory;
private HttpContext context;
// Shared setup properties.
private string connection;
private DbContextOptionsBuilder ctxbld;
private ServiceCollection services;
private ApplicationDbContext dbctxt;
private ServiceProvider serviceProvider;
Testing custom data-dependent Services
The above setup makes our service class test quite compact:
[Test]
public async Task TestBookSelectQuery()
{
BookService esvc = new BookService(dbctxt, logger, eventBus);
List<BookViewModel> lst = await esvc.GetBooks();
Assert.IsTrue(lst.Count > 0);
}
The hard work we did to arrange the data makes testing services simpler.
An entire test run is shown below:
When the test is run, we can view the output as shown:
Testing HTTP REST based Web API Services
Testing HTTP web API services can take two forms:
- Testing unprotected web API methods.
- Testing protected web API methods.
I will cover 2) testing protected web API methods. From this you will be able to figure out 1). To test authentication, I will assume we are using JWT Open ID connect based security.
[Test]
public async Task TestAuthentication()
{
var userCredentials = new Configuration.UserCredentials()
{
UserName = "[a user name] ",
Password = "[a password]"
};
string authToken = String.Empty;
var tokenRequest = new HttpRequestMessage(
HttpMethod.Post,
appConfiguration.Value.UrlTokenAPI + "api/Users/Token");
var tokenClient = httpClientFactory.CreateClient();
Dictionary<string, string> keyvalues =
new Dictionary<string, string>();
keyvalues.Add("UserName", userCredentials.UserName);
keyvalues.Add("Password", userCredentials.Password);
var content = JsonConvert.SerializeObject(keyvalues);
StringContent stringContent =
new StringContent(content, System.Text.Encoding.UTF8, "application/json");
var tokenResponse = await tokenClient.PostAsync(tokenRequest.RequestUri, stringContent);
if (tokenResponse.IsSuccessStatusCode)
{
string respContent = tokenResponse.Content.ReadAsStringAsync().Result;
JObject tokenObjects = JsonConvert.DeserializeObject<JObject>(respContent);
foreach (JToken tokenObject in tokenObjects.Values())
{
if (tokenObject.Values("values") != null)
{
authToken = tokenObject.Value<string>("token").ToString();
break;
}
}
}
if (String.IsNullOrEmpty(authToken))
Assert.Fail("Authentication token cannot be obtained. Check credentials.");
else
Assert.Pass("Authentication token successfully obtained.");
lastResult = authToken;
}
The above method does the following:
- Creates a HttpRequestMessage() for the authentication token URL.
- Post the user credentials into the body when calling the authentication token URL.
- Extract the JWT token from the response.
- Test if the token was returned or not.
The last test I will show combines retrieval of the authentication token and testing a call to a HTTP Web API method.
[Test]
public async Task TestHttpGetWithAuthentication()
{
await TestAuthentication();
string authToken = lastResult;
if (String.IsNullOrEmpty(authToken))
Assert.Fail("Authentication token cannot be obtained. Check credentials.");
string appUri = appConfiguration.Value.UrlCatalogAPI + "api/Book/List";
var getRequest = new HttpRequestMessage(HttpMethod.Get, appUri);
var getClient = httpClientFactory.CreateClient();
if (!string.IsNullOrEmpty(authToken))
{
getClient.DefaultRequestHeaders.Accept.Clear();
getClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
getClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", authToken);
}
var getResponse = await getClient.SendAsync(getRequest);
List<BookViewModel> bookViews = new List<BookViewModel>();
if (getResponse.IsSuccessStatusCode)
{
string respContent = getResponse.Content.ReadAsStringAsync().Result;
JArray books = JArray.Parse(respContent);
foreach (var item in books)
{
BookViewModel bitem = JsonConvert.DeserializeObject<BookViewModel>(item.ToString());
bookViews.Add(bitem);
}
}
else
{
string errorString =
getResponse.Headers.WwwAuthenticate.ToString().Replace("Bearer", "");
if (!errorString.StartsWith("{") || !errorString.EndsWith("}"))
errorString = "{ " + errorString + " }";
errorString = errorString.Replace("=", ":");
ApiErrorResponse apiErrorResponse =
JsonConvert.DeserializeObject<ApiErrorResponse>(errorString);
Assert.Fail("Error calling API method. " + apiErrorResponse.ErrorDescription);
}
if (bookViews.Count == 0)
{
Assert.Fail("Books cannot be retrieved.");
}
else
{
Assert.Pass($"{bookViews.Count} books retrieved.");
}
}
The above method does the following:
- Retrieves a valid JWT token.
- Create a HttpRequestMessage() for the Book List Web API method.
- Create a HttpClient and add the bearer token to the default request headers.
- Submit an HTTP POST for the Book List Web API method with the token.
- Extract the data (as an array).
- Apply a test to the data.
After the above test is run, the detailed output is shown:
As you can see, it can take a fair amount of code to comprehensively integration test a system.
The main of integration testing is to selectively test parts of our system rather than test every combination like we would for achieving code coverage in unit tests.
That’s all for today’s post.
I hope you found this post useful and informative.
Andrew Halil is a blogger, author and software developer with expertise of many areas in the information technology industry including full-stack web and native cloud based development, test driven development and Devops.