Using tests to help you create your code
This post is going to look at the concept of using test driven development to drive out the code for a project.
The idea of TDD has been around for quite some time now, but it seems to be gathering favour with more and more developers now.
This post is going to look at the concept of using test driven development to drive out the code for a project.
The idea of TDD has been around for quite some time now, but it seems to be gathering favour with more and more developers now.
Brief Overview
The process of TDD is shown in the stages below
- Write a test
- Check if the test fails
- Write the smallest amount of production code to make the test pass
- Run the test (Return to step 3 if it fails)
- Refactor the code (both tests and production code)
Theses steps are often referred to as Red (the test fails), Green (the test passes), Refactor.
TDD in Action
The easiest way to explain how TDD works is to show some examples. To do this we are going to look at creating a login page for a website, minus the front end (View). For these examples we are going to use the following development stack
- Castle MonoRail (a MVC Framework for .Net)
- NUnit (a testing framework for .Net)
- Rhino Mocks (a mocking framework for .Net)
Whilst these may not be your development stack, the principles discussed in this post will be roughly the same.
Starting from scratch
We are going to be starting completely from scratch with no pre-written code, so the first thing to do is create the LoginController
class. Yes that’s right we are not going to start with the database first, in fact that will happen much further down the line.
public class LoginController : SmartDispatcherController
{
}
There is no need to worry about the
SmartDispatcherController
, this is a MonoRail class that implements smart ways to invoke actions. Basically it makes our life easier.
So we have the empty controller, we need to think about what we want it to do. This is where TDD comes in. The tests are going to help us create all of the code for the controller and it’s dependencies. So let’s create an empty test fixture for the controller.
[TestFixture]
public class LoginControllerFixture : BaseControllerTest
{
}
The TestFixture attribute comes from the NUnit framework. It basically means that when NUnit sees this class it knows it has tests to run.
The
BaseControllerTest
is a MonoRail testing base class which allows us to test controllers without the need for an ASP.Net Runtime
Creating the first test
Now we have the test fixture ready we can put our first test together.
As we are testing whether users can login in or not, lets start with a test called User_Can_Successfully_Login
.
[TestFixture]
public class LoginControllerFixture : BaseControllerTest
{
[Test]
public void User_Can_Successfully_Login()
{
var controller = new LoginController();
PrepareController(controller);
}
}
So far we haven’t done anything much other than create a reference to the LoginController
class we created earlier. The PrepareController
method comes from the MonoRail BaseControllerTest
and it prepares the controller giving it mock implementations of the services it requires to function normally.
Ok, so we need to actually get this test doing something now. The important thing to remember here is that we are only testing the controller and nothing else. Following the Single Responsibility Principle, the controller will handle the user input. The handling of the authentication will be done by an AuthenticationService
class.
[TestFixture]
public class LoginControllerFixture : BaseControllerTest
{
[Test]
public void User_Can_Successfully_Login()
{
var mockery = new MockRepository();
var authenticationService = mockery.DynamicMock();
var controller = new LoginController(authenticationService);
var authenticatedUser = new AuthenticatedUserDTO { Id = Guid.NewGuid(), Type = "Administrator" };
var authenticationRequest = new AuthenticationRequestDTO {Username = "username", Password = "password"};
PrepareController(controller);
With.Mocks(mockery)
.Expecting(() => Expect.Call(authenticationService.AuthenticationUser(authenticationRequest)).Return(authenticatedUser))
.Verify(() => controller.Login(authenticationRequest));
Assert.AreEqual("/admin/index", Response.RedirectedTo);
}
}
As we currently don’t have an AuthenticationService
class we have added the mocking framework to “mock” the service functionality. The functionality of the service will be tested by the test fixture for that service not by the controller test fixture.
A mocking framework allows us to simulate the output of objects, so we don’t actually have to worry about the implementation of those objects. Remember we are only testing one unit at a time.
Let’s quickly review the whole test method.
- Setup the Mock framework
- Create a mocked instance of the
IAuthenticationService
interface - Create and prepare a reference to the controller we are testing and passed in any of it’s mocked dependencies
- Create a
AuthenticatedUserDTO
object as this is needed by theIAuthenticationService
- Create a
AuthenticationRequestDTO
object that will be passed into theLogin
action - Set the expectations for the controllers
Login
method – Expectations are the methods expected to be called by theLogin
method - Run the
Login
method via the call toVerify
- Make sure the method has redirected to the index action of the admin controller by using assertions.
The test is forcing us to generate code
We have our first test method, however this won’t compile as we have no IAuthenticationService
interface, no AuthenticatedUserDTO
, no AuthenticationRequestDTO
, no Login
method on the LoginController
and we are missing the AuthenticationService
dependency in the controller, so let’s get all this added in.
public class LoginController : SmartDispatcherController
{
private readonly IAuthenticationService authenticationService;
public LoginController(IAuthenticationService authenticationService)
{
this.authenticationService = authenticationService;
}
public void Login([DataBind("login")]AuthenticationRequestDTO authenticationRequest)
{
}
}
We’ve got the authenticationService
dependency and the login action in now.
public interface IAuthenticationService
{
AuthenticatedUserDTO AuthenticateUser(AuthenticationRequestDTO authenticationRequest);
}
Here is our abstraction of the AuthenticationService
. Using the idea of dependency injection we can substitute in any service that inherits from this interface.
As the test has already specified it expects a call to the AuthenticationUser
method of the IAuthenticationService
interface we can also create the signature of that method.
public class AuthenticatedUserDTO
{
public Guid Id { get; set; }
public string Type { get; set; }
}
public class AuthenticationRequestDTO
{
public string Username { get; set; }
public string Password { get; set; }
}
The last two objects are our DTOs (Data Transfer Objects). These are simple objects that contain no business logic, they are used to transfer data between other objects.
Run the test
The code should now compile happily, so we can move on to the next stage of TDD and that is running the test. Clearly the test will fail as we have no code in the LoginController
to make it pass. Now it may seem pointless running a test when there is no code to make it pass, however if you were writing a test for an edge case you would want to make sure the test wasn’t passing first, otherwise the test may well be wrong.
Make the test pass
When using TDD, the developer should remember they need to write the smallest amount of code possible to make the test pass.
public class LoginController : SmartDispatcherController
{
private readonly IAuthenticationService authenticationService;
public LoginController(IAuthenticationService authenticationService)
{
this.authenticationService = authenticationService;
}
public void Login([DataBind("login")]AuthenticationRequestDTO authenticationRequest)
{
var authenticatedUser = authenticationService.AuthenticationUser(authenticationRequest);
Redirect("Admin", "Index");
}
}
If we now run the test it will pass. The final stage of the TDD process is to refactor the code. At this stage there is no code that can be refactored, so we’ll move on.
Test the negative and positive
We are not finished here as we need to test that the controller can cope when a user enters incorrect details. Let’s look at creating a User_Can_Fail_Login
test.
[Test]
public void User_Can_Fail_Login()
{
var mockery = new MockRepository();
var authenticationService = mockery.DynamicMock();
var controller = new LoginController(authenticationService);
var authenticationRequest = new AuthenticationRequestDTO {Username = "username", Password = "password"};
PrepareController(controller);
With.Mocks(mockery)
.Expecting(() => Expect.Call(authenticationService.AuthenticationUser(authenticationRequest)).Return(new AuthenticatedUserDTO()))
.Verify(() => controller.Login(authenticationRequest));
Assert.AreEqual(1, controller.Flash.Count);
Assert.AreEqual("Username or Password not recognised", controller.Flash["error"]);
}
The difference here from the successful login test is that the mocked authenticationService
returns an empty instance of the AuthenticatedUserDTO object. We also have changed the assertions to make sure that an error has been placed into the MonoRail Flash parameter. Now if we run the overall test fixture we will have one passing and one failing test, so we need to revisit the LoginController
class and make the second test pass.
public class LoginController : SmartDispatcherController
{
private readonly IAuthenticationService authenticationService;
public LoginController(IAuthenticationService authenticationService)
{
this.authenticationService = authenticationService;
}
public void Login([DataBind("login")]AuthenticationRequestDTO authenticationRequest)
{
var authenticatedUser = authenticationService.AuthenticationUser(authenticationRequest);
if (authenticatedUser.Id != Guid.Empty)
Redirect("Admin", "Index");
else
{
Flash["error"] = "Username or Password not recognised";
RedirectToReferrer();
}
}
}
If we run the tests now, we should have two passing tests.
Refactor all our code
Again as part of the TDD process we need to look at refactoring our code, this includes the test fixture code as well. From looking at the controller code there isn’t anything that can be refactored, however the test fixture is suffering from DRYness (Don’t Repeat Yourself).
Let’s look at the whole test fixture class again.
[TestFixture]
public class LoginControllerFixture : BaseControllerTest
{
[Test]
public void User_Can_Successfully_Login()
{
var mockery = new MockRepository();
var authenticationService = mockery.DynamicMock();
var controller = new LoginController(authenticationService);
var authenticatedUser = new AuthenticatedUserDTO { Id = Guid.NewGuid(), Type = "Administrator" };
var authenticationRequest = new AuthenticationRequestDTO {Username = "username", Password = "password"};
PrepareController(controller);
With.Mocks(mockery)
.Expecting(() => Expect.Call(authenticationService.AuthenticationUser(authenticationRequest)).Return(authenticatedUser))
.Verify(() => controller.Login(authenticationRequest));
Assert.AreEqual("/admin/index", Response.RedirectedTo);
}
[Test]
public void User_Can_Fail_Login()
{
var mockery = new MockRepository();
var authenticationService = mockery.DynamicMock();
var controller = new LoginController(authenticationService);
var authenticationRequest = new AuthenticationRequestDTO {Username = "username", Password = "password"};
PrepareController(controller);
With.Mocks(mockery)
.Expecting(() => Expect.Call(authenticationService.AuthenticationUser(authenticationRequest)).Return(new AuthenticatedUserDTO()))
.Verify(() => controller.Login(authenticationRequest));
Assert.AreEqual(1, controller.Flash.Count);
Assert.AreEqual("Username or Password not recognised", controller.Flash["error"]);
}
}
You should be able to see the duplication quite clearly. So we need to clean this up.
[TestFixture]
public class LoginControllerFixture : BaseControllerTest
{
private MockRepository mockery;
private IAuthenticationService authenticationService;
private LoginController controller;
private AuthenticationRequestDTO authenticationRequest;
[SetUp]
public void SetUp()
{
mockery = new MockRepository();
authenticationService = mockery.DynamicMock();
controller = new LoginController(authenticationService);
authenticationRequest = new AuthenticationRequestDTO {Username = "username", Password = "password"};
PrepareController(controller);
}
[Test]
public void User_Can_Successfully_Login()
{
mockLoginAction(new AuthenticatedUserDTO { Id = Guid.NewGuid(), Type = "Administrator" });
Assert.AreEqual("/admin/index", Response.RedirectedTo);
}
[Test]
public void User_Can_Fail_Login()
{
mockLoginAction(new AuthenticatedUserDTO());
Assert.AreEqual(1, controller.Flash.Count);
Assert.AreEqual("Username or Password not recognised", controller.Flash["error"]);
}
private void mockLoginAction(AuthenticatedUserDTO authenticatedUser)
{
With.Mocks(mockery)
.Expecting(() => Expect.Call(authenticationService.AuthenticationUser(authenticationRequest)).Return(authenticatedUser))
.Verify(() => controller.Login(authenticationRequest));
}
}
You can see here we’ve cleaned up the test fixture quite a bit.
The first thing we’ve used here is the built in testing framework SetUp
method. This method is run before every test method is run, so we can use it to prepare everything we are going to need for each test.
The second thing we have done is to move the mocking code into a private method that both test methods can use at once.
Once we have finished refactoring any code we need to run the tests again to make sure we haven’t broken anything.
Conclusion
So from starting with an empty controller class we have used two tests to drive out our code and we now have a fully tested controller, with an abstract dependency and two DTOs. Clearly we need to add in some method of keeping the user logged in, using a cookie or session variable, but this isn’t going to be done now as this post would be much longer!
Yes we have written more code that we would have done by just simply writing the controller without tests, however these tests are some major benefits for the future.
- We’ll know if we brake anything in the future as we can keep running the tests at any stage
- If a new developer starts to work on the code, they will be able to read the tests and understand what the controller should be doing. That’s right, we have written some documentation for them!
- To run these tests we’ve not had to ramp up any database or test data
- We’ve not had to set up any web server to test the code
- Developers can have rapid feedback on any development work they are doing, by running the tests as often as they want
- The controller is loosely coupled with it’s dependency (
AuthenticationService
), so we can change the service at any stage, as long as it inherits from the abstract version of the service.
Now we have our controller we can then use the same TDD ideas to drive out the code for the abstract service object we have. This will, more than likely, lead to the driving out of other abstract objects.
Once we have all these objects written we can then use the idea of integration tests to make sure all the objects work together without any mocking.
No comments yet.