Wednesday, October 21, 2015

Unit Testing an Event Handler with Sitecore.FakeDB

In my last post, we looked at how to make an event handler unit testable by wrapping an interface around the Sitecore log, so that we could use NSubstitute to verify the calls made to it.

This only part of the story, of course. For our tests to be meaningful, we need to be able to "arrange" the input that goes into our code, before we make it "act" and before we "assert" how it should behave. Since we are building an ItemSaved handler, we need a way to create Sitecore items in our unit tests.  There is a tool, Sitecore.FakeDB, that makes this straigtforward.

Sitecore.FakeDB

This library, developed by Sitecore Ukraine tech lead Sergey Shushlyapin, allows you to create Sitecore items within a unit test. These items are created in memory, and so no datebase connection is required or used.  Here's a sample from the project's Github wiki:

[Fact]
public void HowToCreateSimpleItem()
{
  using (var db = new Db
    {
      new DbItem("Home") { { "Title", "Welcome!" } }
    })
  {
    Sitecore.Data.Items.Item home = db.GetItem("/sitecore/content/home");
    Xunit.Assert.Equal("Welcome!", home["Title"]);
  }
}

In this sample, a fake database is created, an item named "home" is added to it, which has a field "Title" with a value "Welcome!". I find the crisp syntax this library uses very appealing. The use of collection initializers (the bit in the curly braces) and the fact that a template definition is not required means there is little required ceremony to set up this up, which will make your tests more expressive.

Avoiding Brittleness

In my last post, I had a somewhat silly test that would fail if any change was made to the syntax of the log message.

  [Fact]
  public void Handler_WhenCalled_LogsMessage()
  {
    handler.OnItemSaved(null, null);

    log.Received().Info("Item saved and this code was tested!", handler);
  }
In real life, you don't want to do this. Having tests this tightly specified will mean that you will be getting a steady stream of "false positives" whenever you edit your code. It's better to make loosely specified claims about what your code does (returns a non-empty message, returns a string containing the name of the item). This will give you better information about what the code is currently doing, and will result in more meaningful failures. So let's change that assertion to this looser one:

[Fact]
  public void Handler_WhenCalled_LogsMessage()
  {
    var args = GetSitecoreEventArgs(new DbItem("test"));

    handler.OnItemSaved(null, args);

    log.Received().Info(Arg.Is(s => !string.IsNullOrWhiteSpace(s)), handler);
  }
NSubstitute's Arg.Is<T>(function<T, bool> predicate) allows us to define our assertion as a fact that must be true--for example, that the message is not empty--rather than requiring a fixed value for the entire string.. We will use this approach going forward to define our tests.

Arranging Sitecore.FakeDB

I will not cover the fairly straightforward steps necessary to add FakeDB to a test project; this is explained very well on their wiki. However, I will talk about how I set this up in the current xUnit test project. xUnit does not use SetUp or TearDown methods; instead it instantiates the class before each test, and if the test class implements IDisposable, it runs Dispose after every test. That feature allows us to do away with the using syntax:

public class ItemSavedLoggerTests: IDisposable
{
  private TestableItemSavedHandler handler;
  private ILogWrapper log;
  private Db db;

  public ItemSavedLoggerTests()
  {
    log = Substitute.For();
    handler = new TestableItemSavedHandler(log);
    db = new Db();
  }

  public void Dispose()
  {
    if (db!= null)
    {
      db.Dispose();
    }
  }
And since Sitecore's ItemSaved handler expects EventArgs of type SitecoreEventArgs, with the saved item attached to the first Properties element, we can setup a convenience method to arrange these:

  private SitecoreEventArgs GetSitecoreEventArgs(DbItem dbItem)
  {
    this.db.Add(dbItem);
    var item = db.GetItem("/sitecore/content/" + dbItem.Name);
    var args = new SitecoreEventArgs("name", new object[] { item }, new EventResult());
    return args;
  }
This code accepts a FakeDB item, which can be defined according to the requirements of each test; looks up the corresponding Sitecore Item, and inserts it into a freshly minted SitecoreEventsArgs object, which can be used to exercise the handler code. This keeps the test itself focused on the contents o the item.

Putting It Together

Here is the entire test suite, and the code it supports. A couple of things to point out. I try to keep the arrange part of the test squarely focused on the requirements of the test at hand. If the test doesen't require a template name or ID, one is not provided. This makes it clear what data the system is interacting with in a given test. So no templates if the test is about item names, and no item names if the test is about templates. (I do break this pattern for the name and result fields of the SitecoreEventArgs, because values are required by the constructor.) Finally, as is usually the case, the test suite is a lot longer than the implementation code. This is normal; the test suite is an inventory of all the functionality delivered by the main-line code. Often as the tests get longer, the main code gets shorter. As Robert Martin put it, "As the tests get more specific, the code gets more generic."

2 comments:

  1. Hello Dan,

    Nice post! Glad to see people find FakeDb useful :)

    Have you ever heard about the FakeDb extension for AutoFixture? It has excellent integration with xUnit and allows to generate test data automatically. For instance, you'd not need to add a 'db' field, initialize it in the constructor and then dispose. AutoFixture could do this easily. More samples here: https://github.com/sergeyshushlyapin/Sitecore.FakeDb/wiki/AutoFixture-Samples

    ReplyDelete
  2. Hey Sergey, thanks for the heads up. I'll dig into this in a later post. --Dan

    ReplyDelete