Reposted on my new blog. Come visit me there!
A barrier to getting into writing executable tests (Unit Tests, integration tests, automated UI tests, etc) with some people I work with, is not knowing how to get started. I believe the desire and the recognition of the value and importance of this testing is growing, but I want to help people get over that hump.
- Read the MSDN Unit Testing MVC article.
- consider splitting controllers into a different project as suggested
- Don’t put your data access code inside the Controller methods, use a data layer/object. Create something you can inject and mock.
- Create an MVC project.
- Use Ninject to setup Dependency Injection
- Get the Ninject WebAPI NuGet package, if using WebApi so DI will work with WebApi.
- Create a solution folder in Visual Studio with this structure.
- Tests
- Integration
- Performance
- UI
- Unit
- Decision: unit testing driven or outside in BDD (my benefits of BDD article)/SpecFlow testing, possibly with Selenium. (Probably both).
- Create a testing project (named {WebsiteName}.Controllers.Tests) under Unit.
- Use a mocking tool. I like FakeItEasy, but Moq is very good as well. Get it from NuGet.
- Setup the tests for DI. This is one approach I’ve used that seems pretty straight forward.
- Create a file named SetupDiFOrTests in your test project. This will setup dependencies and expose the fake services (data access) as a static property.
public class SetupDIForTest { private static readonly Bootstrapper bootstrapper = new Bootstrapper; public static void Start { bootstrapper.Initialize(CreateKernel); }
///
private static IKernel CreateKernel { var kernel = new StandardKernel; try { RegisterServices(kernel); return kernel; } catch { kernel.Dispose; throw; } }
///
- Call the setup start method from an assembly init and add in tests. See my example test method of a Timesheet application.
[TestClass] public class When_Creating_TimeSheets { [AssemblyInitialize] public static void AssemblyInit(TestContext testContext) { SetupDIForTest.Start; } [TestMethod] public void It_Should_Populate_The_Users_Select { // Arrange // use FakeItEasy to set what the method will return A.CallTo( => SetupDIForTest.FakeTimeSheetService.GetTimeSheetUsersAsync) .Returns(Task.FromResult(new List{ new TimeSheetUser{ FirstName = "Kevin" }, new TimeSheetUser{ FirstName = "Terry" } }.AsEnumerable));
// Act var controller = new TimeSheetEntriesController(SetupDIForTest.FakeTimeSheetService, SetupDIForTest.FakeLogger); controller.Create;
// Assert var selectList = controller.ViewBag.TimeSheetUserID as SelectList; Assert.IsNotNull(selectList); Assert.AreEqual("Kevin", selectList.First.Text, "First user should be Kevin"); Assert.AreEqual("Terry", selectList.Skip(1).First.Text); Assert.AreEqual(2, selectList.Count, "It should fill the list with all users from GetTimeSheetUsers"); } }
That should be enough to get your writing some unit tests against your controllers. Happy testing!
EDIT from December 9th, 2015. There is a Ninject.MockingKernel.FakeItEasy Nuget library that simplifies things.
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using Acme.Models;
using Acme.Services;
using Acme.Web.Controllers;
using FakeItEasy;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Ninject;
using Ninject.MockingKernel;
using Ninject.MockingKernel.FakeItEasy;
namespace Acme.Web.Tests
{
[TestClass]
public class ProductControllerTests
{
private readonly FakeItEasyMockingKernel _kernel;
public ProductControllerTests
{
_kernel = new FakeItEasyMockingKernel;
_kernel.Bind<IProductService>.ToMock;
}
[TestInitialize]
public void TestInit
{
_kernel.Reset;
}
[TestMethod]
[TestCategory("Product Controller Tests")]
public void Index_OrderedByProductName
{
// Arrange
var testData = new List<Product>
{
new Product {ProductId = 1, Name = "Z Product 1", Description = "this is a description", Active = true},
new Product {ProductId = 4, Name = "A Product 4", Description = "this is a description", Active = true},
};
var productServiceMock = _kernel.Get(typeof (IProductService)) as IProductService;
A.CallTo( => productServiceMock.GetActiveProducts).Returns(testData);
var controller = new ProductsController(productServiceMock);
// Act
var results = (ViewResult) controller.Index;
// Assert
var model = (List<Product>) results.ViewData.Model;
Assert.AreEqual(testData[0].Name, model.Last.Name, "Should be sorted by first name");
}
}
}
P.S.
You may have to do more advanced mocking of the user or other HTTP objects in MVC to get good coverage.
There is a very good course on Pluralsight about Executable Specifications if you’re interested in SpecFlow, when thinking about testing.
