Testing REST APIs in some cases is tricky, because the role of some endpoints is just to pass data around or transform it slightly, so Mock/Unit tests don’t necessarily make sense sometimes. Here we’ll look at a way to set up an in-memory RavenDB document store, start up our API pointing to our in-memory document store, then in the test, add data directly to our database, then interact with it via our API, and finally check the data’s state in our document store.
XUnit is the testing framework preferred by Microsoft, and comes with a lot of utils. First, let’s install the nuget packages we’ll be using.
XUnit and the runners we’ll be using
Install-Package xunit Install-Package xunit.runner.console Install-Package xunit.runner.visualstudio Install-Package Microsoft.NET.Test.Sdk
Helpers and the Moq framework
Install-Package Microsoft.AspNetCore.Mvc.Testing Install-Package Microsoft.TestPlatform.TestHost Install-Package Microsoft.Extensions.Configuration.EnvironmentVariables Install-Package Moq
The RavenDB Test Driver and in-memory store
Install-Package RavenDB.TestDriver
Logging utils
Install-Package Divergic.Logging.Xunit Install-Package tsh.xunit.logging
Code coverage collector
Install-Package coverlet.collector
Our API likely has authentication. In this case we’ll explore Cookie authentication, but Bearer authentication should work the same. We’ll define our Mock Autehntication handler that will replace our Cookie scheme. Here you can set all the claims your application expects of a fully logged in user.
public class MockAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> { public MockAuthenticationHandler( IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock ) : base(options, logger, encoder, clock) { } protected override Task<AuthenticateResult> HandleAuthenticateAsync() { var claims = new[] { new Claim(JwtClaimTypes.Subject, "ApplicationUsers/1-A"), new Claim(JwtClaimTypes.Name, "Test User"), new Claim(JwtClaimTypes.Email, "[email protected]") }; var identity = new ClaimsIdentity(claims, "Test"); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, "Test"); return Task.FromResult(AuthenticateResult.Success(ticket)); } } public class MockSchemeProvider : AuthenticationSchemeProvider { public MockSchemeProvider(IOptions<AuthenticationOptions> options) : base(options) { } protected MockSchemeProvider( IOptions<AuthenticationOptions> options, IDictionary<string, AuthenticationScheme> schemes ) : base(options, schemes) { } public override Task<AuthenticationScheme> GetSchemeAsync(string name) { if (name == "Cookies") { var scheme = new AuthenticationScheme( "Cookies", "Cookies", typeof(MockAuthenticationHandler) ); return Task.FromResult(scheme); } return base.GetSchemeAsync(name); } }
Now we’re ready to set up our application host for testing. We’ll extend WebApplicationFactory
with our own class where we can configure defaults for all tests
public class MyWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class { protected override void ConfigureWebHost(IWebHostBuilder hostBuilder) { // add any configuration items our application expects var myConfiguration = new Dictionary<string, string?> { {"LogStorage:Loki:Url", "https://test"}, {"LogStorage:AzureStorage", " UseDevelopmentStorage=true;DevelopmentStorageProxyUri=https://127.0.0.1"}, {"Authentication:Authority", "https://test" }, {"Authentication:ClientId", "test" }, {"Authentication:ClientSecret", "test" }, {"Redis", null } }; hostBuilder.ConfigureAppConfiguration((ctx, builder) => builder.AddInMemoryCollection(myConfiguration)); // name our environment Test hostBuilder.UseEnvironment("Test") .ConfigureServices(services => { }) .ConfigureTestServices(services => { // Remove services that might interfere with our testing services.RemoveAll(typeof(IHealthCheck)); // Inject our Cookie scheme provider, which will take precedence over the one configured in our Startup.cs services.AddTransient<IAuthenticationSchemeProvider, MockSchemeProvider>(); }) .ConfigureLogging(logging => { logging.ClearProviders(); logging.SetMinimumLevel(LogLevel.Trace); }); base.ConfigureWebHost(hostBuilder); } }
Now we’re ready to move on to our tests. We’ll need to inherit from RavenTestDriver
and get MyWebApplicationFactory
as a class fixture.
The class fixture injects our web application factory via the constructor. The GetDocumentStore method comes from the RavenTestDriver and gets us a client to a newly created in memory database, and we’ll override the config to point to that instance.
public class AccountControllerTests : RavenTestDriver, IClassFixture<MyWebApplicationFactory<Startup>> { private readonly HttpClient _client; private IDocumentStore _store; public AccountControllerTests(MyWebApplicationFactory<Startup> factory) { _store = GetDocumentStore(); var myConfiguration = new Dictionary<string, string?> { {"Raven:Urls:0", _store.Urls[0] }, {"Raven:DatabaseName", _store.Database } }; _client = factory.WithWebHostBuilder(builder => { builder.ConfigureAppConfiguration((ctx, builder) => builder.AddInMemoryCollection(myConfiguration)); }).CreateClient(); } ... }
I find it useful to get some more feedback in tests, so I typically inject an ITestOutputHelper
and add it as a logging provider in the API. This way I can capture log statements made during testing, and I can write output during the test if something goes wrong. So our class becomes
public class AccountControllerTests : RavenTestDriver, IClassFixture<MyWebApplicationFactory<Startup>> { private readonly ITestOutputHelper _output; private readonly HttpClient _client; private IDocumentStore _store; public AccountControllerTests(MyWebApplicationFactory<Startup> factory, ITestOutputHelper output) { _output = output; _store = GetDocumentStore(); var myConfiguration = new Dictionary<string, string?> { {"Raven:Urls:0", _store.Urls[0] }, {"Raven:DatabaseName", _store.Database } }; _client = factory.WithWebHostBuilder(builder => { builder.ConfigureLogging(lb => lb.AddProvider(new XUnitLoggerProvider(output))); builder.ConfigureAppConfiguration((ctx, builder) => builder.AddInMemoryCollection(myConfiguration)); }).CreateClient(); }
Since the API I’m testing uses IHttpClientFactory
to make outbound calls, I also set up for that, but this is optional, but I find it to be very useful
public class AccountControllerTests : RavenTestDriver, IClassFixture<MyWebApplicationFactory<Startup>> { private readonly ITestOutputHelper _output; private readonly HttpClient _client; private IDocumentStore _store; public AccountControllerTests(MyWebApplicationFactory<Startup> factory, ITestOutputHelper output) { _output = output; _store = GetDocumentStore(); var myConfiguration = new Dictionary<string, string?> { {"Raven:Urls:0", _store.Urls[0] }, {"Raven:DatabaseName", _store.Database } }; var mockClientFactory = new Mock<IHttpClientFactory>(); var handlerMock = new Mock<HttpMessageHandler>(); handlerMock .Protected() // Setup the PROTECTED method to mock .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()) // prepare the expected response of the mocked http call .ReturnsAsync(new HttpResponseMessage() { StatusCode = HttpStatusCode.OK }) .Verifiable(); // use real http client with mocked handler here var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri("https://scatteredcode.net/"), }; mockClientFactory.Setup(t => t.CreateClient(It.IsAny<string>())).Returns(httpClient); _client = factory.WithWebHostBuilder(builder => { builder.ConfigureLogging(lb => lb.AddProvider(new XUnitLoggerProvider(output))); builder.ConfigureAppConfiguration((ctx, builder) => builder.AddInMemoryCollection(myConfiguration)); builder.ConfigureServices(services => { services.AddSingleton(mockClientFactory.Object); }); }).CreateClient(); }
Now we have everything we need to write our tests
For reference, this is the API method we’ll be testing
[Route("whoami")] public async Task<IActionResult> Whoami(CancellationToken ct = default) { using (var session = _store.OpenAsyncSession()) { var userId = User.FindFirstValue("sub"); var user = await session.LoadAsync<ApplicationUser>(userId, ct); return Ok(new WhoAmIModel { FirstName = user.FirstName, LastName = user.LastName, Email = user.Email, Role = user.Role }); } }
First we’ll add the data we expect to find in the database
var user = new ApplicationUser { Id = "Users/1", AccountStatus = "Active", FirstName = "First Name", LastName = "Last Name", Email = "[email protected]" }; var team = new Team { Id = "Teams/Team1", Name = "Team 1" }; using (var session = _store.OpenAsyncSession()) { await session.StoreAsync(user); await session.StoreAsync(team); await session.SaveChangesAsync(); }
Make a call to our API method expecting to receive this data
var response = await _client.GetAsync("whoami"); response.EnsureSuccessStatusCode(); var userInfo = await response.Content.ReadFromJsonAsync<WhoAmIModel>();
Assert our results
Assert.NotNull(userInfo); Assert.Equal(user.FirstName, userInfo.FirstName); Assert.Equal(user.LastName, userInfo.LastName); Assert.Equal(user.Email, userInfo.Email); Assert.Equal(user.Role, userInfo.Role);
Our full test
[Fact(DisplayName = "Who am i returns correct user info")] public async Task WhoAmI() { var user = new ApplicationUser { Id = "Users/1", AccountStatus = "Active", FirstName = "First Name", LastName = "Last Name", Email = "[email protected]" }; var team = new Team { Id = "Teams/Team1", Name = "Team 1" }; using (var session = _store.OpenAsyncSession()) { await session.StoreAsync(user); await session.StoreAsync(team); await session.SaveChangesAsync(); } var response = await _client.GetAsync("whoami"); response.EnsureSuccessStatusCode(); var userInfo = await response.Content.ReadFromJsonAsync<WhoAmIModel>(); Assert.NotNull(userInfo); Assert.Equal(user.FirstName, userInfo.FirstName); Assert.Equal(user.LastName, userInfo.LastName); Assert.Equal(user.Email, userInfo.Email); Assert.Equal(user.Role, userInfo.Role); }
Quick Links
Legal Stuff