I haven’t seen much about using the ASP.NET Core TestServer
and EF Core’s InMemoryDb
together, despite them being a natural fit, so I thought I’d blog about it. There are a few different ways you could achieve this (more options at the bottom of the post), this is just a simple solution.
tldr;
- Create a custom environment called Testing.
- In your Startup.cs, add a check to see if you’re in the Testing Environment and if so use the
InMemoryDb
provider. - In your
TestServer
setup, set the environment to Testing. - Grab the
DbContext
fromTestServer
viaserver.Host.Services.GetService(typeof(ApplicationDbContext)) as ApplicationDbContext
and add the data you want there. - You now have an in memory web server and an in memory database working together!
Quick look at final Test class:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class ApplicationUsersControllerGetApplicationUser | |
{ | |
private readonly ApplicationDbContext _context; | |
private readonly HttpClient _client; | |
public ApplicationUsersControllerGetApplicationUser() | |
{ | |
var builder = new WebHostBuilder() | |
.UseEnvironment("Testing") | |
.UseStartup<Startup>(); | |
var server = new TestServer(builder); | |
_context = server.Host.Services.GetService(typeof(ApplicationDbContext)) as ApplicationDbContext; | |
_client = server.CreateClient(); | |
} | |
[Fact] | |
public async Task DoesReturnNotFound_GivenUserDoesNotExist() | |
{ | |
// Act | |
var response = await _client.GetAsync($"/api/ApplicationUsers/abc"); // No users with ID abc | |
// Assert | |
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); | |
} | |
[Fact] | |
public async Task DoesReturnOk_GivenUserExists() | |
{ | |
// Arrange | |
var user = new ApplicationUser | |
{ | |
Id = "123", | |
Email = "test@test.com" | |
}; | |
_context.Users.Add(user); | |
_context.SaveChanges(); | |
// Act | |
var response = await _client.GetAsync($"/api/ApplicationUsers/{user.Id}"); | |
// Assert | |
Assert.Equal(HttpStatusCode.OK, response.StatusCode); | |
string jsonResult = await response.Content.ReadAsStringAsync(); | |
ApplicationUser userFromJson = JsonConvert.DeserializeObject<ApplicationUser>(jsonResult); | |
Assert.Equal(user.Id, userFromJson.Id); | |
Assert.Equal(user.Email, userFromJson.Email); | |
} | |
} |
Test Server
Unit tests test a small piece of the raw logic of your code in isolation of any web server, database, file system, etc. Instead of using a real database, you would use a fake or mock database.
Integration tests on the other hand, are a way of doing automated tests that integrate various components of your app together. It might use a combination of a real web server, real web service, real database, real browser, etc. ASP.NET Core gives us an easy way to write integration tests by way of an object called TestServer
.
TestServer
allows you to boot up an in memory web server with your full middleware pipeline, full set of dependencies registered, etc. This enables you to do things like hit a route and make sure the server returns the response you expect.
For more information on Test Server, checkout the official docs.
EF Core InMemory Database
EF Core has a provider that lets you run against an in memory database. The intent of this provider is purely for testing purposes.
This has a few benefits:
- The reads and writes are very fast.
- You don’t have to worry about having a real database to run your tests against.
- You don’t have to worry about cleaning up your data after your tests are done. When the tests stop, your in memory database goes away.
For more information on InMemory database, checkout the official docs.
Using these two together
By now you may be thinking. “Cool, so I can create an in memory web server and an in memory EF database…. can I combine the two?” The answer is most definitely, yes! I hadn’t seen much out on the Interwebs about this, so hence this blog post.
Let’s assume we have an API endpoint that looks like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Produces("application/json")] | |
[Route("api/ApplicationUsers")] | |
public class ApplicationUsersController : Controller | |
{ | |
private readonly ApplicationDbContext _context; | |
public ApplicationUsersController(ApplicationDbContext context) | |
{ | |
_context = context; | |
} | |
// GET: api/ApplicationUsers/5 | |
[HttpGet("{id}")] | |
public async Task<IActionResult> GetApplicationUser([FromRoute] string id) | |
{ | |
var applicationUser = await _context.Users.SingleOrDefaultAsync(m => m.Id == id); | |
if (applicationUser == null) | |
{ | |
return NotFound(); | |
} | |
return Ok(applicationUser); | |
} | |
} |
It’s pretty straight forward, we inject in a DbContext into the Controller (NOTE: DEMO CODE) and then have an endpoint that takes in an ID and will return the appropriate response based on what the database returns.
So what I’d like to do is swap out the real database here for the in memory database. Let’s do it!
First in your ASP.NET Core Application:
- In our Startup.cs, let’s inject an
IHostingEnvironment
in the constructor and save it to a field/property. - In our
ConfigureServices
, let’s check the Environment to see if the current environment is a custom one called “Testing.” - If so, then rig up the In Memory Database via
services.AddDbContext<ApplicationDbContext>(options => options.UseInMemoryDatabase("TestingDB"));
- Note: TestingDB is just a unique name given to your In Memory Database.
- So it should look like:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class Startup | |
{ | |
public IConfiguration Configuration { get; } | |
public IHostingEnvironment CurrentEnvironment { get; } | |
public Startup(IConfiguration configuration, IHostingEnvironment currentEnvironment) | |
{ | |
Configuration = configuration; | |
CurrentEnvironment = currentEnvironment; | |
} | |
public void ConfigureServices(IServiceCollection services) | |
{ | |
if (CurrentEnvironment.IsEnvironment("Testing")) | |
{ | |
services.AddDbContext<ApplicationDbContext>(options => | |
options.UseInMemoryDatabase("TestingDB")); | |
} | |
else | |
{ | |
services.AddDbContext<ApplicationDbContext>(options => | |
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); | |
} | |
// Everything else removed for brevity | |
} | |
// Configure Method removed for brevity | |
} |
Next in your Unit Testing project (I’m using xUnit, but you could use NUnit or MSTest):
1.Create a new Test Class
a. I follow the convention <ClassUnderTest><MethodUnderTest> for the class name.
b. In this case: ApplicationUsersControllerGetApplicationUser
2. Create a new constructor and set up a WebHostBuilder
with the following code. The two biggest pieces are line 9 where we set the Environment to Testing (to match line 14 above where we check the Environment), and line 13 where we grab the ApplicationDbContext from the ServiceProvider
and save that off.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class ApplicationUsersControllerGetApplicationUser | |
{ | |
private readonly ApplicationDbContext _context; | |
private readonly HttpClient _client; | |
public ApplicationUsersControllerGetApplicationUser() | |
{ | |
var builder = new WebHostBuilder() | |
.UseEnvironment("Testing") | |
.UseStartup<Startup>(); | |
var server = new TestServer(builder); | |
_context = server.Host.Services.GetService(typeof(ApplicationDbContext)) as ApplicationDbContext; | |
_client = server.CreateClient(); | |
} | |
} |
3. Create a new test method
a. I follow the convention Does<Something>_Given<Scenario>.
b. In this case: DoesReturnOk_GivenUserExists
4. Create a user, add it to the context, and save it. Then use the HttpClient we created and saved off above to query our endpoint and ask for the user’s ID.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Fact] | |
public async Task DoesReturnOk_GivenUserExists() | |
{ | |
// Arrange | |
var user = new ApplicationUser | |
{ | |
Id = "123", | |
Email = "test@test.com" | |
}; | |
_context.Users.Add(user); | |
_context.SaveChanges(); | |
// Act | |
var response = await _client.GetAsync($"/api/ApplicationUsers/{user.Id}"); | |
// Assert | |
Assert.Equal(HttpStatusCode.OK, response.StatusCode); | |
string jsonResult = await response.Content.ReadAsStringAsync(); | |
ApplicationUser userFromJson = JsonConvert.DeserializeObject<ApplicationUser>(jsonResult); | |
Assert.Equal(user.Id, userFromJson.Id); | |
Assert.Equal(user.Email, userFromJson.Email); | |
} |
That’s it! Now run your test and you will have successfully booted an in memory web server with an in memory database.
Now that we’ve tested the Ok path, we should probably test the NotFound path. That’s simple to do as well:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Fact] | |
public async Task DoesReturnNotFound_GivenUserDoesNotExist() | |
{ | |
// Act | |
var response = await _client.GetAsync($"/api/ApplicationUsers/abc"); // No users with ID abc | |
// Assert | |
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); | |
} |
The entire GitHub code can be found here: https://github.com/scottsauber/TestServerAndInMemoryDbDemo. The two interesting parts are the Startup and the Tests.
Some “Before You Copy + Paste” Caveats
- If you don’t like polluting your “Production” Startup class with Testing concerns (which is a very valid concern), you could create a Test only Startup class. Gérald has a great blog post on how to do this near the bottom of his post. On your
TestServer
then just swap out.UseStartup<Startup>()
with.UseStartup<YourTestStartupClassHere>()
. - In no way am I advocating for injecting in your DbContext directly into your Controller. This is purely just demo code and to remove layers to make it as easy to reason about as possible. Likely you will have at least one “layer” in between your Controller and your DbContext.
- Note that this will be doing a real integration test and I am ONLY swapping out the DB here. If you need to swap out other components (like external services), you will also have to do that yourself via the
- You will want to limit your creation of the TestServer and HttpClient, due to they are mildly expensive (~100-200ms on my machine), and we want our tests to be as fast as possible. If using xUnit, look into
IClassFixtures
.
Hope this helps!
[…] Using ASP.NET Core TestServer + EF Core In Memory DB Together – Sctorr Sauber […]
Nice and helpful. Thanks for sharing.
_context = server.Host.Services.GetService(typeof(ApplicationDbContext)) as ApplicationDbContext;
can be replaced with
_context = server.Host.Services.GetService()
Hey Paul, thanks for the comment! Glad it was useful.
Is some code missing from your comment? I can’t get that code snippet to compile. 🙁 I really hope that there’s a cleaner way to do it than what I had there, because that part was a little gross to type/look at.
Perhaps Paul ment this:
_context = server.Host.Services.GetService()
Could you use this Scott?
Sorry, the code is changed while commeting…
It should be the generic version, of <ApplicationDbContext>
_context = server.Host.Services.GetService[ApplicationDbContext]()
Hey Scott, great article by the way.
I have a question, do i need in my integration tests check if data is being saved to the database?
Hey Henrick – that’s really up to you if you want to go that far or not.
Going down that path will give you more confidence that everything is working as expected (and you don’t have like SQL permissions problems or something).
However, there is a maintenance cost to doing integration tests all the way to the database. Such as you have to make sure that DB is available, you usually have to get it to the correct state containing specific data for certain tests (which could make parallel test difficult), they’re usually slow, etc.
I typically don’t do integration tests that check if the data’s being saved to the database for the reasons mentioned above.
Hope that helps.
Hey Scott,
Nice post!
Now i’m in this situation, i want to check the data but i can’t access the dbContext in tests, do you know how to do this?
Thanks!
Hi, thanks for this very good article, I have this question, my dbcontext is not at the controller level but inside a business class, when I try to use your example, dbcontext is empty inside the class, any idea?
Hi Mauricio – do you have a small sample that reproduces the problem that you could post to GitHub and I’d be happy to take a look.
I also don’t use the DbContext at the Controller level when doing real stuff (which is why I called out DEMO CODE), and I have this working, so I’m sure I could figure out what your issue is.
Still use this setup today Scott thank you for the article!
Glad to hear it’s working Tahir! Thanks for letting me know.
Thanks, it is very useful!