Write unit tests for ASP.NET
A basic knowledge of ASP.NET and C# will be helpful when working through this tutorial.
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.
- 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:
options.Cluster = "XXX_APP_CLUSTER";
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 toReal-Time-Commenting.Tests
. - Right-click and select
Manage NuGet Packages
and search forMicrosoft.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 :
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Real_Time_Commenting.Tests
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
}
}
}
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:
using System;
using Real_Time_Commenting.Controllers;
using System.Web.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Real_Time_Commenting.Tests
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void CreateGet()
{
HomeController HomeController = new HomeController();
ViewResult result = HomeController.Create() as ViewResult;
Assert.IsNotNull(result);
Assert.IsInstanceOfType(result, typeof(ViewResult));
Assert.AreEqual(string.Empty, result.ViewName);
}
}
}
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 calledreturn 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:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IrealtimeContext
{
public ApplicationDbContext()
: base("DefaultConnection", throwIfV1Schema: false)
{
}
public static ApplicationDbContext Create()
{
return new ApplicationDbContext();
}
public IDbSet<BlogPost> BlogPost { get; set; }
public IDbSet<Comment> Comment { get; set; }
}
In the code block above, we notice that:
- We have added a new interface called
IrealtimeContext
which theApplicationDbContext
must implement. - The public properties
BlogPost
andComment
now implement theIDbSet
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:
public interface IrealtimeContext
{
IDbSet<BlogPost> BlogPost { get; }
IDbSet<Comment> Comment { get; }
int SaveChanges();
}
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:
private readonly IrealtimeContext db;
public HomeController() {
db = new ApplicationDbContext();
}
public HomeController(IrealtimeContext context)
{
db = context;
}
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:
using Real_Time_Commenting.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Entity;
using System.Linq;
using System.Web;
namespace Real_Time_Commenting.Tests
{
public class FakeDbSet<T> : IDbSet<T>
where T : class
{
ObservableCollection<T> _data;
IQueryable _query;
//constructor
public FakeDbSet()
{
_data = new ObservableCollection<T>();
_query = _data.AsQueryable();
}
//find function
public virtual T Find(params object[] keyValues)
{
throw new NotImplementedException("Derive from FakeDbSet<T> and override Find");
}
// add function
public T Add(T item)
{
_data.Add(item);
return item;
}
//remove function
public T Remove(T item)
{
_data.Remove(item);
return item;
}
// Attach function
public T Attach(T item)
{
_data.Add(item);
return item;
}
// Detach function
public T Detach(T item)
{
_data.Remove(item);
return item;
}
// Create function
public T Create()
{
return Activator.CreateInstance<T>();
}
}
}
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:
public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T
{
return Activator.CreateInstance<TDerivedEntity>();
}
public ObservableCollection<T> Local
{
get { return _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();
}
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.
public class FakeBlogPostSet : FakeDbSet<BlogPost>
{
public override BlogPost Find(params object[] keyValues)
{
return this.SingleOrDefault(e => e.BlogPostID == (int)keyValues.Single());
}
}
public class FakeCommentSet : FakeDbSet<Comment>
{
public override Comment Find(params object[] keyValues)
{
return this.SingleOrDefault(e => e.BlogPostID == (int)keyValues.Single());
}
}
public class FakedbContext : IrealtimeContext
{
public FakedbContext()
{
this.BlogPost = new FakeBlogPostSet();
this.Comment = new FakeCommentSet();
}
public IDbSet<BlogPost> BlogPost { get; private set; }
public IDbSet<Comment> Comment { get; private set; }
public int SaveChanges()
{
return 0;
}
}
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.
using System;
using Real_Time_Commenting.Controllers;
using System.Web.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Real_Time_Commenting.Tests
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void CreateGet()
{
var context = new FakedbContext { };
HomeController HomeController = new HomeController(context);
ViewResult result = HomeController.Create() as ViewResult;
Assert.IsNotNull(result);
Assert.IsInstanceOfType(result, typeof(ViewResult));
Assert.AreEqual(string.Empty, result.ViewName);
}
}
}
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
using System.Web.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Real_Time_Commenting.Controllers;
using Real_Time_Commenting.Models;
using System.Linq;
using System.Threading.Tasks;
namespace Real_Time_Commenting.Tests
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestIndex()
{
var context = new FakedbContext { BlogPost = { new BlogPost { Title = "test", Body="test" } } };
HomeController HomeController = new HomeController(context);
ViewResult result = HomeController.Index() as ViewResult;
Assert.IsInstanceOfType(result.ViewData.Model, typeof(IEnumerable<BlogPost>));
var posts = (IEnumerable<BlogPost>)result.ViewData.Model;
Assert.AreEqual("test", posts.ElementAt(0).Title);
Assert.AreEqual("test", posts.ElementAt(0).Body);
}
}
}
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
using System.Web.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Real_Time_Commenting.Controllers;
using Real_Time_Commenting.Models;
using System.Linq;
using System.Threading.Tasks;
namespace Real_Time_Commenting.Tests
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestDetails()
{
var context = new FakedbContext { BlogPost = { new BlogPost { BlogPostID=1, Title = "test", Body = "test" } } };
HomeController HomeController = new HomeController(context);
ViewResult result = HomeController.Details(1) as ViewResult;
Assert.IsInstanceOfType(result.ViewData.Model, typeof(BlogPost));
var post = (BlogPost)result.ViewData.Model;
Assert.AreEqual(1, post.BlogPostID);
}
}
}
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
using System.Web.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Real_Time_Commenting.Controllers;
using Real_Time_Commenting.Models;
using System.Linq;
using System.Threading.Tasks;
namespace Real_Time_Commenting.Tests
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void CreatePost()
{
var context = new FakedbContext{};
BlogPost Post = new BlogPost();
Post.Title = "Test Post";
Post.Body = "Test Body";
HomeController HomeController = new HomeController(context);
RedirectToRouteResult result = HomeController.Create(Post) as RedirectToRouteResult;
Assert.AreEqual("Index", result.RouteValues["Action"]);
Console.WriteLine(result.RouteValues);
Assert.IsNotNull(result.ToString());
}
}
}
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
using System.Web.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Real_Time_Commenting.Controllers;
using Real_Time_Commenting.Models;
using System.Linq;
using System.Threading.Tasks;
namespace Real_Time_Commenting.Tests
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestComments()
{
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" } } };
HomeController HomeController = new HomeController(context);
JsonResult result = HomeController.Comments(1) as JsonResult;
var list = (IList<Comment>)result.Data;
Assert.AreEqual(list.Count, 2);
Console.WriteLine(list[0].Name.ToString());
}
}
}
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
using System.Web.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Real_Time_Commenting.Controllers;
using Real_Time_Commenting.Models;
using System.Linq;
using System.Threading.Tasks;
namespace Real_Time_Commenting.Tests
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public async Task TestComment()
{
var context = new FakedbContext { };
HomeController HomeController = new HomeController(context);
var comment = new Comment { BlogPostID = 1, CommentID = 1, Name = "test", Body = "test" };
ContentResult result = await HomeController.Comment(comment) as ContentResult;
Assert.AreEqual(result.Content, "ok");
}
}
}
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.
8 March 2018
by Samuel Ogundipe