Test Driven Development with ASP.Net MVC (Part 4)

The series in full:

In Part 3 we got the first test up and running. Now we can get straight on with adding more tests and functionality to meet the needs of the User Story. A quick look at the Acceptance Criteria suggests the next functionality to implement:

Scenario 1 – Search criteria options are pre populated
GIVEN that I want to search for an event by region
WHEN I select to choose a region
THEN a list of possible regions is presented

I think at this point I would like to expend on the Acceptance Criteria again, to make it a bit more explicit.  You shouldn’t be afraid the make changes and clarification as you go, it is all part of the process.  So I will change Scenario 1 to:

Scenario 1 – Search criteria options are pre populated
GIVEN that I want to search for an event by region
WHEN I select to choose a region
THEN a list of possible regions is presented
AND the list should contain "All", "North", "South" and "London"
AND "All" should be the default value

This looks a little better to me.  It does also make me think about another Scenario that we need to cover but more about that later. But first, we can start with another test:

        [TestMethod]

        public void PresentAListOfAllPossibleRegionsForTheUserToSelectFrom()

        {

        }

The next part of my TDD process is to think about the behaviour of the functionality that the Controller will need to perform, and how it will achieve it within the architecture of the system. We need some data, and the application will have a service layer, so this is a good indication that we will need a service of some kind.  We also need to get some data to the view so are going to need a model of some kind. Lets fill in the test.

        [TestMethod]

        public void PresentAListOfAllPossibleRegionsForTheUserToSelectFrom()

        {

            //Arrange

            var mockEventService = new Mock<IEventService>();

            var response = new GetAllRegionsResponse();

            response.Regions = new List<RegionDto>();

            response.Regions.Add(new RegionDto {Id = 1, Name = “North”});

            response.Regions.Add(new RegionDto {Id = 2, Name = “South”});

            response.Regions.Add(new RegionDto {Id = 3, Name = “London”});

            mockEventService.Setup(x => x.GetAllRegions()).Returns(response);

            var eventController = new EventController(mockEventService.Object);

            //Act

            var result = (ViewResult)eventController.Search();

            var model = (SearchViewModel)result.Model;

            //Assert

            Assert.AreEqual(3,model.Regions.Count(),

                “Unexpected Number of Regions returned”);

        }

There is a bit going on in the test, so I will try to explain it.  Remember that the red text is Resharpers way of telling us that the implementation is missing, so there is lots of work to do to get this test up and running.  As we are defining the test first and them implementing to make it pass, we only implement the functionality we know we are going to need, not the functionality we think we may need in the future.

The first line is creating a mock for the IEventService. My mocking framework of choice for .Net is Moq. The next part is creating a Response object to be returned from the mock service when we call the GetAllRegions method.

mockEventService.Setup(x => x.GetAllRegions()).Returns(response);

is setting the behaviour of the mocked IEventService. It basically says: when the GetAllRegions method of the IEventService is called, return the Response I have described in the test, not the actual response from the real implementation of IEventService.

Now we are using a service we would expect it to get passed into the constructor of the Controller, IOC style. The assertion for this test will ensure the the 3 regions from the service call are populated on the model.

Now the test is complete we can start on the implementation. I tend to start at the top of the test and fill in the implementation as I go. My advice is to leverage the refactoring capabilities of ReSharper (or your tool of choice – even standard Visual Studio has tooling to take some of the pain away). As part of the implementation I am going to introduce Ninject to take care of the IOC duties.

The steps to complete the implement go a little something like this:

  • Add a new project for the services and related things
  • Setup Ninject for the IOC
  • Create and service contract and an empty service implementation
public interface IEventService
    {
        GetAllRegionsResponse GetAllRegions();
    }
public class EventService : IEventService
    {   
        public GetAllRegionsResponse GetAllRegions()
        {
            throw new NotImplementedException();
        }
    }
  • Create the Response and RegionDto
public class GetAllRegionsResponse
    {
        public List<RegionDto> Regions;
    }
public class RegionDto
    {
        public int Id { getset; }
        public string Name { getset; }
    }
  • Create the SearchViewModel
 public class SearchViewModel
    {
        private List<RegionDto> regions = new List<RegionDto>();

        public List<RegionDto> Regions
        {
            get { return regions; }
            set { regions = value; }
        }
    }
  • Update the Controller to use the new service
public class EventController : Controller
    {
        private readonly IEventService eventService;

        public EventController(IEventService eventService)
        {
            this.eventService = eventService;
        }

        public ViewResult Search()
        {
            var model = new SearchViewModel();

            model.Regions = eventService.GetAllRegions().Regions;

            return View(model);
        }
    }

So lets fire up the tests.  But wait a second there is a problem with the first test we did as the constructor is expecting an argument, but we are not giving it one.  It is an easy fix but teaches us something about the nature of TDD; that you can never assume that tests are written and then forgotten. As the functionality becomes more complex, you will often have to revisit tests as at the time they were based on the most simple implementation that would make the test pass, which has now evolved into something more complex.  We can change the test by adding in the mock for the EventService.

        [TestMethod]

        public void DisplayTheDefaultSearchEventView()

        {

            //Arange

            var mockEventService = new Mock<IEventService>();

            var eventController = new EventController(mockEventService.Object);

            //Act

            var result = (ViewResult)eventController.Search();

            //Assert

            Assert.IsTrue(String.IsNullOrEmpty(result.ViewName));

        }

Any copy and paste is usually an indication of the opportunity for refactoring, and this is no exception. We shall look at refactoring the tests after we have them working. Lets run the tests.

Part4 Test Results 1

Part4 Test Results 1

There is some good news and some bad news. The new test is passing, but now the old test is failing. The reason for this is that we are not supplying an behaviour for the mockEventService, so when the method is called it is returning a null Response and we are essentially asking for null.Regions. Again we can fix this easily by adding the setup for GetAllRegions to the first test.  This is the second duplication of code, so some refactoring of the test class is definitely on the cards.  A quick run of the test lets us know the change was successful.  Just before we refactor the test class, I had a thought that I want to make my second test a little stronger, so I added an additional Assertion.  It is not uncommon to make tests more specific in this way, as you uncover more about the behaviour you want to test for.

            Assert.AreEqual(1, model.Regions.FindAll(x => x.Name == “North”).Count,

                “Expected to find ‘North'”);

I could test for all three regions, but it felt a bit like overkill at this stage. Let’s run the tests.

Part4 Test Results 2

Part4 Test Results 2

That’s more like it. Let the refactoring commence. We can move out the duplication into a [TestInitialize] method to share the implementation amongst all the tests in the class. I have also added a [TestCleanup] method to run after each test has run. This contains a check to verify that all the behaviours specified in the mock setup have been run, so the test will fail if you expect it to do something that it does not do, even if all the Assertions pass. Here is the test class in full.

[TestClass]
public class WhenSearchingEventControllerShould
{
    private EventController eventController;
    private Mock<IEventService> mockEventService;
    private GetAllRegionsResponse response;

    [TestInitialize]
    public void Setup()
    {
        //Arrange
        mockEventService = new Mock<IEventService>();

        response = new GetAllRegionsResponse
            {
                Regions = new List<RegionDto>
                    {
                        new RegionDto {Id = 1, Name = "North"},
                        new RegionDto {Id = 2, Name = "South"},
                        new RegionDto {Id = 3, Name = "London"}
                    }
            };

        mockEventService.Setup(x => x.GetAllRegions()).Returns(response);

        eventController = new EventController(mockEventService.Object); 
    }

    [TestCleanup]
    public void Teardown()
    {
        mockEventService.VerifyAll();
    }

    [TestMethod]
    public void DisplayTheDefaultSearchEventView()
    {
        //Act          
        var result = eventController.Search();

        //Assert
        Assert.IsTrue(String.IsNullOrEmpty(result.ViewName));
    }

    [TestMethod]
    public void PresentAListOfAllPossibleRegionsForTheUserToSelectFrom()
    {  
        //Act
        var result = eventController.Search();
        var model = (SearchViewModel)result.Model;

        //Assert
        Assert.AreEqual(3,model.Regions.Count,
            "Unexpected Number of Regions returned");
        Assert.AreEqual(1, model.Regions.FindAll(x => x.Name == "North").Count,
            "Expected to find 'North'");
    }
}

You can see that the setting up of the Regions is tied to this test class. If we needed a test that had a different setup for the call to GetAllRegions, it may be an indication that the we are testing a different set of behaviours and that the new test should belong in a new test class.

So now we have a second Controller test, however there is still work to do to get the things up and running in an Outside In manner. Check out Part 5 to see how we Test Drive the Service method we are mocking in the Controller tests, and get it talking to the Repository.

The series in full:

Advertisements