What is Unit Testing?
Unit testing is a technique used to validate fine grained functionality in object oriented programming (although you can "unit test" routines in procedural languages too, unit testing is most widely associated with OOP).
A basic unit test might look like:
[Test]
public void AdditionTest()
{
int result = MathLib.Add(1, 2);
Assert.AreEqual(result, 3);
}
where we are inputting known values to a method and comparing the results against what we expect to happen.
Why Unit Test?
The two main benefits of unit testing are:
• Correctly written unit tests ensure the accuracy of the behaviour of your classes.
• Unit Testing encourages Separation of Concerns and the layering of software (e.g. removing domain code from the UI). This is fundamental to developing maintainable software.
There are other benefits of unit testing and test driven development, but talking about them would lead me off topic, so I'll leave that for another rant.
Data Aware Applications in .NET
Visual Studio provides a fantastic environment to enable developers to quickly piece together functional applications in minimal time. (When I mention .NET, I am automatically including the Visual Studio IDE in any conversation, as who develops .NET apps without Visual Studio? Consider the two synonymous). Much of this power comes from the ability to use DataSets, and in particular typed DataSets, and bind them to data aware controls.
The quickest way to get an application up and running is to use the suppled tools and create datasets, dragging them onto your forms (be they WebForms or WinForms) and let VS work its magic. You can then double click on a button and hook-up the UI controls (such as text and check boxes) to work with the data. Validation is often performed in the UI code.
Here we have already run into problems. How do we Unit Test? There are two main problems. How do we run a unit test, given our code resides in UI layer and, and how can we construct repeatable tests when we're talking to a database?
Designing for Unit Tests
This is where unit testing encourages Separation of Concerns. This first thing we need to do is break out our code into three parts, which we just identified through the problems we're having unit testing.
Firstly, we need to separate the UI from the business logic (and validation, which does belong with the business rules). We need to create a separation that will allow testing to be performed. To do this we use interfaces, e.g. IView, where the interfaces represents the UI or View.
Secondly, we need to be able to separate the data layer so we can repeat tests and specify the input values of our function (just like in the example earlier). To do this we pull the data access of the UI and put it behind a DataMapper parent class. Each DataMapper will normally correspond to a table or view in the database and will be used to populate the business objects. Business objects will have no knowledge of it's DataMapper (but the DataMapper will know about the business object, so it can create, read, update, delete).
Most business aware applications are structured so that each form represents a piece of business activity, for example making a booking. It's best to encapsulate (any small) business objects into actual business services. This is known as Service Oriented Architecture (SOA), which you may have heard lots about. We're going to call this service layer the Model and define the services through particular IService interfaces.
For the purpose of unit testing we can create mock objects that for each necessary DataMapper (created within the test assembly) that will simulate a connection to a database and provide idempotent behaviour.
It's not quit complete yet, we need something that synchronises the View with the Service. That synchronisation object is known as the Presenter. Look at that. I've just described the MVP (Model-View-Presenter) pattern.
Example
It's probably a good idea to throw in an example at about this time. A simple case that of logging in to a website.
The UI needs to be able to take a username and password and pass it to the business layer for validation, and then give feedback to the user.
Here's what the unit test might look like
[Test]
public void TestUserLogin()
{
// Normally would use ILoginView, but for the test, we need to simulate login button presses
MockLoginView mockView = new MockLoginView();
ILoginService mockService = new MockLoginService();
LoginPresenter presenter = new LoginPresenter(mockView, mockService);
mockView.LoginButtonPressed();
Assert.IsTrue(presenter.LoggedIn, "Error in Login logic");
}
And here are the interfaces for the MVP pattern.
public delegate void LoginRequestHandler(object sender, LoginRequestedEventArgs e);
public interface ILoginView
{
event LoginRequestHandler LoginRequested;
string Username { get; }
string Password { get; }
IList Errors { set; }
}
public sealed class LoginRequestedEventArgs : EventArgs
{
private string username;
private string password;
public LoginRequestedEventArgs(string username, string password)
{
this.username = username;
this.password = password;
}
public string Username
{
get { return this.username; }
}
public string Password
{
get { return this.password; }
}
}
public interface ILoginService
{
bool Login(string username, string password);
}
Now we need to define the mock objects, so here's the Mock UI
internal class MockLoginView : ILoginView
{
private string username;
private string password;
private IList errors;
public MockLoginView() { }
public void LoginButtonPressed()
{
this.Username = "Rob";
this.Password = "fr34kyp455w0rd";
OnLoginRequest();
}
#region ILoginView Members
public event LoginRequestHandler LoginRequested;
public string Password
{
get { return password; }
set { this.password = value; }
}
public string Username
{
get { return this.username; }
set { this.username = value; }
}
public IList Errors
{
set { this.errors = value; }
}
#endregion
public string GetErrors()
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
foreach(string error in this.errors)
{
if (sb.Length > 0) sb.Append(", ");
sb.Append(error);
}
return sb.ToString();
}
protected void OnLoginRequest()
{
if (LoginRequested != null)
{
LoginRequested(this, new LoginRequestedEventArgs(this.username, this.password));
}
}
}
Notice, I've got a function LoginButtonPressed that simulates the user clicking on the login button in the UI.
Now the mock service object
internal class MockLoginService : ILoginService
{
public bool Login(string username, string password)
{
MD5CryptoServiceProvider hasher = new MD5CryptoServiceProvider();
UTF8Encoding enc = new UTF8Encoding();
string hashedPassword = enc.GetString(hasher.ComputeHash(enc.GetBytes(password)));
User user = new User(1, username, hashedPassword, (uint)Roles.Viewing);
return LoginManager.ValidateUser(user, password);
}
}
Glueing the View and Model together is the Presentation
public class LoginPresenter
{
private ILoginView view;
private ILoginService service;
private bool loggedIn;
public LoginPresenter(ILoginView view, ILoginService service)
{
this.view = view;
this.service = service;
view.LoginRequested +=new LoginRequestHandler(view_LoginRequested);
}
private void view_LoginRequested(object sender, LoginRequestedEventArgs e)
{
// go to the model and determine if able to login.
this.loggedIn = service.Login(e.Username, e.Password);
}
public bool LoggedIn
{
get { return this.loggedIn; }
}
}
In production code, the User object would be retrieved from the DAL (data access layer) by a method such as UserDataMapper.FindUserByUsername(username); which would either retrieve the object from disk, or retrieve it from memory (eg a Hashtable/Dictionary inside the UserDataMapper class), if it had previously been retrieved.
The problem with .NET
Visual Studio allows you to create TableAdapters, which "sort of" fill the role of the DataMappers ( I say "sort of" because, without getting sidetracked on the differences, there are differences ). Visual Studio also creates corresponding DataRow objects that represent business objects (or at least, that's the intent ;)). All this can be done through the tools in VS, without typing even one line of code.
Problem is, this doesn't easily separate the DAL from the business objects. Once again, unit testing is made more difficult.
Further Discussion
In unit test shown above, I test presenter.LoggedIn outside of the View. I chose this way because in practice, the Presentation should be a Page Controller (http://martinfowler.com/eaaCatalog/pageController.html ) which depending on the results of the Login Request would either redirect the user to the new page or return the user to the login page. Having navigation at this level is desirable because it removes the decision from the UI. Which is a good thing when you want to abstract the view.
Another way to implement this pattern is to maintain a reference to the presenter inside the view and then call presenter.LoggedIn.
As with any design, it's always a matter of weighing up the pro's and con's of each and coming to a decision that best suits the particular system under construction.