Thursday, 26 March 2009

TDD with EPiServer

During the last Demo Day (conference organized by Cognifide every 3-4 months) I had a chance to talk about Test Driven Development in EPiServer projects. In this post I would like to recap main points of my presentation.

Obstacles on the way to TDD

Unfortunately it's very hard to apply TDD to ASP.NET WebForms as it is. WebForms was designed as a platform for Rapid Application Development. Features like wide set of rich controls and state management (view state) can significantly speed up development but in the same time WebForms does not provide decent separation of concerns. I think code-behind files were suppose to give us some separation between the HTML and application logic. And to be fair it's "some" separation, but we still need to deal in one code-behind file with ASP.NET components, which are tightly bound to the underlying HttpContext, presentation logic and usually a few other things. It's clearly too much. There are way too many concerns in one place, too many dependencies on ASP.NET runtime. It makes code in applications like this hard to maintain and also makes writing unit test impossible.

Another thing with which you have to cope with is EPiServer. To get an idea of problems which you may encounter check this unit tests:

   1:  [Test]
   2:  public void MyEPiServerUnitTest()
   3:  {
   4:      var page = new PageData();
   5:      page.Property.Add("MetaAuthor", new PropertyString { Value = "MetaAuthor123" });
   6:      page.Property.Add("Heading", new PropertyString { Value = "Heading123" });
   7:      page.Property.Add("MainBody", new PropertyXhtmlString { Value = "MainBody123" });
   8:  
   9:      Assert.IsTrue(page["MainBody"].Equals("MainBody123"));
  10:  }

Basically I'm creating PageData instance with a few properties and then I'm checking if correct values will be returned. This is probably the simplest unit test ever. So if you run it then you don't expect anything else then a green bar right? Surprisingly, instead of green bar you will get exception saying:

   System.ApplicationException: First time you call Settings.Instance you must have a valid HttpContext.

If you want to know who you should blame for calling Settings.Instance then check PropertyXhtmlString :)

As you may know, HttpContext can be mocked, it's not a problem. But in our case it won't help much, we won't get this exception of course, but we will get different one instead.

As you can see the way to TDD is not easy, EPiServer is also closely bound to HttpContext.

Deal with problems in two steps

Step 1 - Isolate as much functionality as you can away from ASP.NET runtime. How? Introduce design pattern called Model-View-Presenter (MVP). This pattern divides the responsibilities into separate classes:
  • View - those are *.aspx and *.ascx files including code-behind files. This layer should be as thin as possible, code-behind files should delegate as much functionality as possible to the Presenter.
  • Presenter is responsible for interface logic which includes initialization of view and dealing with all events. It's important that presenter shouldn't have any reference to System.Web namespace and should be separated from the concrete view with an interface. Additionally presenter has access to business layer and/or backend services.
  • Model - the data on which application is working on, for EPiServer those might be pages, users, files, data stored in the HttpSession etc.
It's worth mentioning that in general there are two variations of MVP design pattern:

There are minor differences between both versions, you can learn more about both versions on Martin Fowler's site: Passive View and Supervising Controller.

Let me show you how it works in practice. I'm going to implement well known news page from EPiServer's public templates, the page for visitors looks like this:


Areas which are set by news page are marked with red rectangles. Other data comes from MasterPage. Below you can check how the aspx file:

   1:  <asp:Content ID="Content4" ContentPlaceHolderID="MainBodyRegion" runat="server">
   2:      <div id="MainBody">
   3:          <h1><asp:Literal ID="litHeading" runat="server" /></h1>
   4:          <asp:Literal ID="litMainBody" runat="server" />
   5:      </div>    
   6:  </asp:Content>
   7:  
   8:  
   9:  <asp:Content ID="Content5" ContentPlaceHolderID="SecondaryBodyRegion" runat="server">
  10:   <div id="SecondaryBody">
  11:      <dl>
  12:          <dt><EPiServer:Translate ID="author" runat="server" Text="/news/writername" /></dt>
  13:          <dd><asp:Literal ID="litMetaAuthor" runat="server" /></dd>
  14:          <dt><EPiServer:Translate ID="Translate1" runat="server" Text="/news/publishdate" /></dt>
  15:          <dd><asp:Literal ID="litPageStartPublish" runat="server" /></dd>
  16:      </dl>
  17:  </div>
  18:  </asp:Content>

As you can see, so far nothing surprising. But now, instead of jumping to the code-behind file, you should create an interface for the view. I'm going to call my interface INewsView and this is how it looks like:

   1:  public interface INewsView
   2:  {
   3:      String Heading { get; set; }
   4:      String MainBody { get; set; }
   5:      String MetaAuthor { get; set; }
   6:      String PageStartPublish { get; set; }
   7:  }

INewsView interface defines a contract between presenter and view. View needs to get above data from presenter and presenter is obligated to deliver them.

Having the interface ready we can start working on the presenter, the simplest implementation looks like this:

   1:  public class NewsPresenter : AbstractPresenterBase<INewsView>
   2:  {
   3:      public override void Initialize()
   4:      {
   5:          View.MetaAuthor = PresenterUtils.GetPropertyValue(CurrentPage, "MetaAuthor", "");
   6:          View.PageStartPublish = PresenterUtils.GetProperty<DateTime>(CurrentPage, "PageStartPublish", "",
   7:                                                                       x => x.ToShortDateString());
   8:  
   9:          View.Heading = PresenterUtils.GetPropertyValue(CurrentPage, "Heading", "");
  10:          View.MainBody = PresenterUtils.GetPropertyValue(CurrentPage, "MainBody", "");
  11:      }
  12:  }

PresenterUtils class is just my helper which is returning values of properties if they are set, or default values otherwise. There is no magic here but as you may notice, logic dealing with CurrentPage is located in one place which makes it much more reusable.

Now we have NewsPresenter ready we can piece it all together. The only missing part is implementation of INewsView interface:


   1:  public partial class NewsItem : SimpleMVPPageBase<NewsPresenter, INewsView>, INewsView
   2:  {
   3:      protected override INewsView ViewImplementation
   4:      {
   5:          get { return this; }
   6:      }
   7:  
   8:      public String MetaAuthor
   9:      {
  10:          get { return litMetaAuthor.Text; }
  11:          set { litMetaAuthor.Text = value; }
  12:      }
  13:  
  14:      public String PageStartPublish
  15:      {
  16:          get { return litPageStartPublish.Text; }
  17:          set { litPageStartPublish.Text = value; }
  18:      }
  19:  
  20:      public string Heading
  21:      {
  22:          get { return litHeading.Text; }
  23:          set { litHeading.Text = value; }
  24:      }
  25:  
  26:      public string MainBody
  27:      {
  28:          get { return litMainBody.Text; }
  29:          set { litMainBody.Text = value; }
  30:      }
  31:  }

This class is located in code-behind file, it implements INewsView interface and represents a link between NewsPresenter and the view and actual ASP.NET controls.

What you can't see in this class is how the presenter is instantiated, when the view implementation is passed to the presenter, this is wrapped in SimpleMVPPageBase class:

   1:  public abstract class SimpleMVPPageBase<PresenterClass, ViewInterface> : SimplePage
   2:      where PresenterClass : AbstractPresenterBase<ViewInterface>, new()
   3:  {
   4:      protected PresenterClass Presenter;
   5:      protected abstract ViewInterface ViewImplementation { get; }
   6:  
   7:      protected override sealed void OnInit(EventArgs e)
   8:      {
   9:          base.OnInit(e);
  10:          Presenter = new PresenterClass
  11:          {
  12:              View = ViewImplementation,
  13:              CurrentPage = CurrentPage
  14:          };
  15:      }
  16:  
  17:      protected override sealed void OnLoad(EventArgs e)
  18:      {
  19:          base.OnLoad(e);
  20:          if (!IsPostBack)
  21:          {
  22:              Presenter.Initialize();
  23:              DataBind();
  24:          }
  25:      }        
  26:  }

SimpleMVPPageBase class is responsible for two things:
  • It instantiates presenter class, and sets for it CurrentPage and view implementation.
  • Also, within OnLoad, it calls Presenter.Initialize() method which should initialize the view.
With all those pieces in place, you should get a working page. So, in fact we are in the starting position but with this version we have significantly better separation of concerns and our logic is testable:


   1:  public class Checking_news_view_initialization : AbstractPresenterTest<NewsPresenter>
   2:  {
   3:      protected override void Before_each_test()
   4:      {
   5:          Presenter.CurrentPage = EPiServerPage.CreateBlankPage()
   6:              .AddProperty<PropertyString>("MetaAuthor", "author123")
   7:              .AddProperty<PropertyString>("Heading", "heading123")
   8:              .AddProperty<PropertyXhtmlString>("MainBody", "mainBody123")
   9:              .AddProperty<PropertyDate>(EPiProperties.PageStartPublish, DateTime.Today)
  10:              .Instance();
  11:  
  12:          Presenter.View = mocks.StrictMock<INewsView>(null);
  13:      }
  14:  
  15:      [Test]
  16:      public void It_should_set_all_properties()
  17:      {
  18:          using (mocks.Record())
  19:          {
  20:              Expect.Call(Presenter.View.Heading).SetPropertyWithArgument("heading123");
  21:              Expect.Call(Presenter.View.MainBody).SetPropertyWithArgument("mainBody123");
  22:              Expect.Call(Presenter.View.MetaAuthor).SetPropertyWithArgument("author123");
  23:              Expect.Call(Presenter.View.PageStartPublish).SetPropertyWithArgument(DateTime.Today.ToShortDateString());
  24:          }
  25:  
  26:          using (mocks.Playback())
  27:          {
  28:              Presenter.Initialize();
  29:          }
  30:      }
  31:  }


Within Before_each_test() method I'm basically creating an instance of PageData with a few properties. I'm also mocking the view using Rhino Mocks. Thanks to that in It_should_set_all_properties() method I can record my expectations and later verify them.

But wait a second ... as I was writing earlier, EPiServer requires valid HttpContext right?

Workaround for this issue is a step 2 on our way to TDD:

   1:  [TestFixture]
   2:  public abstract class AbstractPresenterTest<PresenterClass> where PresenterClass : new()
   3:  {
   4:      protected PresenterClass Presenter { get; private set; }
   5:      protected MockRepository mocks { get; private set; }
   6:  
   7:  
   8:      protected AbstractPresenterTest()
   9:      {
  10:          Presenter = new PresenterClass();
  11:      }
  12:  
  13:      [TestFixtureSetUp]
  14:      protected void SetUpContextForAWholeFixture()
  15:      {
  16:          var settings = new EPiServer.Configuration.Settings
  17:                             {
  18:                                 StringCompressionThreshold = 0
  19:                             };
  20:  
  21:          Type settingsType = settings.GetType();
  22:          settingsType.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)
  23:              .SetValue(settings, settings);
  24:      }
  25:  
  26:      [SetUp]
  27:      protected void SetUpContextForEachTest()
  28:      {
  29:          mocks = new MockRepository();
  30:          Before_each_test();
  31:      }
  32:  
  33:      protected abstract void Before_each_test();
  34:  }


HttpContext related exception won't be triggered if you create instance of Settings and using reflection assign it to the static and private field _instance on Settings class. This way you can also provide your configuration for tests.

Conclusions
  • the most important one is that Test Driven Development with EPiServer is possible!
  • you need to isolate as much functionality from ASP.NET runtime and MVP design patter is doing exactly that for you
  • with a little bit of hacking of EPiServer you can instantiate PageData and test your logic
  • MVP requires additional effort to create interfaces, presenters etc. Downside is that you won't be able to build web applications so rapidly but it should pay off in longer and more complex projects where having a set of automated tests is a big advantage.
Credits
  • My research and presentation were inspired by Raymond Lewallen who was a guest of Poznan .Net Group and was presenting Behavioral Driven Development approach.
  • I have used image from Microsoft site talking about MVP.

3 comments:

flalar said...

Very good post! Nice to see someone from the EPiServer community not only hacking away, but thinking soundly about testing and maintainability. Hope to see more of these types of posts in the future!

Keep up the good work!! Cheers

Dave Warren said...

Hi Marek

We're looking at implementing an EPiserver project using MVP and we've found this blog really helpful.

Would you be able to post a link where we can download your sample project from? In particular we are looking for the functionality in the AbstractPresenterBase class.

Many thanks

Dave

joaoreis01 said...

EpiServer is GUI... Why, can you teel us we should place elements of the GUI in the presenter?