One thing I have noticed from many developers when setting up unit tests is that they like their Setup() methods. This is fine for example (shameless plug) if you are looking to integration test against something like Glass Mapper for Sitecore and want to initialise the configuration before you run your test. However, all too often, I see it used to set up things like mocks, and its this that I disagree with. So I guess for want of a better term – I created what I will (I think it’s probably technically a Builder pattern really) a ‘Test Harness’ pattern.
The Problem
Take a read through the listing below.
using Moq; using NUnit.Framework; using Cardinal.Adapters; namespace Cardinal.Tests { [TestFixture] public class TestClass { private Mock<ISitecorePage> sitecorePageMock; private DependentClass dependentClass; [TestFixtureSetUp] public void Setup() { sitecorePageMock = new Mock<ISitecorePage>(); dependentClass = new DependentClass(sitecorePageMock.Object); } [Test] public void Can_get_some_result_successfully_when_in_page_editor_mode() { // Assign sitecorePageMock.Setup(x => x.IsPageEditor).Returns(true); sitecorePageMock.Setup(x => x.IsPreview).Returns(false); sitecorePageMock.Setup(x => x.IsNormal).Returns(false); // Act var result = dependentClass.DoSomething(); // Assert Assert.AreEqual("Expected Correct Result", result); } [Test] public void Can_get_a_result_when_preview_mode() { // Assign - in order to be safe I am now forced to ensure all values are reset sitecorePageMock.Setup(x => x.IsPageEditor).Returns(false); sitecorePageMock.Setup(x => x.IsPreview).Returns(true); sitecorePageMock.Setup(x => x.IsNormal).Returns(false); // Act var result = dependentClass.DoSomething(); // Assert Assert.AreEqual("Expected Correct Result", result); } [Test] public void Shakey_can_get_a_result_when_normal_mode() { // Assign - surely this is more sensible, but we are now relying on what have executed before HomePageItem item = new HomePageItem(); sitecorePageMock.Setup(x => x.HomePageItem).Returns(item); // Act - This could be affected by page being in edit / preview var result = dependentClass.DoSomething(); // Assert Assert.AreEqual("Expected Correct Result", result); } } }
In the code sample above, the setup takes care of the initialisation of the mocked object. As you read down, you will start to see that my mocked behaviour has to be quite explicit in order to ensure all of the values are correct. In the case of the final method Shakey_can_get_a_result_when_normal_mode(). Running this test in isolation will be fine, HOWEVER – I could in theory get any of the mocked behaviour from any of my other tests when ran as part of the suite thus polluting my tests and potentially giving false positives or negatives. This in my opinion is badly flawed – but then, how can we cope with objects with many dependencies.
The Test Harness
Now consider the same tests written differently using a test harness
using Moq; using NUnit.Framework; using Cardinal.Adapters; namespace Cardinal.Tests { [TestFixture] public class TestClass { [Test] public void Can_get_some_result_successfully_when_in_page_editor_mode() { // Assign var testHarness = new DependantClassTestHarness(); testHarness.SetPageEditorMode(); // Act var result = testHarness.DependentClass.DoSomething(); // Assert Assert.AreEqual("Expected Correct Result", result); } [Test] public void Can_get_a_result_when_preview_mode() { // Assign var testHarness = new DependantClassTestHarness(); testHarness.SetPreviewMode(); // Act var result = testHarness.DependentClass.DoSomething(); // Assert Assert.AreEqual("Expected Correct Result", result); } [Test] public void Shakey_can_get_a_result_when_normal_mode() { // Assign var testHarness = new DependantClassTestHarness(); HomePageItem item = new HomePageItem(); testHarness.SitecorePageMock.Setup(x => x.HomePageItem).Returns(item); // Act - this is no longer dependant on anything other than its own mock setup var result = testHarness.DependentClass.DoSomething(); // Assert Assert.AreEqual("Expected Correct Result", result); } public class DependantClassTestHarness { public DependantClassTestHarness() { SitecorePageMock = new Mock<ISitecorePage>(); DependantClass = new DependantClass(SitecorePageMock.Object); } public Mock<ISitecorePage> SitecorePageMock { get; private set; } public DependantClass DependantClass { get; private set; } public void SetPageEditorMode() { // Here we define the common behaviour of being in edit mode SitecorePageMock.Setup(x => x.IsPageEditor).Returns(true); SitecorePageMock.Setup(x => x.IsPreview).Returns(false); SitecorePageMock.Setup(x => x.IsNormal).Returns(false); } public void SetPreviewMode() { // Here we define the common behaviour of being in preview mode SitecorePageMock.Setup(x => x.IsPageEditor).Returns(false); SitecorePageMock.Setup(x => x.IsPreview).Returns(true); SitecorePageMock.Setup(x => x.IsNormal).Returns(false); } } } }
In the above, not only is the DependantClassTestHarness a nice easy way to start with a fresh copy of your mocked dependencies & class to test, meaning things like lazy loading will definitely NOT have taken effect and that your mock will not be subject previous setup’s. I have also defined grouped standard behaviours using methods to allow common scenarios to be set up.
This also has the added bonus, that if I have a class to test with say – 6 or 7 dependencies, I don’t have to go through the pain of setting each in every individual test.
Hi Nat, great article!
Another way to achieve similar results is to use NUnit [SetUp] attribute instead of [TestFixtureSetUp].
[TestFixtureSetUp] is used to run code once before any test of a fixture is run. http://www.nunit.org/index.php?p=fixtureSetup&r=2.6.4
[SetUp] is used to run code before each test in a fixture. This is where I initialize mocks and the object instance to test. http://www.nunit.org/index.php?p=setup&r=2.6.4
Thanks dru 😀
I have used [SetUp] in the past, both work :D. I like with this approach the fact that I can pick and choose common setups more easily this way in a way that I can easily share test harnesses between classes if I choose to.
Also with setup – generally the variables would be global which means they HAVE to be explicitly cleared in some way in your setup method.