What Will I Learn?
The benefits of unit testing to software development cannot be over emphasized, from the beginning stages to the final stages of the life cycle of a project, unit testing will help detect faults and prevent unnecessary extra hours trying to debug and fix defects in your software . In the course of this tutorial you will learn the following,
- You will learn how to create unit test for you web api 2 application.
- You will learn how to modify scaffolded controllers to support dependency injection and allow for passing of context objects to enable testing.
- You will learn how to mock entityframwork for unit testing.
Requirements
Some requirements include.
- knowledge of ASP.NET Web API
- knowledge of EntityFramework6
- Visual studio 2013 and above
- Familiarity with Nuget packages
Difficulty
- Intermediate
Tutorial Contents
This tutorial will be made up of two parts, part one which involves creating our web api 2 application and part two which focuses on writing our unit test.
Part 1(Web Api 2 Application)
Creating our Application
Launch Visual studio, create a new ASP.Net Web Api application named Book_UnitTestApp. (I chose this name to capture what we are learning, it can be anything).
In the New Asp.Net Application - Book_UnitTestApp window, select the "empty template", in the section to add folders and core references, tick "Web Api" and finally tick the add unit test check box then click "ok" to proceed.
At this point, your solution should contain two projects, the Book_UnitTestApp and the Book_UnitTestApp.Test project, lets proceed.
Creating our Model Class
Add the following class to the model folder of the Book_UnitTestApp project as shown in the code snippet below.
public class Book
{
public int Id { get; set; }
public string Name { get; set; }
public string Author { get; set; }
}
Add Controller
Right click on the controller folder in our Book_UnitTestApp project, click on "Add" then select "Add New Scaffolded Item". Choose Web API 2 Controller with actions, using Entity Framework as shown in the screen shot below then click "Add".
Set the following values then click '"Add" to create our controller.
- Controller name - BooksController
- Model class - Book
- Data context class: [Select the add button which fills in the values to the context]
The controller which is automatically generated contains all CRUD methods required to manipulate the Book Class.
Enabling Dependency Injection
At this point, the automatically generated code only allows the BooksController class use an Instance of the Book_UnitTestAppContext class which will not allow us pass mock data when testing. Using dependency injection, we will modify our application to let BooksController class have access to an instance of an interface called IBook_UnitTestAppContext which will help us pass mock data for testing.
Right click the model folder and add an interface named IBook_UnitTestAppContext that inherits from the IDisposable interface as shown in the code snippet below.
public interface IBook_UnitTestAppContext : IDisposable
{
DbSet<Book> Books { get; }
int SaveChanges();
void MarkAsModified(Book item);
}
Open the automatically generated context class (Book_UnitTestAppContext.cs) and make the following modifications.
- Ensure that it implements our new IBook_UnitTestAppContext interface.
- Implement MarkAsModified method.
Or just copy and paste the following code to replace the current public class Book_UnitTestAppContext.
public class Book_UnitTestAppContext : DbContext, IBook_UnitTestAppContext
{
// You can add custom code to this file. Changes will not be overwritten.
//
// If you want Entity Framework to drop and regenerate your database
// automatically whenever you change your model schema, please use data migrations.
// For more information refer to the documentation:
// http://msdn.microsoft.com/en-us/data/jj591621.aspx
public Book_UnitTestAppContext() : base("name=Book_UnitTestAppContext")
{
}
public System.Data.Entity.DbSet<Book_UnitTestApp.Models.Book> Books { get; set; }
public void MarkAsModified(Book item)
{
Entry(item).State = EntityState.Modified;
}
}
Note changes in inheritance and new mark as modified method.
Open the BooksController.cs file and replace the existing default code with the one in the code snippet as shown below. Changes are made to the type of data base field and a BooksController constructor is added to initialize our IBook_UnitTestAppContext interface. Changes is also made in our PutBook method as we will be replacing the line that sets it to modified with a call to the MarkAsModified method.
public class BooksController : ApiController
{
private IBook_UnitTestAppContext db = new Book_UnitTestAppContext();
public BooksController() { }
public BooksController(IBook_UnitTestAppContext context)
{
db = context;
}
// GET: api/Books
public IQueryable<Book> GetBooks()
{
return db.Books;
}
// GET: api/Books/5
[ResponseType(typeof(Book))]
public IHttpActionResult GetBook(int id)
{
Book book = db.Books.Find(id);
if (book == null)
{
return NotFound();
}
return Ok(book);
}
// PUT: api/Books/5
[ResponseType(typeof(void))]
public IHttpActionResult PutBook(int id, Book book)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != book.Id)
{
return BadRequest();
}
db.MarkAsModified(book);
try
{
db.SaveChanges();
}
catch (DbUpdateConcurrencyException)
{
if (!BookExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return StatusCode(HttpStatusCode.NoContent);
}
// POST: api/Books
[ResponseType(typeof(Book))]
public IHttpActionResult PostBook(Book book)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.Books.Add(book);
db.SaveChanges();
return CreatedAtRoute("DefaultApi", new { id = book.Id }, book);
}
// DELETE: api/Books/5
[ResponseType(typeof(Book))]
public IHttpActionResult DeleteBook(int id)
{
Book book = db.Books.Find(id);
if (book == null)
{
return NotFound();
}
db.Books.Remove(book);
db.SaveChanges();
return Ok(book);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
private bool BookExists(int id)
{
return db.Books.Count(e => e.Id == id) > 0;
}
}
That brings us to the end of Part 1
Part 2 (Unit Testing)
Installing Nuget Packages
Search and install the following nuget packages to the Book_UnitTestApp.Tests project.
- EntityFramework
- Microsoft ASP.NET Web API 2 Core package
Creating Test DbContext
Add a class named TestDbSet to the test project as shown in the code snippet below. This is the base class for our test Db Set.
public class TestDbSet<T> : DbSet<T>, IQueryable, IEnumerable<T>
where T : class
{
ObservableCollection<T> _data;
IQueryable _query;
public TestDbSet()
{
_data = new ObservableCollection<T>();
_query = _data.AsQueryable();
}
public override T Add(T item)
{
_data.Add(item);
return item;
}
public override T Remove(T item)
{
_data.Remove(item);
return item;
}
public override T Attach(T item)
{
_data.Add(item);
return item;
}
public override T Create()
{
return Activator.CreateInstance<T>();
}
public override TDerivedEntity Create<TDerivedEntity>()
{
return Activator.CreateInstance<TDerivedEntity>();
}
public override ObservableCollection<T> Local
{
get { return new ObservableCollection<T>(_data); }
}
Type IQueryable.ElementType
{
get { return _query.ElementType; }
}
System.Linq.Expressions.Expression IQueryable.Expression
{
get { return _query.Expression; }
}
IQueryProvider IQueryable.Provider
{
get { return _query.Provider; }
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return _data.GetEnumerator();
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return _data.GetEnumerator();
}
}
Add a class named TestBookDbSet as shown in the code snippet below.
class TestBookDbSet : TestDbSet<Book>
{
public override Book Find(params object[] keyValues)
{
return this.SingleOrDefault(product => product.Id == (int)keyValues.Single());
}
}
Add a class named TestBookAppContext as shown below.
class TestBookAppContext : IBook_UnitTestAppContext
{
public TestBookAppContext()
{
this.Books = new TestBookDbSet();
}
public DbSet<Book> Books { get; set; }
public int SaveChanges()
{
return 0;
}
public void MarkAsModified(Book item) { }
public void Dispose() { }
}
Creating our Test Class
Add a class named TestBookController as shown in the code snippet below.
[TestClass]
public class TestBookController
{
[TestMethod]
public void PostBook_ShouldReturnSameBook()
{
var controller = new BooksController(new TestBookAppContext());
var item = GetTestBook();
var result =
controller.PostBook(item) as CreatedAtRouteNegotiatedContentResult<Book>;
Assert.IsNotNull(result);
Assert.AreEqual(result.RouteName, "DefaultApi");
Assert.AreEqual(result.RouteValues["id"], result.Content.Id);
Assert.AreEqual(result.Content.Name, item.Name);
}
[TestMethod]
public void PutBook_ShouldReturnStatusCode()
{
var controller = new BooksController(new TestBookAppContext());
var item = GetTestBook();
var result = controller.PutBook(item.Id, item) as StatusCodeResult;
Assert.IsNotNull(result);
Assert.IsInstanceOfType(result, typeof(StatusCodeResult));
Assert.AreEqual(HttpStatusCode.NoContent, result.StatusCode);
}
[TestMethod]
public void PutBook_ShouldFail_WhenDifferentID()
{
var controller = new BooksController(new TestBookAppContext());
var badresult = controller.PutBook(999, GetTestBook());
Assert.IsInstanceOfType(badresult, typeof(BadRequestResult));
}
[TestMethod]
public void GetBook_ShouldReturnProductWithSameID()
{
var context = new TestBookAppContext();
context.Books.Add(GetTestBook());
var controller = new BooksController(context);
var result = controller.GetBook(3) as OkNegotiatedContentResult<Book>;
Assert.IsNotNull(result);
Assert.AreEqual(3, result.Content.Id);
}
[TestMethod]
public void GetBooks_ShouldReturnAllProducts()
{
var context = new TestBookAppContext();
context.Books.Add(new Book { Id = 1, Name = "Demo1", Author = "David Ekpin" });
context.Books.Add(new Book { Id = 2, Name = "Demo2", Author = "Jerry Banfield" });
context.Books.Add(new Book { Id = 3, Name = "Demo3", Author = "Dan Larimar" });
var controller = new BooksController(context);
var result = controller.GetBooks() as TestBookDbSet;
Assert.IsNotNull(result);
Assert.AreEqual(3, result.Local.Count);
}
[TestMethod]
public void DeleteBook_ShouldReturnOK()
{
var context = new TestBookAppContext();
var item = GetTestBook();
context.Books.Add(item);
var controller = new BooksController(context);
var result = controller.DeleteBook(3) as OkNegotiatedContentResult<Book>;
Assert.IsNotNull(result);
Assert.AreEqual(item.Id, result.Content.Id);
}
Book GetTestBook()
{
return new Book() { Id = 3, Name = "Demo name", Author = "David Ekpin" };
}
}
The assert methods used are helper methods that help us determine if a method under test is performing correctly or not.
Having followed all steps correctly, you are ready to run your test.
Running Tests
On the top bar of your visual studio GUI you will see a button the reads TEST, click on test and follow the arrow as follows.
Test > Windows > Test Explorer.
On clicking test explorer, you should be able to see and run all your tests as shown in the screen shots below.
Thanks for learning with me and always remember that testable code is better code.
Posted on Utopian.io - Rewarding Open Source Contributors