Test Driven Development
The TDD is a application development way which consist writing the tests before the code.
Why ’’TDD’’ ?
The TDD is an advanced technique which will drive the development and provides dependencies loose coupling. With unit testing the parts of the Code are always tested and the feasibility of the code is covered and controlled.
Pros ?
- The UT will guaranty the good functionality of all components and parts of your application.
- They can act as a documentation during SDLC.
- The development based on UT'ing enforce critical analyse and conceptional.
- Application has the tendency to be better designed, as it will be loose coupled , maintainable as we are free to refactor and decide over conception.
- Reduce debug time.
The Test Frameworks ?
MsTest , NUnit , xUnit , MbUnit
The Example
Here , we will begin to have an example as easy as possible to Test and MVC project.
In MVC, there are many types of test as Action, View, Route , Redirection, Binding and etc...
To begin we create a Web Application(MVC) and in the project type selection have to check the Test Project creation , so your solution will have two project , your application and your test project.
By default Visual Studio will create 3 test methods for HomeController . the references are already included so you find the using statement in top of your test class as Microsoft.VisualStudio.TestTools.UnitTesting .
All tests are marked as TestMethod(microsoft MSTest Framework) and the test class is decorated by TestClass. you can use Fixture if you are doing UT by NUnit. The Test Methods are includes and the include 3 parts as : Arrange , Act and Assert.
Prepare the Project ?
To begin going forward on this article you can download the project source in GitHub with the following link:
https://github.com/EIDIVANDI/MyTDDTestProject
The First UT:
As we mentioned above the Visual Studio have created 3 UT for HomeController by Default. the Index text looks follow:
[TestMethod]
public void Index()
{
//Arrange
HomeController controler = new HomeController() ;
//Act
ViewResult result = controller.Index() as ViewResult ;
//Assert
Assert.AreEqual("Index", result.ViewBag.Message);
}
you can use a varity of the static methods of Assert class, you can have a brief review in the following link:
https://msdn.microsoft.com/fr-fr/library/microsoft.visualstudio.testtools.unittesting.assert.aspx .
The most used are : AreEqual, IsNotNull, IsNull, AreNotEqual, AreNotSame, AreSame, IsFalse, IsTrue, IsInstanceOfType.
To validate the functionality of your code run the tests by going to Test Menu/RUN/All Tests, and you will find the Test Explorer with a list of all existing tests and test state as Passed or Failed .
Testing the business layer?
Add a test class
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MyProject;
using MyProjectDal.Entity;
using MyProjectBLL.Persons;
namespace MyProject.Tests.BusinessLayer
{
[TestClass]
public class PersonManagementTest
{
[TestMethod]
public void GetByName_ExistingPerson_ReturnsPerson_WithDependencies()
{
// Arrange
string personName = "Paul";
// Act
Person result = PersonManagement.GetByName(personName);
// Assert
Assert.IsNotNull(result);
}
}
}
Note: Before Executing the application Run All the tests , the result will be failed which is marked as Red X , if you look at the down part of test explorer you will see the details of the error is caused by sql connection and the reason is the DB context of DAL that is not created yet , but imagine a production environment with lots of UT and a failure on about 20% of tests , YES, Agree with you , it's a time wasting and shocking to be faced , the problem is the coupling of dependence against the isolation principle.
using MyProjectDAL.Context;
using MyProjectDAL.Entity;
using System.Linq;
namespace MyProjectBLL.Persons
{
public class PersonManagement
{
public static Person GetByName(string name)
{
Person p = null;
using (var db = new PersonContext())
{
p = db.Persons.SingleOrDefault(x => x.PersonName.Contains(name));
}
return p;
}
}
}
Looking at the above snippet , the problem is the direct dependency with the DataBase Context.
Repository Pattern
To achieve the solution , we need to implement the most loose coupled scenario, so the following interface is created to declare the CRUD data methods.
public interface IRepository
{
Person GetByName(string name);
}
Puis, nous créons une classe repository qui implémente l’interface
public class EFRepository : IRepository
{
public Person GetByName(string name)
{
Person p = null;
using (var db = new PersonContext())
{
p = db.Persons.SingleOrDefault(x => x.PersonName.Contains(name));
}
return p;
}
}
Advantage of this interface is the possibility to have more implementations of repository with the meme methods and different functionality(ex. Oracle, Sql, Xml and etc. )
We need to change some parts of code to be able to do the DI (Dependency injection)
public class PersonManagement
{
private IRepository _repository;
public PersonManagement()
{
_repository = new EFRepository();
}
public PersonManagement(IRepository repository)
{
_repository = repository;
}
}
You can do it better by the following snippet:
public class PersonManagement
{
private IRepository _repository;
public PersonManagement():this(new EFRepository())
{}
public PersonManagement(IRepository repository)
{
_repository = repository;
}
}
Now , we change the GetByName method of the PersonManagement Class to
public Person GetByName(string name)
{
Person p = _repository.GetByName(name);
return p;
}
Now, we need to change all the lines of code witch are using the GetByName static method of PersonManagement
Change the line
Personmanagement.GetByName();
as
PersonManagement pm= new PersonManagement();
Person p= pm.GetByName(name) ;
So, the resulting code will be as :
[TestClass]
public class PersonManagementTest
{
[TestMethod]
public void GetByName_ExistingPerson_ReturnsPerson()
{
// Arrange
string PersonName = "Paul";
PersonManagement pm = new PersonManagement();
// Act
MyProject.Models.Person result = pm.GetByName(PersonName);
// Assert
Assert.IsNotNull(result); // The Person exists
}
}
But, It's not enough correct.
Improve the UT by Faking:
There are some type of Fake : Stud , shim , moq , dummy and etc…
Here , we cover the Moq, to begin we need to add the moq nuget by using Manage Nuget Package or Package Manager Console (install-package moq)
In the PersonManagementTest class add using Moq;
and change the code as is followed :
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MyProjectBLL.Persons;
using MyProjectDAL.Entity;
using MyProjectBLL.Repository;
using Moq;
namespace MyProject.Tests.Business
{
[TestClass]
public class PersonManagementTest
{
Mock<IRepository> _repository = new Mock<IRepository>();
[TestMethod]
public void GetByName_ExistingPerson_ReturnsPerson()
{
// Arrange
string personName = "Paul";
_Repository.Setup(x => x.GetByName(It.Is<string>(y => y == "Paul")))
.Returns(new Person { PersonName = "Paul" });
var pm = new PersonManagement(_Repository.Object);
// Act
var result = pm.GetByName(personName);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(personName, result.PersonName);
}
}
}
Test the controller:
The controller testing contains the action results testing
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MyProject.Controllers;
using System.Web.Mvc;
namespace MyProject.Tests.Controllers
{
[TestClass]
public class PersonControllerTests
{
[TestMethod]
public void Index_NoInputs_ReturnsDefaultViewResult()
{
// Arrange
PersonController controller = new PersonController ();
// Act
ViewResult result = (ViewResult)controller.Index();
// Assert
Assert.IsNotNull(result);
Assert.AreEqual("", result.ViewName);
Assert.IsNull(result.Model);
}
}
}
View Model testing:
In this part the Mocking is done by the repository, the params are passed via RoutDate, and the Controller context is simulated .
[TestMethod]
public void Display_ExistingPerson_ReturnView()
{
// Arrange
Mock<IRepository> _repository = new Mock<IRepository>();
_repository.Setup(x => x.GetByName(It.Is<string>(y => y == "Paul")))
.Returns(new Person { PersonName = "Paul" });
PersonManagement pm = new PersonManagement(_repository.Object);
PersonController controller = new PersonController(pm);
string personName = "Paul";
RouteData routeData = new RouteData();
routeData.Values.Add("id", personName);
ControllerContext context = new ControllerContext { RouteData = routeData };
controller.ControllerContext = context;
// Act
ViewResult result = (ViewResult)controller.Display();
// Assert
Assert.IsNotNull(result);
Assert.AreEqual("", result.ViewName);
Assert.IsNotNull(result.Model);
Assert.IsInstanceOfType(result.Model, typeof(Person));
Assert.AreEqual(personName, ((Person)result.Model).PersonName);
}
Redirection testing:
The Display action contains the redirection for the purpose of control the behavior when the entity is not found.
[TestMethod]
public void Display_NonExistingPerson_ReturnNotFoundView()
{
// Arrange
string personName = "Alex";
Mock<IRepository> _repository = new Mock<IRepository>();
_repository.Setup(x => x.GetByName(It.Is<string>(y => y == "Paul")))
.Returns(new Person { PersonName = "Paul" });
PersonManagement pm = new PersonManagement(_repository.Object);
PersonController controller = new PersonController(pm);
RouteData routeData = new RouteData();
routeData.Values.Add("id", personName);
ControllerContext context = new ControllerContext { RouteData = routeData };
controller.ControllerContext = context;
// Act
var result = controller.Display() as RedirectToRouteResult;
// Assert
Assert.IsNotNull(result);
Assert.IsInstanceOfType(result, typeof(RedirectToRouteResult));
Assert.AreEqual("NotFound", result.RouteValues["action"]);
}
Http Codes Testion ?
The Action methods who manage a HttpCode as 404(NotFound ) return HttpStatusCodeResult as the return ActionResult, we need to change some part of Display action when the result is null.
If (model == null )
return HttpNotFound(‘’ Person not found ’’) ;
So, the final code will be as below:
[TestMethod]
public void Display_NonExistingPerson_ReturnsHttp404()
{
// Arrange
string PersonName = "Alex";
SetControllerContext(PersonName);
// Act
var result = _Controller.NotFoundError() as HttpStatusCodeResult;
// Assert
Assert.IsNotNull(result);
Assert.IsInstanceOfType(result, typeof(HttpStatusCodeResult));
Assert.AreEqual(404, result.StatusCode);
}
Routes Testing:
The routes are one important part of MVC and route testing is considerably important and is one of the major parts.
To be able to test the routes we need to add the reference of System.Web.Routing to our project.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Web;
using System.Web.Routing;
using System.Web.Mvc;
using MyProject;
namespace MyProject.Tests.Routes
{
[TestClass]
public class RoutesTest
{
public RoutesTest()
{
// Create the routes table
RouteCollection routes = new RouteCollection();
RouteConfig.RegisterRoutes(routes);
}
}
}
You see exactly the code of Application_Start of Global.asax of your MVC application in the constructor of test class.
[TestMethod]
public void DefaultRoute_HomePage_HomeControllerIndexActionOptionalId()
{
// Arrange
Mock<HttpContextBase> mockContextBase = new Mock<HttpContextBase>();
mockContextBase.Setup(x => x.Request.AppRelativeCurrentExecutionFilePath)
.Returns("~/");
// Act
RouteData routeData = routes.GetRouteData(mockContextBase.Object);
// Assert
Assert.IsNotNull(routeData);
Assert.AreEqual("Home", routeData.Values["controller"]);
Assert.AreEqual("Index", routeData.Values["action"]);
Assert.AreEqual(UrlParameter.Optional, routeData.Values["id"]);
}
Also, it is needed to test Ignored routes which are as the xsd files.
[TestMethod]
public void IgnoreRoute_AXDResource_StopRoutingHandler()
{
// Arrange
Mock<HttpContextBase> mockContextBase = new Mock<HttpContextBase>();
mockContextBase.Setup(x => x.Request.AppRelativeCurrentExecutionFilePath)
.Returns("~/Resource.axd");
// Act
RouteData routeData = routes.GetRouteData(mockContextBase.Object);
// Assert
Assert.IsNotNull(routeData);
Assert.IsInstanceOfType(routeData.RouteHandler, typeof(StopRoutingHandler));
}
Easy to understand because well segmented.