Unit Testing Web Api 2 Applications that use EntityFramework

in #utopian-io7 years ago (edited)

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

Sort:  

Your contribution cannot be approved because it does not follow the Utopian Rules.

  • Submissions containing unexplained essential steps, codes or examples will be rejected.

You can contact us on Discord.
[utopian-moderator]

Hey @laxam, I just gave you a tip for your hard work on moderation. Upvote this comment to support the utopian moderators and increase your future rewards!

Thank you for the contribution. It has been approved.

You can contact us on Discord.
[utopian-moderator]

Hey @davidekpin I am @utopian-io. I have just upvoted you!

Achievements

  • Seems like you contribute quite often. AMAZING!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x