Saturday, March 13, 2010

TDD kata for ASP.NET MVC controllers (part 2)

The purpose of this kata is to get practice in working with each ActionResult type that is returned from the controller. As before, the controller is tested in isolation (you do not create an actual ASP.NET MVC project, just a class library containing controllers and references to System.Web.Mvc).

In part 1, you set up the test project and related projects, and worked with the following return types derived from ActionResult:
1) ViewResult with ViewData
2) ViewResult with ViewData.Model
3) The same, with a mocked repository
4) RedirectToRouteResult

In part 2, we work with the following additional return types derived from ActionResult:
* RedirectToRouteResult, with TempData
* PlainText (just a string)
* ContentResult (as plain text)
* ContentResult (as RSS feed)
* JsonResult
* JavaScriptResult

If you haven't already completed part 1 of this kata, you can go there now by clicking here.

With part 1 completed, you should have a solution with 4 projects (Tests.Unit, Controllers, Domain, and Repository.) Your tests and controllers projects should have references to System.Web.Mvc and System.Web.Mvc.Routing; all of your controllers should inherit from the Controller base class.

You are now ready to proceed forward into part 2. I'll continue the numbering for tests and steps as of end of part 1:

Test #5 (RedirectToRouteResult with TempData)
17) In HelpTopicsController.cs class, modify the existing method call to also assign a TempData message.
18) In the existing test, assert that the controller instance TempData property has an expected key/value pair, for example: sut.TempData["message"] = "My cross-page message."
Test #6 (Plain Text - just a string)
19) Create TextOutputControllerTests.cs class.
20) Create test for TextOutputController.
21) Call TextOutputController.ShowPlainText(string text) and return the string parameter directly (no casting.)
22) Assert that the returned method string matches input parameter string.
Test #7 (Plain Text as ContentResult)
23) Create another test for TextOutputController.
24) Call TextOutputController.ShowContentResultPlainText(string text) and return Content() as ActionResult.
25) In test, cast the method return to ContentResult.
26) Assert that contentResult.ContentType = "text/plain".
27) Assert that contentResult.Content matches the input string.
Test #8 (RSS feed as ContentResult)
28) Create another test for TextOutputController.
29) Create instance of XDocument() named rssFeed.
30) Call TextOutputController.ShowContentResultRSS(XDocument rssFeed) and return Content(XDocument rssFeed, string contentType) as ContentResult.
31) In test, cast the method return to ContentResult.
32) Assert that contentResult.ContentType = "application/rss+xml".
33) Assert that contentResult.Content matches rssFeed.
Test #9 (JsonResult)
34) Create JsonOutputControllerTests.cs class.
35) Create a test for JsonOutputController.
36) Create instance of List (you created Customer in the Domain namespace in part 1.
37) Use object initializers to add FirstName and LastName property values to 2 instances of Customer in this list.
38) Call JsonOutputController.ShowJsonResult(List customers) and return Json(customers) as ActionResult.
39) In test, cast the method return to JsonResult.
39) In test, cast jsonResult.Data to List with variable name: jsonCustomers.
40) Assert that customers[0] is same as jsonCustomers[0]. (To do this, you will need to override Equals() and GetHashCode() on Customers where equality is based on FirstName and LastName properties.)
Test #10 (JavascriptResult)
41) Create JavaScriptOutputControllerTests.cs class.
42) Create a test for JavaScriptOutputController.
43) Create a simple JavaScript and assign to string javaScriptString eg. "alert('Greetings!');"
44) Call JavaScriptOutputController.OutputJavaScript(string javaScriptString) and return JavaScript(javaScriptString) as JavaScriptResult.
45) Assert that javaScriptString and javaScriptResult.Script are equal.

This demonstrates all returned ActionResult types from controllers (other than file/streaming output types) for ASP.NET MVC v1.

Below are code samples for each of the above tests and controllers.

HelpTopicsControllerTests.cs

[TestFixture]
public class HelpTopicsControllerTests
{
    [Test]
    public void GetHelpMethod_TopicInput_ReturnsRedirectRouteValues()
    {
        const string helpTopic = "searching";
        var sut = new HelpTopicsController();
        var redirectToRouteResult = (RedirectToRouteResult)sut.GetHelp(helpTopic);

        Assert.AreEqual("helpTopic", redirectToRouteResult.RouteValues["controller"]);
        Assert.AreEqual("index", redirectToRouteResult.RouteValues["action"]);
        Assert.AreEqual(helpTopic, redirectToRouteResult.RouteValues["helpTopic"]);
    }

    [Test]
    public void GetHelpWithInfoMethod_TopicInput_ReturnsRedirectTempData()
    {
        const string helpTopic = "searching";
        const string additionalMessage = "My cross-page message";
        var sut = new HelpTopicsController();
        var redirectToRouteResult = (RedirectToRouteResult)sut.GetHelpPlusAdditionalMessage(helpTopic, additionalMessage);

        Assert.AreEqual(additionalMessage, sut.TempData["message"]);
    }
}


HelpTopicsController.cs

public class HelpTopicsController : Controller
{
    public RedirectToRouteResult GetHelp(string helpTopic)
    {
        return RedirectToAction("index", "helpTopic", new { helpTopic = helpTopic });
    }

    public RedirectToRouteResult GetHelpPlusAdditionalMessage(string helpTopic, string additionalMesage)
    {
        TempData["message"] = additionalMesage;
        return RedirectToAction("index", "helpTopic", new { helpTopic = helpTopic });
    }
}


TextOutputControllerTests.cs

[TestFixture]
public class TextOutputControllerTests
{
    [Test]
    public void ShowPlainTextMethod_NoInputParameters_ReturnsPlainText()
    {
        var sut = new TextOutputController();
        const string somePlainText = "This is plain text";
        var plainTextOutput = sut.ShowPlainText(somePlainText);

        Assert.AreEqual(somePlainText, plainTextOutput);
    }

    [Test]
    public void ShowContentResultPlainTextMethod_NoInputParameters_ReturnsContentResultAsTextPlain()
    {
        var sut = new TextOutputController();
        const string somePlainText = "This is plain text";
        var contentResult = (ContentResult)sut.ShowContentResultPlainText(somePlainText);

        Assert.AreEqual("text/plain", contentResult.ContentType);
        Assert.AreEqual(somePlainText, contentResult.Content);
    }

    [Test]
    public void ShowContentResultRSSMethod_RSSFeedInputParameters_ReturnsContentResultAsRSS()
    {
        var sut = new TextOutputController();
        var rssFeed = new XDocument();
        var contentResult = (ContentResult)sut.ShowContentResultRSS(rssFeed);

        Assert.AreEqual("application/rss+xml", contentResult.ContentType);
        Assert.AreEqual(rssFeed.ToString(), contentResult.Content);
    }
}


TextOutputController.cs

public class TextOutputController : Controller
{
    public string ShowPlainText(string text)
    {
        return text;
    }
    
    public ActionResult ShowContentResultPlainText(string text)
    {
        return Content(text, "text/plain");
    }

    public ContentResult ShowContentResultRSS(XDocument rssFeed)
    {
        return Content(rssFeed.ToString(), "application/rss+xml");
    }
}


JsonOutputControllerTests.cs

[TestFixture]
public class JsonOutputControllerTests
{
    [Test]
    public void ShowJsonResultMethod_CustomerInputParameters_ReturnsJsonArray()
    {
        var sut = new JsonOutputController();
        var customers = new List
                            {
                                new Customer {FirstName = "June", LastName = "Jones"},
                                new Customer {FirstName = "Gary", LastName = "Li"}
                            };
        var jsonResult = (JsonResult)sut.ShowJsonResult(customers);
        var jsonCustomers = (List)jsonResult.Data;

        Assert.AreEqual(customers[0], jsonCustomers[0]);
        Assert.AreEqual(customers[1], jsonCustomers[1]);
    }
}


Customer.cs

public class Customer
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public override bool Equals(object obj)
    {
        var other = (Customer) obj;
        return other.FirstName.Equals(this.FirstName)
               && other.LastName.Equals(this.LastName);
    }

    public override int GetHashCode()
    {
        return this.FirstName.GetHashCode() + this.LastName.GetHashCode();
    }
}


JsonOutputController.cs

public class JsonOutputController : Controller
{
    public ActionResult ShowJsonResult(List customers)
    {
        return Json(customers);
    }
}



JavaScriptOutputControllerTests.cs

[TestFixture]
public class JavaScriptOutputControllerTests
{
    [Test]
    public void GreetingsMethod_NoInput_ReturnsJavaScriptResult()
    {
        var sut = new JavaScriptOutputController();
        const string javaScriptString = "alert('Greetings!');";
        var javaScriptResult = sut.OutputJavaScript(javaScriptString);

        Assert.AreEqual(javaScriptString, javaScriptResult.Script);    
    }
}


JavaScriptOutputController.cs

public class JavaScriptOutputController : Controller
{
    public JavaScriptResult OutputJavaScript(string javaScriptString)
    {
        return JavaScript(javaScriptString);
    }
}

2 comments:

Denis said...

Hello,

I'm doing this asp.net mvc tdd kata.
I think it is very useful.

Looking forward for part 3 :)

Kevin said...

I was wondering how to test the TempData was set when the action returned a RedirectToResult (which doesn't have a TempData property). Thanks for pointing out that it's on the controller in your example. It makes sense now.