Kirkbride Solutions

How To: Create Simple.Net Core API with Integration Tests

C#tutorial

Hello and welcome! I'm going to be doing a small mini-series on some ways to build a .Net Core API. This first one is going to involve setting up the API and creating a suite of Integration Tests. Generally if I'm only building the API I tend to have more coverage around Integration Testing then say Unit Testing (for better or worse)... These tests will be separate from any Unit Testing and are valuable when verifying that new features don't break your existing clients. They can also help give you a road map by doing some Test Driven Development (TDD). Lets jump into some code! The code is available on GitHub here. You can follow along yourself or just review the code some of the Commits are called out in this post for reference. First I'll open Visual Studio 2019 and hit the "Create a new project" button.

VS Create a new project

From there I'll choose ASP.NET Core Web Application.

VS project templates

I'll call it Conferences.Api and hit Create. After that I chose .Net Core and ASP.Net Core 2.2 (for now). The other settings are up to however you want to run/configure the solution. I'm using Docker and the API project template

You solution will look something like this:

VS solution tree

Lets add an xUnit Test Project, right click solution Add -> New Project:

VS Add xUnit project

I'm going to call it Xconferences.Api.IntegrationTests. We're almost to the code I promise… Just need to install a few (okay a lot) of NuGet packages to the testing solution. Including a Reference to our Api project. (You will need the package Microsoft.AspNetCore.HttpsPolicy if you kept the Https checkbox checked during project creation).

NuGet Package Depedency tree view

Commit: IntegrationTests Packages.

Back in the main API project I'm going to add a folder called Domain.

At this point we'll begin setting up our database. To make it easy we'll just use EntityFrameworkCore and Code First Migrations.

To do this we'll need our connection string. You can set this up however you normally would. In my demo project I'll just include it in the appsettings.json file to make it easy.

  {
"ConnectionStrings": {
"DefaultConnection": "Server=TESTDB\\SQLEXPRESS;Database=Conference;Trusted_Connection=True;MultipleActiveResultSets=true;"
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}

Next we'll start building our Data Context and Domain Models. Go ahead and create two Classes in the Domain Directory. We'll need a Conference.cs file and a ConferenceDataContext.cs file.

We're going to start building out the Conference "table". One tip if you're typing these properties yourself is to use the "prop" key word and hit the Tab key. This actually executes a code snippet you can use to quickly add properties.

public class Conference
{
public int Id { get; set; }

public string Name { get; set; }

public string State { get; set; }

public string City { get; set; }

public DateTime StartDate { get; set; }

public DateTime EndDate { get; set; }

public bool Attending { get; set; }

public bool Speaking { get; set; }

public Topic FocusTopic { get; set; }
}

public class Topic
{
public int Id { get; set; }

public string Name { get; set; }
}

You can tweak your objects to be more in line with what you may want to track about Conferences. This is just a really simple example. For example you may want to track all of sessions you plan to attend.

Lets setup our ConferenceDataContext by making it a DBContext. You accomplish this by adding a : and typing out DBContext. It'll throw red squiggles you can hit "ctrl+." and select "Using Microsoft.EntityFrameworkCore" to resolve the error.

Error

We need to make a change in our Statup.cs file in the method ConfigureServices.

services.AddDbContext<ConferenceDataContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

If there are any red squiggles hit them with the "Ctrl+." and import anything that is missing.

Open up the Package Manager Console (this is the old way I know…). This can be accomplished by going Tools -> NuGet Package Manager -> Package Manager Console. You can also accomplish this using the .Net Core Cli by following this link.

Make sure the Default Project is Conferences.Api and enter the command:

Add-migration InitialMigration

The command should execute. It will create a migrations file with Up and Down methods. This will show you the tables and how they will be created on the Database side. We need to "Update" our database to reflect the changes in the migration file. This can be accomplished using this command:

Update-database

You should now be able to see your new Database. This can be viewed however you prefer to view them. Via server explorer in Visual Studio or I tend to use SSMS for this. We'll just create some test data in the database. I'll start with Topics.

Topics Table

Then I'll create some Conferences.

Conference Table

So now we're going to use a little EntityFramework magic to make Integration Testing our API easier. What we're going to do is create an In Memory version of our database that we can manipulate to run our tests.

Let's make some changes to the Startup.cs

public class Startup
{
public Startup(IConfiguration configuration, IHostingEnvironment environment)
{
Configuration = configuration;
Environment = environment;
}

public IConfiguration Configuration { get; }
public IHostingEnvironment Environment { get; set; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

if (Environment.IsEnvironment("testing"))
{
services.AddDbContext<ConferenceDataContext>(options => options.UseInMemoryDatabase("test"));
}
else
{
services.AddDbContext<ConferenceDataContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
}

}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseMvc();
}
}

We've added an IHostingEnvironment property that we can use to manage which version of our context we want to use.

Cool! Let's write some tests. First we'll create a class in our IntegrationTests project called HttpBase.cs You can delete the existing UnitTest1.cs file or repurpose it.

using Conferences.Api;
using Conferences.Api.Domain;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;

namespace XConferences.Api.IntegrationTests
{
public class HttpBase
{
protected readonly HttpClient Client;
protected readonly ConferenceDataContext Context;

public HttpBase()
{
var builder = new WebHostBuilder()
.ConfigureAppConfiguration((hosting, config) =>
{

})
.UseEnvironment("testing")
.UseStartup<Startup>();

var server = new TestServer(builder);
Client = server.CreateClient();
Context = server.Host.Services.GetService(typeof(ConferenceDataContext)) as ConferenceDataContext;
}
}
}

So all of our test or spec files will inherit from the HttpBase. This will allow them to leverage the In Memory database as well as this default HttpClient.

Create a folder called ConferencesResource in the test project. Add a Class called GetConferences.cs. Make it a public class and make it inherit from HttpBase.

Let's write our first test. This one will be simple so we demonstrate some fundamental TDD.

using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Xunit;

namespace XConferences.Api.IntegrationTests.ConferencesResource
{
public class GetConferences : HttpBase
{
[Fact]
public async Task GetsAnOk()
{
var response = await Client.GetAsync("api/v1/conferences");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}
}

If you do not already have it open you'll probably want to open the Test Explorer. You can open it by going Test -> Windows -> Test Explorer. I tend to keep mine docked on the left hand side.

You'll want to build and then hit the Green Arrow to run your test! Now it'll fail obviously. That's what we want though.

Red Test

We have our "Red" part of our test done. Now let's make it Green! We'll create an API controller in our Conferences.Api project. You can delete the ValuesController or keep it. That's up to you. We'll add a file called ConferencesController.cs Just make it pass this test, something like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace Conferences.Api.Controllers
{
[Route("api/v1/[controller]")]
[ApiController]
public class ConferencesController : ControllerBase
{
[HttpGet("")]
public async Task<IActionResult> GetConferences()
{
return Ok();
}
}
}

So now if hit Green Arrow in Test Explorer we should get a "Green" successful test.

Green Test

That's awesome isn't it!? But also not very useful… So lets refactor our response and then write a better test!

Commit: Gets an Ok!

Our GetConferences resource should return conferences. So lets expand the controller to return a ConferencesResponse class. We'll do that by Creating a new folder called "Models" in our ConferencesApi project. We'll create a class called ConferencesResponse. This is the representation we will be providing to our clients. I'm going to be attempting to make this API pretty RESTful. In this instance I'll be adhering to some of the Hypertext Application Language (HAL) specifications. You can find some more information on HAL here

Here's the response class:

public class ConferencesResponse
{
[JsonProperty("_embedded")]
public List<ConferencesSummaryItem> Embedded { get; set; }
public string TopicFilter { get; set; }
public int Count { get; set; }
}

public class ConferencesSummaryItem
{
public int Id { get; set; }
public string Name { get; set; }
public string State { get; set; }
public string City { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public string FocusTopic { get; set; }
}

So this part isn't perfect TDD, but I'll use it to teach a little Visual Studio trick. Now that we have our response we can update the controller to return this response. I wouldn't recommend doing this inline in the controller like this. For this solution I'll be using Automapper shortly to refactor this, but we'll start here:

[Route("api/v1/[controller]")]
[ApiController]
public class ConferencesController : ControllerBase
{
ConferenceDataContext _context;

public ConferencesController(ConferenceDataContext context) => _context = context;

[HttpGet("")]
public async Task<IActionResult> GetConferences()
{
ConferencesResponse response = new ConferencesResponse();
try
{
response.Embedded = (from a in _context.Conferences.ToList()
join b in _context.Topics on a.FocusTopic.Id equals b.Id
select new ConferencesSummaryItem
{
Id = a.Id,
Name = a.Name,
State = a.State,
City = a.City,
StartDate = a.StartDate,
EndDate = a.EndDate,
FocusTopic = b.Name
}).ToList();
response.Count = response.Embedded.Count;
}
catch(Exception ex)
{
// do something here like log the error
return StatusCode(500);
}
return Ok(response);
}
}

So we can go run our solution. Using Docker or IIS Express. However, you run it you should see either a IP or localhost url that you can use to test the API. I'm calling mine using Postman (you can get it here). If Postman is freshly installed you might need to turn off SSL certification in settings by clicking on the little wrench.

Postman Settings

Postman

To make our testing easier you can actually copy the response you get back in Postman. Go into our GetConferences.cs put your cursor after the end of the class, but still in the namespace. Then go to Edit -> Paste Special -> Paste Json As Classes

VS Paste Special JSON

This is a really handy feature, and I believe worth skipping ahead a bit. We can use the classes created by doing this to validate our response in our next Integration Test!

So what we want to do now is actually create our In Memory Mock data and test that our API is able to return that data. We'll then use the classes we captured from the json to validate our response. Like this:

[Fact]
public async Task GetsConferences()
{
var topic1 = new Topic
{
Id = 1,
Name = "APIs"
};
var topic2 = new Topic
{
Id = 2,
Name = "Angular"
};
var conference1 = new Conference
{
Id = 1,
Name = "Test Conf",
State = "TX",
City = "Austin",
StartDate = DateTime.Today,
EndDate = DateTime.Today.AddDays(4),
Attending = true,
Speaking = true,
FocusTopic = topic1
};
var conference2 = new Conference
{
Id = 2,
Name = "S Conf",
State = "TX",
City = "Austin",
StartDate = DateTime.Today,
EndDate = DateTime.Today.AddDays(4),
Attending = true,
Speaking = false,
FocusTopic = topic1
};
var conference3 = new Conference
{
Id = 3,
Name = "Code Conf",
State = "GA",
City = "Atlanta",
StartDate = DateTime.Today,
EndDate = DateTime.Today.AddDays(3),
Attending = false,
Speaking = false,
FocusTopic = topic2
};
Context.Topics.Add(topic1);
Context.Topics.Add(topic2);
Context.Conferences.Add(conference1);
Context.Conferences.Add(conference2);
Context.Conferences.Add(conference3);
await Context.SaveChangesAsync();

var response = await Client.GetAsync("api/v1/conferences");
var conferences = await response.Content.ReadAsAsync<conferencesResponse>();

Assert.Equal(3, conferences.count);
Assert.Equal("Test Conf", conferences._embedded[0].name);
Assert.Equal("S Conf", conferences._embedded[1].name);
Assert.Equal("Code Conf", conferences._embedded[2].name);
// feel free to validate any other fields as well
}

So we have this test to verify that we can Get Conferences back, and that we get back what we expect. I'm going to pullout the mock data setup into its own method. I basically move it to the bottom and collapse the code.

private async void SetupMockData()
{
var topic1 = new Topic
{
Id = 1,
Name = "APIs"
};
var topic2 = new Topic
{
Id = 2,
Name = "Angular"
};
var conference1 = new Conference
{
Id = 1,
Name = "Test Conf",
State = "TX",
City = "Austin",
StartDate = DateTime.Today,
EndDate = DateTime.Today.AddDays(4),
Attending = true,
Speaking = true,
FocusTopic = topic1
};
var conference2 = new Conference
{
Id = 2,
Name = "S Conf",
State = "TX",
City = "Austin",
StartDate = DateTime.Today,
EndDate = DateTime.Today.AddDays(4),
Attending = true,
Speaking = false,
FocusTopic = topic1
};
var conference3 = new Conference
{
Id = 3,
Name = "Code Conf",
State = "GA",
City = "Atlanta",
StartDate = DateTime.Today,
EndDate = DateTime.Today.AddDays(3),
Attending = false,
Speaking = false,
FocusTopic = topic2
};
Context.Topics.Add(topic1);
Context.Topics.Add(topic2);
Context.Conferences.Add(conference1);
Context.Conferences.Add(conference2);
Context.Conferences.Add(conference3);
await Context.SaveChangesAsync();
}

Lets rerun our tests just to make sure we still look good.

[Fact]
public async Task GetsFilteredConferences()
{
SetupMockData();

var response = await Client.GetAsync("api/v1/conferences?topic=APIs");
var conferences = await response.Content.ReadAsAsync<conferencesResponse>();

Assert.Equal(2, conferences.count);
Assert.Equal("Test Conf", conferences._embedded[0].name);
Assert.Equal("S Conf", conferences._embedded[1].name);
Assert.Equal("APIs", conferences.topicFilter);
}

VS Red and Green Tests

So we have our Red. Time to make it pass. We'll need to make some changes to the Controller:

[HttpGet("")]
public async Task<IActionResult> GetConferences([FromQuery] string topic)
{
ConferencesResponse response = new ConferencesResponse();
try
{
response.Embedded = (from a in _context.Conferences.ToList()
join b in _context.Topics on a.FocusTopic.Id equals b.Id
select new ConferencesSummaryItem
{
Id = a.Id,
Name = a.Name,
State = a.State,
City = a.City,
StartDate = a.StartDate,
EndDate = a.EndDate,
FocusTopic = b.Name
}).ToList();
if (!String.IsNullOrEmpty(topic))
{
response.Embedded = response.Embedded.Where(p => p.FocusTopic == topic).ToList();
response.TopicFilter = topic;
}
response.Count = response.Embedded.Count;
return Ok(response);
}
catch(Exception ex)
{
// do something here like log the error
return StatusCode(500);
}
}

So this will make it Green, but we'll want to do a lot of refactoring.. It's time to go in and setup automapper. We want to pull our logic out of the controller.

VS All Green Tests

Go to your NuGet Packages and install AutoMapper for the Conferences.Api project. Once it's added we'll need to configure some files. Create a new folder called Configuration. Add a new class called ConferencesProfile.cs

using AutoMapper;
using Conferences.Api.Domain;
using Conferences.Api.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Conferences.Api.Configuration
{
public class ConferencesProfile : Profile
{
public ConferencesProfile()
{
CreateMap<Conference, ConferencesSummaryItem="">()
.ForMember(dest => dest.FocusTopic, opt => opt.MapFrom(s => s.FocusTopic.Name));

}
}
}

We'll also want to update Startup.cs to include some code needed by AutoMapper. Underneath our Context handling code in ConfigureServices add:

MapperConfiguration config = new MapperConfiguration(cfg =>
{
cfg.AddProfile(new ConferencesProfile());
}
IMapper mapper = config.CreateMapper();
services.AddSingleton(mapper);
services.AddSingleton(config);

We'll now create another folder called Mapper. Where we'll want to create our EfConferenceMap.cs class and also our IMapConferences.cs interface. First create the EfConferenceMap.cs class like below:

using AutoMapper;
using AutoMapper.QueryableExtensions;
using Conferences.Api.Domain;
using Conferences.Api.Models;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Conferences.Api.Mapper
{
public class EfConferenceMap
{
ConferenceDataContext _context;
IMapper _mapper;
MapperConfiguration _config;

public EfConferenceMap(ConferenceDataContext context, IMapper mapper, MapperConfiguration config)
{
_context = context;
_mapper = mapper;
_config = config;
}

public async Task<ConferencesResponse> GetAllConferences(string topic)
{
var query = GetConferences();

if (topic != null)
{
query = query.Where(c => c.FocusTopic.Name == topic);
}
var response = new ConferencesResponse
{
Embedded = await query
.ProjectTo<ConferencesSummaryItem>(_config)
.ToListAsync()
};
response.Count = response.Embedded.Count;
response.TopicFilter = topic;

return response;
}

private IQueryable<Conference> GetConferences()
{
return _context.Conferences;
}
}
}

If you select the "public class" delcaration and hit "Ctrl+." you should see an option in the Quick Options menu called "Extract Interface…". Select that and fill out the Extract Interface form that pops up.

VS Extract Interface

Hit Ok. You should get something like this created:

using System.Threading.Tasks;
using Conferences.Api.Models;

namespace Conferences.Api.Mapper
{
public interface IMapConferences
{
Task<ConferencesResponse> GetAllConferences(string topic);
}
}

Back in our Statup.cs file we'll want to setup our IMapEmployees for Dependency Injection. Like this:

services.AddScoped<IMapConferences, EfConferenceMap>();

We can finally Refactor our controller to use our new AutoMapper functionality to return our response like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Conferences.Api.Domain;
using Conferences.Api.Mapper;
using Conferences.Api.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace Conferences.Api.Controllers
{
[Route("api/v1/[controller]")]
[ApiController]
public class ConferencesController : ControllerBase
{
private IMapConferences _conferenceMapper;

public ConferencesController(IMapConferences conferenceMapper) => _conferenceMapper = conferenceMapper;

[HttpGet("")]
public async Task<IActionResult> GetConferences([FromQuery] string topic)
{
try
{
ConferencesResponse response = await _conferenceMapper.GetAllConferences(topic);
return Ok(response);
}
catch(Exception ex)
{
// do something here like log the error
return StatusCode(500);
}
}
}
}

Conveniently we have our integration tests we can run to make sure everything still works as it did before all that refactoring! Go ahead and execute all the tests.

VS All Green Tests

Feel free to do some manual verification using Postman as well.

Postman Testing

This api will be updated over time. Some of the changes will be discussed in other blog posts. Feel free to flesh out your version of the API with new and interesting features!

The final commit for this post on the Github repo is First Post Final