Sunday, June 19, 2011

Writing and testing a 2007 MS Word Add-in using NUnit, Moq, and the MVP pattern

Hey everyone,

Today I want to show you an approach to writing a testable UI. To do this I'm going to write a 2007 Microsoft Word Add-in using Microsoft's Visual Studio Tools for Office. The goal is to write an application level add-in and test it using NUnit and Moq.

 I'm going to design it around the Model-View-Presenter pattern: We'll have two interfaces: An IView, which will be an Office ribbon and contain no logic. Then we'll  have an IPresenter, which will handle all of the "UI logic" such as taking actions based on user dialog results etc.

Our Presenter won't actually have any domain logic in it: we'll be delegating that to another class (I'll explain that in a minute). This means that when we test it, we'll be doing behavior testing instead of testing for object states. This also means we'll be using Moq to setup our Mock domain objects and verifying that calls are made to them.

Of course it's important to note that this kind of testing involves some coupling between your tests and the implementation of the class under test, but I think it's acceptable since the Presenter is behavior based.

So what kind of add-in will we be doing? Well at first I was going to be boring and use a very contrived example. But then I came up with a nice domain-specific idea.

I figured why not do something chemistry related? That's always fun. So we're going to make an addition to the Word ribbon that allows the user to enter either a chemical formula or an entire reaction equation. When the user clicks the "Action" button, the add-in will either render the equation as an image and insert it, or if the user enters a single compound it will insert the molar mass at the cursor position.

Here's the user interface:


Don't worry about the actual logic behind rendering and molar mass calculation. I've already taken care of that using my chemical database program I wrote last year. Now of course I think the code is crap, but it still works and that's the main thing.

So let's get started by defining our interfaces. Our IPresenter will only contain a single method: Action() which returns void and expects no parameters. Our Ribbon will implement IView. To start with, we'll give IView two properties: MolarMass and EquationText:


IView.cs:
namespace Chemistry.UI
{
    public interface IView
    {
        double MolarMass
        {
            set;
        }
        string EquationText
        {
            get; 
        }
    }
}
IPresenter.cs:
namespace Chemistry.UI
{
    public interface IPresenter
    {
        void Action(); 
    }
}

In our first iteration we'll simply assume that the user-entered text is a compound and we want to calculate the molar mass of it. Let's write a simple test that will confirm our IPresenter implementation makes a call to our domain object (an instance of IFormulaParser):

Presenter.cs:
namespace Chemistry.UI
{
    public class Presenter : IPresenter
    {
        public void Action()
        {
            throw new NotImplementedException();
        }
    }
}
IFormulaParser.cs:
namespace Chemistry.UI
{
    public interface IFormulaParser
    {
        double MolarMass(string compound); 
    }
}
And now our test file, PresenterTests.cs:

using NUnit.Framework;
using Moq;
using Chemistry.UI; 
namespace Tests
{
    public class PresenterTests
    {
        private IPresenter _presenter;
        private Mock<IFormulaParser> _formulaParser; 

        [SetUp] 
        public void SetUp()
        {
            _presenter = new Presenter();
            _formulaParser = new Mock<IFormulaParser>();            
        }

        [TestCase] 
        public void Action_UserEntersCompound_CallsMolarMassMethod()
        {
            _presenter.Action(); 
            _formulaParser.Verify(parser => parser.MolarMass("O2")); 
        }
    }
}

So in our test class we initialize a new Presenter object and a mock FormulaParser. For now we've set the IFormulaPresenter to return a value of 32 when MolarMass("O2") is called. In our test case, we make sure that our FormulaParser is called correctly. Of course the test fails since Presenter doesn't do anything. Let's change it so that it uses the text entered by the user instead our hard-coded value.

using NUnit.Framework;
using Moq;
using Chemistry.UI; 
namespace Tests
{
    public class PresenterTests
    {
        private IPresenter _presenter;
        private Mock<IFormulaParser> _formulaParser;
        private Mock<IView> _view; 

        [SetUp] 
        public void SetUp()
        {
            _presenter = new Presenter();
            _formulaParser = new Mock<IFormulaParser>();            
            _view = new Mock<IView>(); 
        }

        [TestCase("O2",32.0)] 
        [TestCase("H2", 2.0)]
        [TestCase("C6H6",78.0)]
        public void Action_UserEntersCompound_CallsMolarMassMethod(string compound, double molarMass)
        {
            _view.Setup(view => view.EquationText).Returns(compound);
            _formulaParser.Setup(parser => parser.MolarMass(compound)).Returns(molarMass);                 

            _presenter.Action(); 

            _formulaParser.Verify(parser => parser.MolarMass(compound)); 
        }
    }
}


Now we've done a couple things. First, we've created a new mock IView. We also changed our test to accept parameters and set up the view's EquationText property with the compound string. Finally, we check again to make sure the presenter calls the FormulaParser.MolarMass method. Now let's make the Presenter pass the test.

public class Presenter : IPresenter
{
 private IFormulaParser _formulaParser;
 private IView _view;

 public Presenter(IView view)
 {
  _view = view;
  _formulaParser = new FormulaParser(); 
 }

 public void Action()
 {
  _view.MolarMass = _formulaParser.MolarMass(_view.EquationText); 
 }
}

Alright, so now the Presenter is using the IView's equation text property, calculating the molar mass, and setting the IView's MolarMass property to the result. Let's also update the test cases to verify that View's MolarMass property is also set:

[SetUp] 
public void SetUp()
{
 _view = new Mock<IView>();
 _formulaParser = new Mock<IFormulaParser>();                  
 _presenter = new Presenter(_formulaParser.Object, _view.Object);                  
}

[TestCase("O2",32.0)] 
[TestCase("H2", 2.0)]
[TestCase("C6H6",78.0)]
public void Action_UserEntersCompound_CallsMolarMassMethod(string compound, double molarMass)
{
 _view.Setup(view => view.EquationText).Returns(compound);
 _formulaParser.Setup(parser => parser.MolarMass(compound)).Returns(molarMass);  
              
 _presenter.Action(); 

 _formulaParser.Verify(parser => parser.MolarMass(compound));
 _view.VerifySet(view => view.MolarMass = molarMass); 
}

At this point, our test passes. And at this point if we implement IView in our Ribbon object, everything should work. So let's proceed and implement rendering equations as well. Let's start by writing a test to make sure our rendering object is called. To do thish we need to add our IEquationRenderer interface, which has a reference to System.Drawing:

public interface IEquationRenderer
{
 Image RenderEquation(string equation); 
}

And modify the Presenter constructor:

public Presenter(IEquationRenderer equationRenderer, IFormulaParser formulaParser,  IView view)
{
 _view = view;
 _formulaParser = formulaParser;
 _equationRenderer = equationRenderer; 
}

Finally add our test case:

[TestCase("H2 + O2 ----> H2O")]
public void Action_UserEntersEquation_CallsEquationRenderer(string equation)
{                        
 _view.Setup(view => view.EquationText).Returns(equation);            
 _presenter.Action();
 _equationRenderer.Verify(renderer => renderer.RenderEquation(equation));
 _view.VerifySet(view => view.EquationImage = It.IsAny<Image>()); 
}

An important note here: I wasn't able to mock the Image interface that we're passing around, so we're verifying that the IView gets its EquationImage property set to "any" Image object instead of a dummy Image that we would pass around normally.

If we make this test pass by just adding a call to our EquationRenderer, we could also still be calling the molar mass calculation which we don't want. So let's add another test that ensures that when we parse an equation, we don't try a molar mass calculation:

[TestCase("H2 + O2 ----> H2O")]
public void Action_UserEntersEquation_DoesNotTryMolarMassCalculation(string equation)
{
 _view.Setup(view => view.EquationText).Returns(equation);
 _presenter.Action();
 _formulaParser.Verify(parser => parser.MolarMass(It.IsAny<string>()), Times.Never()); 
}

This test will fail because we need some conditional logic to differentiate compounds and equations. Let's do this by adding a method to IFormulaParser:

bool IsEquation(string text); 

Let's set this up in our equation tests to return true by adding this setup line to our two test cases:

_formulaParser.Setup(parser => parser.IsEquation(equation)).Returns(true); 

The actual implementation will just check for the presence of "---->," but that could always change. Now we can modify the Presenter to take this into account:

public void Action()
{
 if (_formulaParser.IsEquation(_view.EquationText))
 {
  _view.EquationImage = _equationRenderer.RenderEquation(_view.EquationText); 
 }
 else
 {
  _view.MolarMass = _formulaParser.MolarMass(_view.EquationText);
 }            
}

And now our tests pass. At this point, the rest of the work is implementation details. Here's an example of the final result:

The important thing to note with this approach is that we've created an extremely testable UI layer. Since all of the logic is in the Presenter class, we can test all aspects of the UI. This becomes even more exciting when your UI logic involves the user making decisions by answering prompts and responding to dialogs. You could for example wrap a call to MessageBox.Show in an IView DisplayMessage() method that returns a DialogResult. This would allow you to test the Presenter and just mock the IView.

Anyway, just for reference, the full source code of this add-in is on my website. Apologies in advance for the weird project structure, the Test project is under the "Tests" folder. The solution uses Visual Studio 2010.

No comments:

Post a Comment