Search This Blog

Loading...

Friday, September 26, 2008

Testing ASP.NET MVC using Typemock AAA Syntax

ASP.NET MVC is great, I think by now everyone agrees with this statement. It makes unit testing so easy and so natural. There are three components inside MVC , namely view, model and controller. View is the output, usually it's the HTML, but it can be other things. Model, on the other hand, is the data access layer, your data access logic, including what database to access should go here.Controller, on the other hand, binds the view and the model together. With three loosely-coupling components, MVC makes unit testing easier.

AAA syntax stands for Arrange, Act and Assert . It is a way to make a test readable, maintainable and convenient. Traditionally, unit testing has been plagued with the problem of test coupling, much like object coupling where a test method tries to test too many unrelated things. The idea of mocks and stubs is to keep the tests simple and focus on what they should do by mocking away all the external dependencies in the production code. But mocking and stub generate their own sets of problem, a lot of people don't seem to really get it, so, the guys at Typemock try to do away with the mocks and stubs concepts altogether by giving us this AAA concepts.

What a lofty goal.

I tried AAA on windows form, and on ASP.NET MVC, and I was really please with what I got. So, in this post I am going to show you how can we test ASP.NET MVC code by using Typemock's AAA syntax. AAA ensures the separation of concern for unit test code. Which means that the data access test code only tests the data access logic, the controller test code tests only controller logic, and the view test code tests... well, we don't test view anyway ( or, rather, we don't know how to unit test the view, yet).

To keep things simple, let's say we are going to implement a custom made login, registration functionalities. In line with the tradition of MVC, we have the view, the controller and the model. I am not going to show view, since we can't test the view. For model, we are going to define a class named LINQMembershipProvider, inherited from MembershipProvider. So, all your user validation, registration, password retrieval should go here.
public class LINQMembershipProvider: MembershipProvider
{

public override bool ValidateUser(string username, string password)
{

LINQDataContext dc = new LINQDataContext ();
UserInfo user=dc.UserInfos.SingleOrDefault< userinfo> (uit => uit.Diggers == userName);

if (user == null)
{
return false;
}
return (user.password == password);
}

}


I used LINQ ( the DataContext) to access the database. You can easily read up the information on LINQ on the Internet. Notice that by subclassing MembershipProvider, we are independent of the database and data access strategy. If you don't like LINQ, or you don't like SQL server, you can subclass your own MembershipProvider and write your own logic for data access. The other layer won't change. This is the power of separation of concern.

For controller, we are going to define a class named UserInfoController, inherited from Controller.

public class UserInfoController : Controller
{
public UserInfoController(IFormsAuthentication formsAutho):this (formsAutho, null)
{

}
public UserInfoController():this(null, null)
{

}
public UserInfoController(IFormsAuthentication formsAuth, MembershipProvider provider)
{
FormsAuth = formsAuth ?? new FormsAuthenticationWrapper();
Provider = provider ?? Membership.Provider;
}


public ActionResult Login(string username, string password, bool? rememberMe)
{
//RedditMembershipProvider Provider = (RedditMembershipProvider)Membership.Provider;
ViewData["Title"] = "Login";

// Non-POST requests should just display the Login form
if (Request.HttpMethod != HttpMethod.Post)
{
return View();
}

// Basic parameter validation
List< string>  errors = new List< string> ();

if (String.IsNullOrEmpty(username))
{
errors.Add("You must specify a username.");
}

if (errors.Count == 0)
{

// Attempt to login
bool loginSuccessful = Provider.ValidateUser(username, password);

if (loginSuccessful)
{

FormsAuth.SetAuthCookie(username, rememberMe ?? false);
return RedirectToAction("Main", "Item");
}
else
{
errors.Add("The username or password provided is incorrect.");
}
}

// If we got this far, something failed, redisplay form
ViewData["errors"] = errors;
ViewData["username"] = username;
return View();

}

}


Notice how the controller works? It doesn't touch the database; instead, it calls an instance of MembershipProvider, and lets it deals with the data access thing. Also, it doesn't really mess with the view; it only tells the view what is to be expected.

How to test this code? With AAA syntax, it's very easy. We can test the model and the controller separately. Here's the test code for the model, namely the MembershipProvider class:

[TestFixture]
public class MyMembershipTest
{
MembershipProvider provider;
public RedditMembershipTest()
{

}

[SetUp]
public void Init()
{
provider = Membership.Provider;
}

[RowTest, RollBack]
[Row("Joseph", "Joseph")]
public void ValidationTestPass(string username, string password)
{
Assert.IsTrue(provider.ValidateUser(username, password));
}
}

I've defined Membership.Provider as an instance of MyMembershipProvider in the app.config file. For this layer, I let it touches the test database and return the actual results. So you can see that I test the results from the database explicitly. You can use a different database and test your data logic.

Now, here comes the interesting part, the code to test the controller:

[TestFixture]
public class UserInfoControllerTest2
{
public UserInfoControllerTest2()
{

}
private UserInfoController controllerFake;
private HttpRequestBase requestFake;
private UserInfoController controller;
private IFormsAuthentication formAuthenFake;
private RedditMembershipProvider providerFake;

[SetUp]
public void Init()
{
controllerFake = Isolate.Fake.Instance<userinfocontroller>(Members.CallOriginal);
Isolate.Swap<userinfocontroller>().With(controllerFake);

requestFake = Isolate.Fake.Instance<httprequestbase>(Members.MustSpecifyReturnValues);
Isolate.WhenCalled(() => requestFake.HttpMethod).WillReturn(HttpMethod.Post);
Isolate.WhenCalled(() => controllerFake.Request).WillReturn(requestFake);

formAuthenFake = Isolate.Fake.Instance<iformsauthentication>(Members.ReturnNulls);
Isolate.WhenCalled(() => controllerFake.FormsAuth).WillReturn(formAuthenFake);

providerFake = Isolate.Fake.Instance<redditmembershipprovider>(Members.ReturnNulls);
Isolate.WhenCalled(() => providerFake.Name).WillReturn("RedditMembershipProvider");
Isolate.WhenCalled(() => controllerFake.Provider).WillReturn(providerFake);

controller = new UserInfoController();



}

[TearDown]
public void Clean()
{

}

[Isolated]
[RowTest, RollBack]
[Row("Joseph", "Joseph")]
public void LoginTest(string username, string password)
{
Isolate.WhenCalled(() => providerFake.ValidateUser(username, password)).WillReturn(true);
RedirectToRouteResult result = (RedirectToRouteResult)controller.Login(username, password, false);
Assert.AreEqual("Item", (result).Values["controller"]);
Assert.AreEqual("Main", (result).Values["action"]);
Isolate.Verify.WasCalledWithExactArguments(() => providerFake.ValidateUser(username, password));
Isolate.Verify.WasCalledWithExactArguments(() => formAuthenFake.SetAuthCookie(username, false));
}


}


You can read up the AAA syntax here . Notice that in the test code, we don't care the results we got from the database; all we care is whether we call the correct method ( with the correct parameters) in order to get the database results (providerFake.ValidateUser). If you are concerned about the model's correctness you should write expectation for the Membership class in MyMembershipTest . Also, note that, we don't care how the output is laid out (that's the job of the view). All we concern is whether we are giving the correct information to the view or not.

So, by properly mirroring the test code along the structure for MVC, one can increase the readability of the test code significantly.

You can download the source code from here . In fact, I created this post by taking a part of the source code.

Questions, comments are all welcomed!

3 comments:

Doron said...

This is a great example of the Isolator AAA syntax usage! Couldn't put it better myself.

In order to keep the test simpler and more readable (which is a major goal for us), you can compact your test further by using one of these techniques:

Chains:

instead of this call block:
requestFake = Isolate.Fake.Instance(Members.MustSpecifyReturnValues);
Isolate.WhenCalled(() => requestFake.HttpMethod).WillReturn(HttpMethod.Post);
Isolate.WhenCalled(() => controllerFake.Request).WillReturn(requestFake);

you can set up this behavior in one command chain:
Isolate.WhenCalled(() => controllerFake.Request.HttpMethod).WillReturn(HttpMethod.Post);

Recursive fakes: If you need your fake object as an entry point to return other fake objects, you should use recursive fakes - your fake object will create and return fake objects for method return values (see our creating fakes blog post).

This means that instead of specifically calling WhenCalled() for each fake you want controllerFake to return, you can simply create controllerFake by doing:

Isolate.Fake.Instance<UserInfoController >(Members.ReturnRecursiveFakes).

Lastly, thanks for the feedback - I would love to hear more (including criticism!). I hope we can keep making test writing simpler and cleaner, and even achieve some 'lofty goals' along the way :)

Doron,
Typemock development team

Soon Hui said...

Thanks, Doron!

Recursive fake is indeed a great idea, will update my code to use it~~

ulu said...

Off-topic: You might want to try using Ivonna (http://sm-art.biz/Ivonna.aspx) to unit test your views. I haven't tried it with MVC, but it should work. Just mock the controller with TypeMock, and inspect the output.

However, since raw html is so hard to test (as opposed to nice and structured WebForms), I'd rather tag it as "virtually untestable". With all my respect to MVC, this is definitely one of its weak points.

Back to the topic: I totally agree that AAA is the way to go. Recently I'm trying to abandon my current, much more chaotic, way of writing tests. Here's the pattern I find very useful:

1. Put your Arrange part to the base test class. This includes all mock setups, like setting up results for ValidateUser().
2. Put your Act part to the test class' Setup method. Example: controller.Login(..).
3. Each test method should contain a single assert. If possible, make it one line of code (applies to the Act part as well). This includes Verify.WasCalled calls: each one deserves its own test method.

The benefits are numerous. The clear distinction between the A's helps in understanding the test code. Small test methods make testing more granular. It is easier to make the small test pass, it is easier to see what's wrong then it fails. The test methods can be named according to user stories, e.g., rather than LoginTest, we can have Login_Redirects_To_ItemController and Login_ShouldCall_ValidateUser methods.

ulu