Write unit tests for ASP.NET

Introduction

Writing unit tests is a critical step in building robust, high-quality software. When developing an application, it is often helpful to write unit tests that make assertions on various methods and how they are used in the application.

In this article, we’ll look at writing unit tests for ASP.NET applications using the default test library that comes with Visual Studio

Note that a basic understanding of the following is required to follow this guide:

  • ASP.NET MVC
  • C#

Setting up our environment

First things first, you need an application to test. To speed up this guide and focus on unit testing, grab this sample application here

First, the process to use the sample app:

    git clone https://github.com/samuelayo/Net_real_time_commenting_pusher.git

After cloning, open the Real-Time-Commenting.sln file in Visual Studio.

Note: .sln is the acronym for a solution file in .Net . The .sln file contains text-based information that the environment uses to find and load the name-value parameters for the persisted data and the project VSPackages it references. When a user opens a solution, the environment cycles through the preSolution, Project, and postSolution information in the .sln file to load the solution, projects within the solution, and any persisted information attached to the solution.

First create a free Pusher account and log in to your dashboard to create an app.

To create an app:

  • Click on Your apps menu by the side-bar.
  • Click on the create New app button by the bottom.
  • Give your app a name, and select a cluster. (It’s also fine to leave the default cluster).
  • Optionally, you can select the back-end tech, which is .NET and the front-end stack (JavaScript).
  • If you don’t mind, you could also fill in what you’ll be building with Pusher.
  • Click on the Create my app button.
Create Pusher app
  • Move to the App Keys section at the top-bar of your page and copy out your credentials.

Fill in your Pusher app credentials in your Controllers\HomeController file by replacing this line with your XXX_APP_CLUSTER, XXX_APP_ID, XXX_APP_KEY and XXX_APP_SECRET respectively:

1options.Cluster = "XXX_APP_CLUSTER";
2    var pusher = new Pusher("XXX_APP_ID", "XXX_APP_KEY", "XXX_APP_SECRET", options);

Also, remember to fill in your secret key and app cluster in your Views\Home\Details.cshtml file by updating this line:

    var pusher = new Pusher('XXX_APP_KEY', {cluster: 'XXX_CLUSTER'});

To have a better understanding of what the sample app above does, refer to this tutorial.

Setting up tests

If you look at the sample application you have cloned, notice that there are no tests in this application. So how do you go about adding tests to an existing application? Visual Studio makes this task an easy one.

Click on file at the topbar, navigate to new, make another navigation to project. A new dialog box will pop up. By the left sidebar of the new dialog, navigate to visual c#, then scroll down to tests.

By the middle bar, select unit test project. Move down to where you have the name of the project, make sure the name of the project tallies with the name of the project/solution you want to add unit tests for with an extension of .Tests. Here, the name will be Real-Time-Commenting.Tests.

Next, in the solution section, select Add to Solution. Then click ok.

That’s how simple it is to add unit tests to an existing application. Next, you need to write the tests that will be performed.

Please note you might need to add some new references to your unit test as some libraries might not be available. For this tutorial, a reference to System.Web.Mvc will be required, so we can have access to functions like ViewResult, ActionResult, etc.

To add this reference:

  • Move to the solution explorer scroll down to Real-Time-Commenting.Tests.
  • Right-click and select Manage NuGet Packages and search for Microsoft.AspNet.``Mvc in the search bar and hit the search button.
  • Install the Microsoft.AspNet.Mvc package . This should be the first package in the search results.

We have now added the reference to our test.

Next, we need to add a reference to our main app Real-Time-Commenting

Understanding the default test

By default, Visual Studio scaffolds a file called UnitTest1.cs as seen :

1using System;
2    using Microsoft.VisualStudio.TestTools.UnitTesting;
3
4    namespace Real_Time_Commenting.Tests
5    {
6        [TestClass]
7        public class UnitTest1
8        {
9            [TestMethod]
10            public void TestMethod1()
11            {
12            }
13        }
14    }

In the code block above, there are three main differences from a normal ASP.NET class, which are:

  • The reference to Microsoft.VisualStudio.TestTools.UnitTesting which exposes the other two differences which I will point out next.
  • The [TestClass] decorator: any Class to be used for testing must have this decorator just before the class decoration.
  • The [TestMethod] decorator: any function which tests and asserts anything must have this decorator. Any method without this decorator will be treated as a normal method.

Writing your first test

Let us take a quick look at writing a functional test. We will attempt to test the create function of our Homecontroller first as it does not interact with our database yet.

Our test is seen below:

1using System;
2    using Real_Time_Commenting.Controllers;
3    using System.Web.Mvc;
4    using Microsoft.VisualStudio.TestTools.UnitTesting;
5    namespace Real_Time_Commenting.Tests
6    {
7        [TestClass]
8        public class UnitTest1
9        {
10            [TestMethod]
11            public void CreateGet()
12            {
13                HomeController HomeController = new HomeController();
14                ViewResult result = HomeController.Create() as ViewResult;
15                Assert.IsNotNull(result);
16                Assert.IsInstanceOfType(result, typeof(ViewResult));
17                Assert.AreEqual(string.Empty, result.ViewName);
18            }
19        }
20    }

The test above shows how easy it is to test a controller method. In our CreateGet method, we

  • Created a new instance of the HomeController
  • Called the Create function and cast it to be of type ViewResult which makes sense as the function returns a view.
  • Assert that the result is not null
  • Assert that the result is truly an instance of ViewResult
  • Assert that the ViewName is empty. This should pass as we only called return view() in the method, passing no argument/name to the view function.

Testing methods that interact with the database

In the section above, we saw how easy it is to setup and write our first unit test. It would be nice if that were how all controllers behaved. However, in a real-world application, calls would be made to the database, and we will need to test methods that interact with the database.

There are different methods to achieve this kind of test such as mocking, using fake DbContext and a lot more.

In this piece, we will use a fake DbContext to test methods that interact with the database.

Adding an interface

Usually, in ASP.NET applications, the DB context is usually a class that has classes defined (our models), using the DbSet class which can be found in Models\IdentityModel.cs.

DbSet<T> implements IDbSet<T>, so we can create an interface for our context to implement the IDbSet class.

Open your Models\IdentityModels.cs file and replace the ApplicationDbContext class with:

1public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IrealtimeContext
2    {
3        public ApplicationDbContext()
4            : base("DefaultConnection", throwIfV1Schema: false)
5
6        {
7        }
8
9        public static ApplicationDbContext Create()
10        {
11            return new ApplicationDbContext();
12        }
13
14        public IDbSet<BlogPost> BlogPost { get; set; }
15        public IDbSet<Comment> Comment { get; set; }
16    }

In the code block above, we notice that:

  • We have added a new interface called IrealtimeContext which the ApplicationDbContext must implement.
  • The public properties BlogPost and Comment now implement the IDbSet class directly.

Next, we need to create the IrealtimeContext class which we asked the ApplicationDbContext class to implement. Just after the code block above, add:

1public interface IrealtimeContext
2    {
3        IDbSet<BlogPost> BlogPost { get; }
4        IDbSet<Comment> Comment { get; }
5        int SaveChanges();
6    }

Now we can update our controller to be based on this interface rather than the EF specific implementation.

Note: EF stands for Entity Framework, which is the framework used for database interactions in ASP.NET MVC.

Open your HomeController, replace the line that says ApplicationDbContext db = new ApplicationDbContext(); with this code block:

1private readonly IrealtimeContext db;
2    public HomeController() {
3        db = new ApplicationDbContext();
4    }
5    public HomeController(IrealtimeContext context)
6    {
7        db = context;
8    }

Here, we created a constructor with an overloaded method which assigns the instance of our DB based on the parameter supplied. While testing, we will pass in our own fake IDbset instance which does not commit to the database but rather uses a data access layer with an in-memory fake.

Building the fake implementation

Here, we need to build a fake implementation of IDbSet<TEntity>, this is easy to implement. We need to make functions like Add, Find, Attach, Remove, Detach, Create and other methods exposed by the IDbSet interface available. In your Tests solution, create a new file called FakeDbSet.cs and add:

1using Real_Time_Commenting.Models;
2    using System;
3    using System.Collections.Generic;
4    using System.Collections.ObjectModel;
5    using System.Data.Entity;
6    using System.Linq;
7    using System.Web;
8
9    namespace Real_Time_Commenting.Tests
10    {
11        public class FakeDbSet<T> : IDbSet<T>
12        where T : class
13        {
14            ObservableCollection<T> _data;
15            IQueryable _query;
16
17            //constructor
18
19            public FakeDbSet()
20            {
21                _data = new ObservableCollection<T>();
22                _query = _data.AsQueryable();
23            }
24
25            //find function
26
27            public virtual T Find(params object[] keyValues)
28            {
29                throw new NotImplementedException("Derive from FakeDbSet<T> and override Find");
30            }
31
32            // add function
33
34            public T Add(T item)
35            {
36                _data.Add(item);
37                return item;
38            }
39            //remove function
40            public T Remove(T item)
41            {
42                _data.Remove(item);
43                return item;
44            }
45
46            // Attach function
47
48            public T Attach(T item)
49            {
50                _data.Add(item);
51                return item;
52            }
53
54            //  Detach function
55
56            public T Detach(T item)
57            {
58                _data.Remove(item);
59                return item;
60            }
61
62            // Create function
63
64            public T Create()
65            {
66                return Activator.CreateInstance<T>();
67            }
68        }
69    }

Next, we also want to fake some other functions and properties which will be used in the fake DbSet such as ObservableCollection, ElementType, Expression, provider GetEnumerator etc. Below is what the fake implementation looks like:

1public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T
2    {
3        return Activator.CreateInstance<TDerivedEntity>();
4    }
5
6    public ObservableCollection<T> Local
7    {
8        get { return _data; }
9    }
10
11    Type IQueryable.ElementType
12    {
13        get { return _query.ElementType; }
14    }
15
16    System.Linq.Expressions.Expression IQueryable.Expression
17    {
18        get { return _query.Expression; }
19    }
20
21    IQueryProvider IQueryable.Provider
22    {
23        get { return _query.Provider; }
24    }
25
26    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
27    {
28        return _data.GetEnumerator();
29    }
30
31    IEnumerator<T> IEnumerable<T>.GetEnumerator()
32    {
33        return _data.GetEnumerator();
34    }

The above block is a class which implements all the compulsory methods of the IDbSet<TEntity>, which stores objects in memory, as opposed to writing them to a database.

Note that in the code above, we have no logic in our Find method. This is because the find implementation for various models might vary. So instead we return a virtual function that would be overridden.

Next, let us overwrite the find function for our BlogPost and Comments models.

1public class FakeBlogPostSet : FakeDbSet<BlogPost>
2    {
3        public override BlogPost Find(params object[] keyValues)
4        {
5            return this.SingleOrDefault(e => e.BlogPostID == (int)keyValues.Single());
6        }
7    }
8
9    public class FakeCommentSet : FakeDbSet<Comment>
10    {
11        public override Comment Find(params object[] keyValues)
12        {
13            return this.SingleOrDefault(e => e.BlogPostID == (int)keyValues.Single());
14        }
15    }
16
17    public class FakedbContext : IrealtimeContext
18    {
19        public FakedbContext()
20        {
21            this.BlogPost = new FakeBlogPostSet();
22            this.Comment = new FakeCommentSet();
23        }
24
25        public IDbSet<BlogPost> BlogPost { get; private set; }
26
27        public IDbSet<Comment> Comment { get; private set; }
28
29        public int SaveChanges()
30        {
31            return 0;
32        }
33    }

In the code block above, we have three separate classes. The first two classes implement our FakeDbSet class, with an argument of which model we are associating with it. As of now, we have only two models in our application, hence the names FakeBlogPostSet for the BlogPost model and FakeCommentSet for the Comments model.

Because these classes implement our FakeDbSet class, we can override the Find method in the class declaration.

In the FakeBlogPostSet we override the find function and tell it to return the collection whose BlogPostID matches the id.

In the FakeCommentSet we override the find function and tell it to return the collection whose BlogPostID matches the id. Note here that we are not checking against the CommentID because the application we are testing returns all comments that belong to a BlogPost.

Finally, we have the FakedbContext class. This class implements the IrealtimeContext which we had interfaced in our Models\IdentityModels.cs file. Remember that to pass any DbContext to our application, It must interface this Class.

Now we can import our FakedbContext class, pass it to our controller during tests and have it use memory to store our test objects.

Rewriting our first test with the new FakedbContext

In our first test, we wrote a test for the create function of our Homecontroller first as it does not interact with our database yet. While it still does not interact with our database, I’d like to show you you how our new FakedbContext does not affect the function when passed to the controller.

1using System;
2    using Real_Time_Commenting.Controllers;
3    using System.Web.Mvc;
4    using Microsoft.VisualStudio.TestTools.UnitTesting;
5    namespace Real_Time_Commenting.Tests
6    {
7        [TestClass]
8        public class UnitTest1
9        {
10            [TestMethod]
11            public void CreateGet()
12            {
13                var context = new FakedbContext { };
14                HomeController HomeController = new HomeController(context);
15                ViewResult result = HomeController.Create() as ViewResult;
16                Assert.IsNotNull(result);
17                Assert.IsInstanceOfType(result, typeof(ViewResult));
18                Assert.AreEqual(string.Empty, result.ViewName);
19            }
20        }
21    }

Notice any difference in the code above from our first test? Yes. The difference here is that:

  • We defined a new context of class FakedbContext which we passed into the constructor of the HomeController. If you run your tests it would pass with no failure.

Testing all methods in our controller

Now we have our super FakedbContext setup, we can test all methods in our controller which consist of view responses, JSON responses and async tasks with a string response.

Testing the index method

1using System.Web.Mvc;
2    using Microsoft.VisualStudio.TestTools.UnitTesting;
3    using Real_Time_Commenting.Controllers;
4    using Real_Time_Commenting.Models;
5    using System.Linq;
6    using System.Threading.Tasks;
7
8    namespace Real_Time_Commenting.Tests
9    {
10        [TestClass]
11        public class UnitTest1
12        {
13            [TestMethod]
14            public void TestIndex()
15            {
16                var context = new FakedbContext { BlogPost = { new BlogPost { Title = "test", Body="test" } } };
17                HomeController HomeController = new HomeController(context);
18                ViewResult result = HomeController.Index() as ViewResult;
19                Assert.IsInstanceOfType(result.ViewData.Model, typeof(IEnumerable<BlogPost>));
20                var posts = (IEnumerable<BlogPost>)result.ViewData.Model;
21                Assert.AreEqual("test", posts.ElementAt(0).Title);
22                Assert.AreEqual("test", posts.ElementAt(0).Body);
23            }
24        }
25    }

TestIndex: this method tests the Index function of our HomeController. The Index method returns a view alongside a list of all the BlogPosts in our database.

First, we declare a variable called context which is an instance of our FakedbContext class passing in a new BlogPost object, we then pass in the new context to the constructor of our HomeController.

Next, we call the Index function of our HomeController, casting it to be of type ViewResult.

We then check if the result is of type IEnumerable<BlogPost>, we also check that the title and body of the first object equal the title and body we had set in our FakeDbContext.

Testing the details method

1using System.Web.Mvc;
2    using Microsoft.VisualStudio.TestTools.UnitTesting;
3    using Real_Time_Commenting.Controllers;
4    using Real_Time_Commenting.Models;
5    using System.Linq;
6    using System.Threading.Tasks;
7
8    namespace Real_Time_Commenting.Tests
9    {
10        [TestClass]
11        public class UnitTest1
12        {
13            [TestMethod]
14            public void TestDetails()
15            {
16                var context = new FakedbContext { BlogPost = { new BlogPost { BlogPostID=1, Title = "test", Body = "test" } } };
17                HomeController HomeController = new HomeController(context);
18                ViewResult result = HomeController.Details(1) as ViewResult;
19                Assert.IsInstanceOfType(result.ViewData.Model, typeof(BlogPost));
20                var post = (BlogPost)result.ViewData.Model;
21                Assert.AreEqual(1, post.BlogPostID);
22
23            }
24        }
25    }

TestDetails: this method tests the details method of our HomeController. The details method accepts an integer parameter called id. It uses this id to fetch the BlogPost whose id matches in the database, then returns a view alongside the result it gets from the database.

First, we declare a variable called context which is an instance of our FakedbContext class passing in a new BlogPost object, we then pass in the new context to the constructor of our HomeController.

Next, we call the details method, passing in 1 as the id we want to retrieve, casting it to be of type ViewResult. We then verify that the result’s model is of our BlogPost type. Also, we verify that the id of the data returned by the method is equal to 1.

Testing the post action of the create method

1using System.Web.Mvc;
2    using Microsoft.VisualStudio.TestTools.UnitTesting;
3    using Real_Time_Commenting.Controllers;
4    using Real_Time_Commenting.Models;
5    using System.Linq;
6    using System.Threading.Tasks;
7
8    namespace Real_Time_Commenting.Tests
9    {
10        [TestClass]
11        public class UnitTest1
12        {
13            [TestMethod]
14            public void CreatePost()
15            {
16                var context = new FakedbContext{};
17                 BlogPost Post = new BlogPost();
18                 Post.Title = "Test Post";
19                 Post.Body = "Test Body";
20                 HomeController HomeController = new HomeController(context);
21                RedirectToRouteResult result = HomeController.Create(Post) as RedirectToRouteResult;
22                Assert.AreEqual("Index", result.RouteValues["Action"]);
23                Console.WriteLine(result.RouteValues);
24                 Assert.IsNotNull(result.ToString());
25            }
26        }
27    }

CreatePost: this method test the POST method for create. This create method adds a new post, and then returns a RedirectToAction.

First, we declare a variable called context which is an instance of our FakedbContext class. Next, we create a new BlogPost instance passing in the title and the body. We then call the create method passing in our new BlogPost object, casting the result to type RedirectToRouteResult.

Note: we cast the result type here to type RedirectToRouteResult because the method we are testing here returns a RedirectToAction.

We assert that the result’s RouteValues["Action"] is equal to index which means RedirectToAction triggered a redirect to the index method.

Testing the comments method

1using System.Web.Mvc;
2    using Microsoft.VisualStudio.TestTools.UnitTesting;
3    using Real_Time_Commenting.Controllers;
4    using Real_Time_Commenting.Models;
5    using System.Linq;
6    using System.Threading.Tasks;
7
8    namespace Real_Time_Commenting.Tests
9    {
10        [TestClass]
11        public class UnitTest1
12        {
13            [TestMethod]
14            public void TestComments()
15            {
16                var context = new FakedbContext { Comment = { new Comment { BlogPostID = 1, CommentID = 1, Name = "test", Body = "test" }, new Comment { BlogPostID = 1, CommentID = 1, Name = "test", Body = "test" } } };
17                HomeController HomeController = new HomeController(context);
18                JsonResult result = HomeController.Comments(1) as JsonResult;
19                var list = (IList<Comment>)result.Data;
20                Assert.AreEqual(list.Count, 2);
21                Console.WriteLine(list[0].Name.ToString());
22            }
23        }
24    }

TestComments: this method test the comments method of our Controller. The comments method accepts an integer id which is the Id of the BlogPost it wants to get comments for.

First, we declare a variable called context which is an instance of our FakedbContext class, passing in an object of two comments as our comments. Next, we pass the instance to the constructor of our HomeController. We then call the comments method passing in the id of the BlogPost we want to get comments for, casting the result as a JsonResult.

Just before we do our assertion, we cast the JsonResult to a list of type comments. After this, we assert that there are two comments in the response.

Testing the comment method

1using System.Web.Mvc;
2    using Microsoft.VisualStudio.TestTools.UnitTesting;
3    using Real_Time_Commenting.Controllers;
4    using Real_Time_Commenting.Models;
5    using System.Linq;
6    using System.Threading.Tasks;
7
8    namespace Real_Time_Commenting.Tests
9    {
10        [TestClass]
11        public class UnitTest1
12        {
13            [TestMethod]
14            public async Task TestComment()
15            {
16                var context = new FakedbContext { };
17                HomeController HomeController = new HomeController(context);
18                var comment = new Comment { BlogPostID = 1, CommentID = 1, Name = "test", Body = "test" };
19                ContentResult result = await HomeController.Comment(comment) as ContentResult;
20                Assert.AreEqual(result.Content, "ok");
21            }
22        }
23    }

TestComment: this method tests the async method Comment which adds a new comment to the database and broadcasts the comment to Pusher.

First, we declare a variable called context which is an instance of our FakedbContext class. Next, we pass the context into the constructor of our HomeController. We then create a new comment object which we will broadcast, then call the comment method passing in the new comment object.

The method we are testing returns a string content, and we cast the result to be of type ContentResult. Finally, we assert that the results content is equal to the string ok.

Conclusion

During this tutorial, we have covered how to write unit tests in ASP.NET.

We have gone through the process of writing tests for an existing ASP.NET and Pusher application.

We have also covered testing asynchronous methods, methods that return a RedirectToAction, Content, View and JSON

The codebase to this guide can be found here. Feel free to download and experiment with the code.