ASP.NET April 05, 2010

Performing and Testing Redirects with ASP.NET Web Forms MVP

I’ve been playing around with the ASP.NET Web Forms MVP project lately. One of the things that has been notoriously hard to test with Web Forms, at least when it comes to unit testing, is redirects.

I’ve been playing around with the ASP.NET Web Forms MVP project lately trying to find a better way to work with Web Forms applications when switching to ASP.NET MVC isn’t a viable option. One of the things that has been notoriously hard to test with Web Forms, at least when it comes to unit testing, is redirects. MVC solves this beautifully with it’s RedirectResult class. Web Forms MVP doesn’t have such a pretty solution but since it uses the fairly new HttpContextBase class instead of the concrete HttpContext class it does make redirects testable.

Let’s look at a simple, and silly, example. We have a presenter named SquareRootPresenter that calculates the square root of a number inputted in the view. Since we are superstitious we don’t want to calculate the square root of the number 666 but will then instead redirect the visitor to Google.

public class SquareRootPresenter 
    : WebFormsMvp.Presenter<ISquareRootView>
{
    public SquareRootPresenter(ISquareRootView view)
        : base(view)
    {
        View.Calculate += View_Calculate;
    }

    public void View_Calculate(int argument)
    {
        if(argument == 666)
            HttpContext.Response.Redirect(
                "http://www.google.se/search?q=exorcism");

        View.Model.Result = Math.Sqrt(argument);
    }

    public override void ReleaseView()
    {
    }
}

How do we create a unit test that ensures that the View_Calculate method will perform the redirect? Since it’s using the HttpContext property of the presenter which is an instance of HttpContextBase we can create test doubles for that, along with it’s Response property which is an instance of HttpResponseBase. Using xUnit.net and manually created test doubles we can implement the test like this:

public class SquareRootPresenterTests
{
    [Fact]
    public void WhenCalculateInputIsNumberOfTheBeast_RedirectToGoogle()
    {
        ISquareRootView view = new FakeSquareRootView();
        SquareRootPresenter presenter = 
            new SquareRootPresenter(view);
        presenter.HttpContext = new FakeHttpContext();

        presenter.View_Calculate(666);

        Assert.Equal("http://www.google.se/search?q=exorcism", 
            presenter.HttpContext.Response.RedirectLocation);
    }

    public class FakeSquareRootView : ISquareRootView
    {
        public event Action<int> Calculate;

        public event EventHandler Load;

        public SquareRootModel Model
        {
            get; set;
        }
    }

    public class FakeHttpContext : HttpContextBase
    {
        private FakeHttpResponse response = new FakeHttpResponse();
        public override HttpResponseBase Response
        {
            get
            {
                return response;
            }
        }
    }

    public class FakeHttpResponse : HttpResponseBase
    {
        public override void Redirect(string url)
        {
            RedirectLocation = url;
        }

        public override string RedirectLocation
        {
            get; set;
        }
    }
}

Using an isolation framework such as Moq we can also write the test like this:

public class SquareRootPresenterTests
{
    [Fact]
    public void WhenCalculateInputIsNumberOfTheBeast_RedirectToGoogle()
    {
        ISquareRootView view = CreateFakeView();
        Mock<HttpResponseBase> mockResponse = CreateMockResponse();
        HttpContextBase fakeContext = CreateFakeContext(mockResponse.Object);
        SquareRootPresenter presenter = CreateFakePresenter(view, fakeContext);

        presenter.View_Calculate(666);

        mockResponse.Verify(response => 
            response.Redirect("http://www.google.se/search?q=exorcism"));
    }

    private ISquareRootView CreateFakeView()
    {
        Mock<ISquareRootView> fakeView = new Mock<ISquareRootView>();
        fakeView.SetupGet(view => view.Model).Returns(new SquareRootModel());
        return fakeView.Object;
    }

    private Mock<HttpResponseBase> CreateMockResponse()
    {
        Mock<HttpResponseBase> fakeResponse = new Mock<HttpResponseBase>();
        fakeResponse.Setup(response =>
                           response.Redirect(It.IsAny<string>()));
        return fakeResponse;
    }

    private HttpContextBase CreateFakeContext(HttpResponseBase response)
    {
        Mock<HttpContextBase> fakeContext = new Mock<HttpContextBase>();
        fakeContext.SetupGet(context => context.Response)
            .Returns(response);
        return fakeContext.Object;
    }

    private SquareRootPresenter CreateFakePresenter(ISquareRootView view, HttpContextBase fakeContext)
    {
        SquareRootPresenter presenter =
            new SquareRootPresenter(view);
        presenter.HttpContext = fakeContext;
        return presenter;
    }
}

Compared to the ease with which we can test redirects with ASP.NET MVC all this mocking-black-magic that we have to do is a bit annoying, but considering that we’re dealing with Web Forms I think it’s definitely a step forward compared to not being able to test redirects at all (with unit tests), or having to create a bunch of abstraction layers to test them. We can and should of course also create helper classes for this type of test if we have several of them, making each test require a lot less code.

Stopping execution after the redirect

UPDATE: After I first posted this article Tatham Oddie pointed out that when running our code using our tests execution will continue after the call to Redirect() forcing us to make sure that it can execute in order to not break our tests. Normally the HttpResponse class’ Redirect method would would call Response.End() which in turn throws an ThreadAbortException which the framework then catches and ignores to break the execution. We can do something similar by instructing our mock Response class to throw an exception when Redirect() is called to break execution and then asserting that it has been thrown in our test, like this:

public class SquareRootPresenterTests
{
    [Fact]
    public void WhenCalculateInputIsNumberOfTheBeast_RedirectToGoogle()
    {
        ISquareRootView view = CreateFakeView();
        Mock<HttpResponseBase> mockResponse = CreateMockResponse();
        HttpContextBase fakeContext = CreateFakeContext(mockResponse.Object);
        SquareRootPresenter presenter = CreateFakePresenter(view, fakeContext);

        RedirectException exception = Record.Exception(
            () => presenter.View_Calculate(666)) as RedirectException;

        Assert.NotNull(exception);
        mockResponse.Verify(response => 
            response.Redirect("http://www.google.se/search?q=exorcism"));
    }

    private ISquareRootView CreateFakeView()
    {
        Mock<ISquareRootView> fakeView = new Mock<ISquareRootView>();
        return fakeView.Object;
    }

    private Mock<HttpResponseBase> CreateMockResponse()
    {
        Mock<HttpResponseBase> fakeResponse = new Mock<HttpResponseBase>();
        fakeResponse.Setup(response =>
                           response.Redirect(It.IsAny<string>())).Throws(new RedirectException());
        return fakeResponse;
    }

    private HttpContextBase CreateFakeContext(HttpResponseBase response)
    {
        Mock<HttpContextBase> fakeContext = new Mock<HttpContextBase>();
        fakeContext.SetupGet(context => context.Response)
            .Returns(response);
        return fakeContext.Object;
    }

    private SquareRootPresenter CreateFakePresenter(ISquareRootView view, HttpContextBase fakeContext)
    {
        SquareRootPresenter presenter =
            new SquareRootPresenter(view);
        presenter.HttpContext = fakeContext;
        return presenter;
    }

    public class RedirectException : Exception
    {
    }
}

PS. For updates about new posts, sites I find useful and the occasional rant you can follow me on Twitter. You are also most welcome to subscribe to the RSS-feed.

Joel Abrahamsson

Joel Abrahamsson

I'm a passionate web developer and systems architect living in Stockholm, Sweden. I work as CTO for a large media site and enjoy developing with all technologies, especially .NET, Node.js, and ElasticSearch. Read more

Comments

comments powered by Disqus

More about ASP.NET