Dashboard > MonoRail > Home > TDDingControllers
TDDingControllers
Added by hamilton verissimo, last edited by Dusty Candland on Apr 04, 2008  (view change)
Labels: 
(None)


Test-driven development (or TDD for short) is an agile practice that dictates that tests must be written first. Code must be added to make the test pass, and finally you have a ground so you can refactor and/or review the implementation. Combined with "deliver the simplest thing that could possibly work", TDD is a valuable practice to be focused on what is important for the iteration.

For quite sometime, MonoRail offered the TestSupport that allowed you to test any piece of code by faking requests. While this works, it's heavyweight and flawed in a variety of aspects.

  • It uses the ASP.Net Hosting runtime, so it starts a virtual web server (without the TCP layer though) and creates a HttpWorkerRequest that starts the whole ASP.Net pipeline. The good thing about it is that your code, albeit in a test environment, won't notice it's not on a real web server. However that has a cost, the time it takes to set up and run
  • We cannot override the web.config, so it's difficult to configure anything for a "test environment"
  • Sometimes it just fails. It's hard to rely on something that is not deterministic. A build failure is an important event, and the infrastructure needs to be accountable.

So a more simple and lightweight option is to use MonoRail's interfaces to fake a context (environment), request and response. Some people use mocking tools to achieve that. We - at Castle Stronghold - have also used that, but we don't think it's a good thing to force people to use an specific tool. So we have developed mocked classes and a base test class (optional, only there for your convenience), so you can start to write tests for your controllers in an clean and easy way.

Our first test

Let's develop a controller to handle a product search. Bear in mind that when you are doing TDD, you must design for testability. It won't happen accidentally. You also must be concerned about testing the interactions your object has with others, instead of always doing end to end testing.

So we would like to test a non-existent controller called ProductSearchController.

ProductSearchControllerTestCase.cs
[TestFixture]
public class ProductSearchControllerTestCase
{
    private ProductSearchController searchController;

    [SetUp]
    public void Init()
    {
        searchController = new ProductSearchController();
    }

    [Test]
    public void PerformSearch()
    {
        searchController.Search("lipitor");

        Assert.IsNotNull(searchController.PropertyBag["results"]);

        Product[] products = (Product[]) searchController.PropertyBag["results"];

        Assert.AreEqual(2, products.Length);

        foreach(Product prod in products)
        {
           Assert.Contains("lipitor", prod.Name);
        }
    }
}

At this point the code won't even compile. So the next step is making it compile. So we add the controller to our web project:

ProductSearchController.cs
public class ProductSearchController : SmartDispatcherController
{
    public void Search(string terms)
    {
       PropertyBag["results"] = new Product[] { new Product("lipitor"), new Product("lipitor") };
    }
}

At this point, if you're not familiar with TDD, you might be wondering if I should reconsider my career choice. Rest assured, that is fine. We need to make the test pass first. If performing a product search was something obvious we could have done it directly. But it isn't. So we add a code to make the test pass, then we refactor to a more correct implementation.

However, at this point, your test code is not passing. Hmm. Not good..

Enters the new test support.

All you have to do is

  1. Make your test class inherit from BaseControllerTest (Assembly Castle.MonoRail.Framework.TestSupport, same namespace)
  2. Use the PrepareController method before using the controller. This takes care of initializing the controller state to a good state, so it's safe to make operations on it.

So our code will look like this:

ProductSearchControllerTestCase.cs
using Castle.MonoRail.TestSupport;

[TestFixture]
public class ProductSearchControllerTestCase : BaseControllerTest
{
    private ProductSearchController searchController;

    [SetUp]
    public void Init()
    {
        searchController = new ProductSearchController();
        PrepareController(searchController, "", "productsearch", "search");
    }

    [Test]
    public void PerformSearch()
    {
        searchController.Search("lipitor");

        Assert.IsNotNull(searchController.PropertyBag["results"]);

        Product[] products = (Product[]) searchController.PropertyBag["results"];

        Assert.AreEqual(2, products.Length);

        foreach(Product prod in products)
        {
           Assert.Contains("lipitor", prod.Name);
        }
    }
}

All green! Right? If not, post a comment (see below).

Now you should refactor the controller code to use your preferred method to implement the product search.

How it works

It's really simple. We provide mock implementations of the context and services that MonoRail depends on. Bear with me, this is very new code, so you might incur in a situation where a NotImplementedException is throw. If so, please fill a jira issue with your code that demonstrates the problem.

The BaseControllerTest in this case just coordinates the creation, and expose a few protected virtual methods so you can customize anything you want. Follows its code:

BaseControllerTest.cs
public abstract class BaseControllerTest
{
	private readonly string domain;
	private readonly string domainPrefix;
	private readonly int port;
	private string virtualDir = "/";
	private MockRailsEngineContext context;

	protected BaseControllerTest() : this("app.com", "www", 80)
	{
	}

	protected BaseControllerTest(string domain, string domainPrefix, int port)
	{
		this.domain = domain;
		this.domainPrefix = domainPrefix;
		this.port = port;
	}

	protected void PrepareController(Controller controller, string areaName, string controllerName, string actionName)
	{
		if (controller == null)
		{
			throw new ArgumentNullException("controller", "'controller' cannot be null");
		}
		if (areaName == null)
		{
			throw new ArgumentNullException("areaName");
		}
		if (controllerName == null)
		{
			throw new ArgumentNullException("controllerName");
		}
		if (actionName == null)
		{
			throw new ArgumentNullException("actionName");
		}

		BuildRailsContext(areaName, controllerName, actionName);
		controller.InitializeFieldsFromServiceProvider(context);
		controller.InitializeControllerState(areaName, controllerName, actionName);
	}

	private void BuildRailsContext(string areaName, string controllerName, string actionName)
	{
		UrlInfo info = BuildUrlInfo(areaName, controllerName, actionName);
		IRequest request = BuildRequest();
		IResponse response = BuildResponse();
		ITrace trace = BuildTrace();
		context = BuildRailsEngineContext(request, response, trace, info);
	}

	protected virtual IRequest BuildRequest()
	{
		return new MockRequest();
	}

	protected virtual IResponse BuildResponse()
	{
		return new MockResponse();
	}

	protected virtual ITrace BuildTrace()
	{
		return new MockTrace();
	}

	protected virtual MockRailsEngineContext BuildRailsEngineContext(IRequest request, IResponse response, ITrace trace, UrlInfo urlInfo)
	{
		return new MockRailsEngineContext(request, response, trace, urlInfo);
	}

	protected virtual UrlInfo BuildUrlInfo(string areaName, string controllerName, string actionName)
	{
		return new UrlInfo(domain, domainPrefix, virtualDir, "http", port,
			Path.Combine(Path.Combine(areaName, controllerName), actionName), areaName, controllerName, actionName, "rails");
	}
}

What is missing?

Well, like I said, this is brand new code, so problems will certainly arise. Also we need something similar to test ViewComponents.

Blog posts

Any chance can include a more complicate test sample, for example a ARSmartDispatcherController with multi step controller action? (e.g. ShowAdd + Save)

Site running on a free Atlassian Confluence Community License granted to Castle Project. Evaluate Confluence today.
Powered by Atlassian Confluence, the Enterprise Wiki. (Version: 2.5.4 Build:#809 Jun 12, 2007) - Bug/feature request - Contact Administrators