Mocking Entity Framework when Unit Testing ASP.NET Web API 2
This guidance and application demonstrate how to create unit tests for your Web API 2 application that uses the Entity Framework. It shows how to modify the scaffolded controller to enable passing a context object for testing, and how to create test objects that work with Entity Framework.
For an introduction to unit testing with ASP.NET Web API, see Unit Testing with ASP.NET Web API 2.
This tutorial assumes you are familiar with the basic concepts of ASP.NET Web API. For an introductory tutorial, see Getting Started with ASP.NET Web API 2.
Software versions used in the tutorial
- Visual Studio 2017
- Web API 2
In this topic
This topic contains the following sections:
- Prerequisites
- Download code
- Create application with unit test project
- Create the model class
- Add the controller
- Add dependency injection
- Install NuGet packages in test project
- Create test context
- Create tests
- Run tests
If you have already completed the steps in Unit Testing with ASP.NET Web API 2, you can skip to the section Add the controller.
Visual Studio 2017 Community, Professional or Enterprise edition
Download the completed project. The downloadable project includes unit test code for this topic and for the Unit Testing ASP.NET Web API 2 topic.
## Create application with unit test project
You can either create a unit test project when creating your application or add a unit test project to an existing application. This tutorial shows creating a unit test project when creating the application.
Create a new ASP.NET Web Application named StoreApp.
In the New ASP.NET Project windows, select the Empty template and add folders and core references for Web API. Select the Add unit tests option. The unit test project is automatically named StoreApp.Tests. You can keep this name.
After creating the application, you will see it contains two projects - StoreApp and StoreApp.Tests.
In your StoreApp project, add a class file to the Models folder named Product.cs. Replace the contents of the file with the following code.
[!code-csharpMain]
1: using System;
2:
3: namespace StoreApp.Models
4: {
5: public class Product
6: {
7: public int Id { get; set; }
8: public string Name { get; set; }
9: public decimal Price { get; set; }
10: }
11: }
Build the solution.
Right-click the Controllers folder and select Add and New Scaffolded Item. Select Web API 2 Controller with actions, using Entity Framework.
Set the following values:
- Controller name: ProductController
- Model class: Product
- Data context class: [Select New data context button which fills in the values seen below]
Click Add to create the controller with automatically-generated code. The code includes methods for creating, retrieving, updating and deleting instances of the Product class. The following code shows the method for add a Product. Notice that the method returns an instance of IHttpActionResult.
[!code-csharpMain]
1: // POST api/Product
2: [ResponseType(typeof(Product))]
3: public IHttpActionResult PostProduct(Product product)
4: {
5: if (!ModelState.IsValid)
6: {
7: return BadRequest(ModelState);
8: }
9:
10: db.Products.Add(product);
11: db.SaveChanges();
12:
13: return CreatedAtRoute("DefaultApi", new { id = product.Id }, product);
14: }
IHttpActionResult is one of the new features in Web API 2, and it simplifies unit test development.
In the next section, you will customize the generated code to facilitate passing test objects to the controller.
Currently, the ProductController class is hard-coded to use an instance of the StoreAppContext class. You will use a pattern called dependency injection to modify your application and remove that hard-coded dependency. By breaking this dependency, you can pass in a mock object when testing.
Right-click the Models folder, and add a new interface named IStoreAppContext.
Replace the code with the following code.
[!code-csharpMain]
1: using System;
2: using System.Data.Entity;
3:
4: namespace StoreApp.Models
5: {
6: public interface IStoreAppContext : IDisposable
7: {
8: DbSet<Product> Products { get; }
9: int SaveChanges();
10: void MarkAsModified(Product item);
11: }
12: }
Open the StoreAppContext.cs file and make the following highlighted changes. The important changes to note are:
- StoreAppContext class implements IStoreAppContext interface
- MarkAsModified method is implemented
[!code-csharpMain]
1: using System;
2: using System.Data.Entity;
3:
4: namespace StoreApp.Models
5: {
6: public class StoreAppContext : DbContext, IStoreAppContext
7: {
8: public StoreAppContext() : base("name=StoreAppContext")
9: {
10: }
11:
12: public DbSet<Product> Products { get; set; }
13:
14: public void MarkAsModified(Product item)
15: {
16: Entry(item).State = EntityState.Modified;
17: }
18: }
19: }
Open the ProductController.cs file. Change the existing code to match the highlighted code. These changes break the dependency on StoreAppContext and enable other classes to pass in a different object for the context class. This change will enable you to pass in a test context during unit tests.
[!code-csharpMain]
1: public class ProductController : ApiController
2: {
3: // modify the type of the db field
4: private IStoreAppContext db = new StoreAppContext();
5:
6: // add these contructors
7: public ProductController() { }
8:
9: public ProductController(IStoreAppContext context)
10: {
11: db = context;
12: }
13: // rest of class not shown
14: }
There is one more change you must make in ProductController. In the PutProduct method, replace the line that sets the entity state to modified with a call to the MarkAsModified method.
[!code-csharpMain]
1: // PUT api/Product/5
2: public IHttpActionResult PutProduct(int id, Product product)
3: {
4: if (!ModelState.IsValid)
5: {
6: return BadRequest(ModelState);
7: }
8:
9: if (id != product.Id)
10: {
11: return BadRequest();
12: }
13:
14: //db.Entry(product).State = EntityState.Modified;
15: db.MarkAsModified(product);
16:
17: // rest of method not shown
18: }
Build the solution.
You are now ready to set up the test project.
## Install NuGet packages in test project
When you use the Empty template to create an application, the unit test project (StoreApp.Tests) does not include any installed NuGet packages. Other templates, such as the Web API template, include some NuGet packages in the unit test project. For this tutorial, you must include the Entity Framework packge and the Microsoft ASP.NET Web API 2 Core package to the test project.
Right-click the StoreApp.Tests project and select Manage NuGet Packages. You must select the StoreApp.Tests project to add the packages to that project.
From the Online packages, find and install the EntityFramework package (version 6.0 or later). If it appears that the EntityFramework package is already installed, you may have selected the StoreApp project instead of the the StoreApp.Tests project.
Find and install Microsoft ASP.NET Web API 2 Core package.
Close the Manage NuGet Packages window.
Add a class named TestDbSet to the test project. This class serves as the base class for your test data set. Replace the code with the following code.
[!code-csharpMain]
1: using System;
2: using System.Collections.Generic;
3: using System.Collections.ObjectModel;
4: using System.Data.Entity;
5: using System.Linq;
6:
7: namespace StoreApp.Tests
8: {
9: public class TestDbSet<T> : DbSet<T>, IQueryable, IEnumerable<T>
10: where T : class
11: {
12: ObservableCollection<T> _data;
13: IQueryable _query;
14:
15: public TestDbSet()
16: {
17: _data = new ObservableCollection<T>();
18: _query = _data.AsQueryable();
19: }
20:
21: public override T Add(T item)
22: {
23: _data.Add(item);
24: return item;
25: }
26:
27: public override T Remove(T item)
28: {
29: _data.Remove(item);
30: return item;
31: }
32:
33: public override T Attach(T item)
34: {
35: _data.Add(item);
36: return item;
37: }
38:
39: public override T Create()
40: {
41: return Activator.CreateInstance<T>();
42: }
43:
44: public override TDerivedEntity Create<TDerivedEntity>()
45: {
46: return Activator.CreateInstance<TDerivedEntity>();
47: }
48:
49: public override ObservableCollection<T> Local
50: {
51: get { return new ObservableCollection<T>(_data); }
52: }
53:
54: Type IQueryable.ElementType
55: {
56: get { return _query.ElementType; }
57: }
58:
59: System.Linq.Expressions.Expression IQueryable.Expression
60: {
61: get { return _query.Expression; }
62: }
63:
64: IQueryProvider IQueryable.Provider
65: {
66: get { return _query.Provider; }
67: }
68:
69: System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
70: {
71: return _data.GetEnumerator();
72: }
73:
74: IEnumerator<T> IEnumerable<T>.GetEnumerator()
75: {
76: return _data.GetEnumerator();
77: }
78: }
79: }
Add a class named TestProductDbSet to the test project which contains the following code.
[!code-csharpMain]
1: using System;
2: using System.Linq;
3: using StoreApp.Models;
4:
5: namespace StoreApp.Tests
6: {
7: class TestProductDbSet : TestDbSet<Product>
8: {
9: public override Product Find(params object[] keyValues)
10: {
11: return this.SingleOrDefault(product => product.Id == (int)keyValues.Single());
12: }
13: }
14: }
Add a class named TestStoreAppContext and replace the existing code with the following code.
[!code-csharpMain]
1: using System;
2: using System.Data.Entity;
3: using StoreApp.Models;
4:
5: namespace StoreApp.Tests
6: {
7: public class TestStoreAppContext : IStoreAppContext
8: {
9: public TestStoreAppContext()
10: {
11: this.Products = new TestProductDbSet();
12: }
13:
14: public DbSet<Product> Products { get; set; }
15:
16: public int SaveChanges()
17: {
18: return 0;
19: }
20:
21: public void MarkAsModified(Product item) { }
22: public void Dispose() { }
23: }
24: }
By default, your test project includes an empty test file named UnitTest1.cs. This file shows the attributes you use to create test methods. For this tutorial, you can delete this file because you will add a new test class.
Add a class named TestProductController to the test project. Replace the code with the following code.
[!code-csharpMain]
1: using System;
2: using Microsoft.VisualStudio.TestTools.UnitTesting;
3: using System.Web.Http.Results;
4: using System.Net;
5: using StoreApp.Models;
6: using StoreApp.Controllers;
7:
8: namespace StoreApp.Tests
9: {
10: [TestClass]
11: public class TestProductController
12: {
13: [TestMethod]
14: public void PostProduct_ShouldReturnSameProduct()
15: {
16: var controller = new ProductController(new TestStoreAppContext());
17:
18: var item = GetDemoProduct();
19:
20: var result =
21: controller.PostProduct(item) as CreatedAtRouteNegotiatedContentResult<Product>;
22:
23: Assert.IsNotNull(result);
24: Assert.AreEqual(result.RouteName, "DefaultApi");
25: Assert.AreEqual(result.RouteValues["id"], result.Content.Id);
26: Assert.AreEqual(result.Content.Name, item.Name);
27: }
28:
29: [TestMethod]
30: public void PutProduct_ShouldReturnStatusCode()
31: {
32: var controller = new ProductController(new TestStoreAppContext());
33:
34: var item = GetDemoProduct();
35:
36: var result = controller.PutProduct(item.Id, item) as StatusCodeResult;
37: Assert.IsNotNull(result);
38: Assert.IsInstanceOfType(result, typeof(StatusCodeResult));
39: Assert.AreEqual(HttpStatusCode.NoContent, result.StatusCode);
40: }
41:
42: [TestMethod]
43: public void PutProduct_ShouldFail_WhenDifferentID()
44: {
45: var controller = new ProductController(new TestStoreAppContext());
46:
47: var badresult = controller.PutProduct(999, GetDemoProduct());
48: Assert.IsInstanceOfType(badresult, typeof(BadRequestResult));
49: }
50:
51: [TestMethod]
52: public void GetProduct_ShouldReturnProductWithSameID()
53: {
54: var context = new TestStoreAppContext();
55: context.Products.Add(GetDemoProduct());
56:
57: var controller = new ProductController(context);
58: var result = controller.GetProduct(3) as OkNegotiatedContentResult<Product>;
59:
60: Assert.IsNotNull(result);
61: Assert.AreEqual(3, result.Content.Id);
62: }
63:
64: [TestMethod]
65: public void GetProducts_ShouldReturnAllProducts()
66: {
67: var context = new TestStoreAppContext();
68: context.Products.Add(new Product { Id = 1, Name = "Demo1", Price = 20 });
69: context.Products.Add(new Product { Id = 2, Name = "Demo2", Price = 30 });
70: context.Products.Add(new Product { Id = 3, Name = "Demo3", Price = 40 });
71:
72: var controller = new ProductController(context);
73: var result = controller.GetProducts() as TestProductDbSet;
74:
75: Assert.IsNotNull(result);
76: Assert.AreEqual(3, result.Local.Count);
77: }
78:
79: [TestMethod]
80: public void DeleteProduct_ShouldReturnOK()
81: {
82: var context = new TestStoreAppContext();
83: var item = GetDemoProduct();
84: context.Products.Add(item);
85:
86: var controller = new ProductController(context);
87: var result = controller.DeleteProduct(3) as OkNegotiatedContentResult<Product>;
88:
89: Assert.IsNotNull(result);
90: Assert.AreEqual(item.Id, result.Content.Id);
91: }
92:
93: Product GetDemoProduct()
94: {
95: return new Product() { Id = 3, Name = "Demo name", Price = 5 };
96: }
97: }
98: }
You are now ready to run the tests. All of the method that are marked with the TestMethod attribute will be tested. From the Test menu item, run the tests.
Open the Test Explorer window, and notice the results of the tests.
|