Wednesday, February 22, 2012

Write your own blog engine, part 1

Introduction

I've decided that I need to write another book about TDD programming. The project I've chosen is a blogging engine, per Rob Conery's suggestion - it should be simple enough that I don't get bogged down in details, but complex enough not to be perceived as a toy object.

What is the minimal feature set? I've decided to start with these features:

  • Display the blog title
  • Display the most recent five posts - date, title and content
  • Login (especially since it's required by the next features)
  • Add a new post
  • Delete an existing post
I will develop this blog engine using ASP.NET MVC 3 and the tests will use the MSTest framework. Start by creating a solution with two projects, an empty MVC 3 application (I've called it MyBlogEngine.MVC) and a test project (MyBlogEngine.Tests). Make sure to add a reference to the blog engine from the test project.

I will try to make sure the design of the application stays clean at all times. In particular:

  1. All dependencies between classes will be passed in constructors
  2. All dependencies that are services ("injectables", in Misko Hevery's terminology) will be exposed as interfaces

Coding, more precisely designing code, is always a matter of choices. Given that I'm using TDD I'll try to keep as simple as possible, following YAGNI (You Ain't Gonna Need It) until I am sure I actually need it. I’ve read on Jeff Atwood’s blog about someone who started this project – writing his own blog engine – five times; each time he got sidetracked by “frameworkitis”, the attempt to develop frameworks that might be useful for every feature imaginable... while never actually getting as far as posting an article. I will attempt not to get bitten by the same problem.

I will confess to being completely dead to pleasant (visible) design; you will need to "prettify" the results yourself, but the functionality should work as expected. I will create a distinct class for acceptance tests and write these tests to exercise the complete application, end to end. Since that means manipulating the resulting HTML I will use the HTML Agility pack to make it easier; right-click the References folder in the MyBlogEngine.Tests project, choose "Manage NuGet Packages...", select Online / All on the left hand side and then enter "agility" in the search box in the top right. Select the HtmlAgilityPack package from the list and click Install.

That being said, I'll start with the first feature: displaying the blog title. Create an AcceptanceTests class and copy / type in the following code:

[TestClass]
public class AcceptanceTests
{
  private const string BASE_URL = "http://localhost:63516/";

  [TestMethod]
  public void HomePageHasCorrectTitle()
  {
    using (var web = new WebClient())
    {
      var html = web.DownloadString(BASE_URL);

      var doc = new HtmlDocument();
      doc.LoadHtml(html);
      var title = doc.DocumentNode.SelectSingleNode("/html/head/title").InnerText;
      Assert.AreEqual("MyBlogEngine", title);
    }
  }
}

I am using ReSharper; it makes my life easier. In particular, it tells me that I need to add a reference to System.Xml and the following using clauses:

using System.Net;
using HtmlAgilityPack;
using Microsoft.VisualStudio.TestTools.UnitTesting;

I will not specify the required usings each time; I hope that won't be too big of a problem.

Press F5 to launch the MVC application but then go back to Visual Studio and stop the debugger. Also, the "63516" port shown above is the port my Visual Studio decided to listen to; either change yours to match or change the code.

Ok, time to run the test. You can use Ctrl-R, A to run all the tests in the solution or you can click on the corresponding button in the tests toolbar. The test fails with the error "The remote server returned an error: (404) Not Found." This is good - I haven't created the home controller yet so it should error out. I should fix this, but I don't want to write production code without unit tests to guard it.

Unit tests, in contrast to acceptance tests:

  1. Only test a small portion of the code (usually a method)
  2. Do not access the network, the database or really anything except the class being tested.

Per MVC conventions, the home page is returned by the method Index in the HomeController class so add a new HomeControllerTests class to the test project:

[TestClass]
public class HomeControllerTests
{
}

The first test should make sure that the "/" page returns something (note that you will need to add a reference to System.Web.Mvc from the test project):

[TestMethod]
public void IndexReturnsView()
{
  var sut = new HomeController();

  var result = sut.Index() as ViewResult;

  Assert.IsNotNull(result);
}

In order to make the project compile I'll need to add the controller. Right-click on the Controllers folder in the MyBlogEngine.MVC project and select Add / Controller. Type "Home" (so that the name of the controller is HomeController) and choose the "Empty Controller" template. Delete the extra crud that Visual Studio insists on adding and leave the class looking like this:

public class HomeController : Controller
{
  public ActionResult Index()
  {
    return View();
  }
}

Now go back to the unit test, add the required using clause to make it pass and run the tests again. Note that the unit test passes (the method does return a view) but the acceptance test fails, this time with "The remote server returned an error: (500) Internal Server Error." That's because when the application tried to render the view, it couldn't find it. Fixing that problem is easy: create a new folder under Views (called Home) and then add a new Index view to it. Leave the contents of the view unchanged and re-run the tests.

The acceptance test is still failing but this time with a much better error message: "Expected:<MyBlogEngine>. Actual:<Index>." All that remains to be done is to fix this; change the contents of the Index view to:

@{
    ViewBag.Title = "MyBlogEngine";
}

<h2>@ViewBag.Title</h2>

This time all the tests pass. We've reached the first milestone!

1 comment:

Gaurav Neautiyal said...

HIi, thanks You So much...