Unit testing a method that has a dependency to DataFactory
In this post I’ll go through the steps I went through when I decided that I wanted to test unit testing some code I had written. To not bore you too much about what I was trying to do I’ve created a service class for the sake of this post that looks like this:
1: public class PageUpdater
2: {
3: public bool UpdatePageOnCondition(PageData pageData, Func<PageData, bool> predicate)
4: {
5: if (predicate(pageData))
6: {
7: DataFactory.Instance.Save(pageData, EPiServer.DataAccess.SaveAction.ForceCurrentVersion, EPiServer.Security.AccessLevel.NoAccess);
8: return true;
9: }
10:
11: return false;
12: }
13:
14: public bool UpdatePageWithPageNameContainingString(PageData pageData, string pattern)
15: {
16: return UpdatePageOnCondition(pageData, (pd) => pd.PageName.Contains(pattern));
17: }
18: }
Version 1
I want to test that if I call the method UpdatePageWithPageNameContainingString and pass in a PageData which PageName contains the string “a” the method should return true. Simple enough, and here’s the test (yea I’m using MS Test Framework so all you can cool xUnit kids can laugh now).
1: [TestMethod]
2: public void A_Page_With_A_Pattern_Matching_Page_Name_Should_Be_Saved()
3: {
4: // Arrange
5: EPiServer.Core.PageData pd = new EPiServer.Core.PageData();
6: pd.PageName = "Title with a";
7:
8: EPiServer.Service.PageUpdater pageUpdater = new EPiServer.Service.PageUpdater();
9:
10: // Act
11: var result = pageUpdater.UpdatePageWithPageNameContainingString(pd, "a");
12:
13: // Assert
14: Assert.IsTrue(result);
15: }
While this compile the test itself throws an exception when run, namely “EPiServer.Core.EPiServerException: Property 'PageName' does not exist, can only assign values to existing properties.”
So apparently we can’t just instantiate our PageData object like that and set the value we’re interested in.
Version 2
Version two contains a lot of different ways of trying to create a PageData object and equally many failed attempts to do so. Say what you will about PageData but it won’t win any POCO-awards anytime soon. If anyone has a way (without having access to DataFactory), feel free to leave a comment.
Version 3
I took a new approach here fueled by the wise words of David Wheeler, “Any problem in computer science can be solved with another layer of indirection”. I decided to use a similar approach that the Asp.Net MVC team used when working around dependencies to for instance HttpContext, create base classes and wrapper classes. So since I in this example only am interested in the PageName property I created two simple classes.
1: public abstract class PageDataBase
2: {
3: public virtual string PageName { get; set; }
4: public abstract PageData GetWrappedObject();
5: }
1: public class PageDataWrapper : PageDataBase
2: {
3: private readonly PageData pageData;
4:
5: public override string PageName
6: {
7: get
8: {
9: return pageData.PageName;
10: }
11: set
12: {
13: pageData.PageName = value;
14: }
15: }
16:
17: public PageDataWrapper(PageData pageData)
18: {
19: this.pageData = pageData;
20: }
21:
22: public override PageData GetWrappedObject()
23: {
24: return this.pageData;
25: }
26: }
I then update my service class to talk to my newly created base class
1: public class PageUpdaterV2
2: {
3: public bool UpdatePageOnCondition(Abstractions.PageDataBase pageData, Func<Abstractions.PageDataBase, bool> predicate)
4: {
5: if (predicate(pageData))
6: {
7: DataFactory.Instance.Save(pageData.GetWrappedObject(), EPiServer.DataAccess.SaveAction.ForceCurrentVersion, EPiServer.Security.AccessLevel.NoAccess);
8: return true;
9: }
10:
11: return false;
12: }
13:
14: public bool UpdatePageWithPageNameContainingString(Abstractions.PageDataBase pageData, string pattern)
15: {
16: return UpdatePageOnCondition(pageData, (pd) => pd.PageName.Contains(pattern));
17: }
18: }
In my test method I can now either create a class that implements the abstract PageDataBase class for testing purpose or simply mock it like I’ve done in this test below (if you don’t know what Moq is, head over to their homepage here)
1: [TestMethod]
2: public void A_Page_With_A_Pattern_Matching_Page_Name_Should_Be_Saved_Ver4()
3: {
4: // Arrange
5: var pageDataMock = new Moq.Mock<EPiServer.Abstractions.PageDataBase>();
6: pageDataMock.Setup(x => x.PageName).Returns("Title with a");
7:
8: EPiServer.Service.PageUpdaterV2 pageUpdater = new EPiServer.Service.PageUpdaterV2();
9:
10: // Act
11: var result = pageUpdater.UpdatePageWithPageNameContainingString(pageDataMock.Object, "a");
12:
13: // Assert
14: Assert.IsTrue(result);
15: }
Running this test results in another exception, but not the same one. Yay, we must be moving forward. “System.TypeInitializationException: The type initializer for 'EPiServer.DataFactory' threw an exception. ---> System.ArgumentException: The application relative virtual path '~/' is not allowed here”. It wouldn’t be surprising if this is caused by the call to DataFactory, remember that in my test project I don’t have any EPiServer configuration setup at all.
Version 4
In the test I’m writing I’m not interested in what happens in the DataFactory.Save method so I don’t really care what happens there. I certainly don’t want to setup a correct config for EPi just to get my test method to run.
So once again I’m in abstraction / wrapping land, this time with the DataFactory… luckily for me someone else has done a great job with this already. Enter EPiAbstractions!
I once again rewrite my service class and now instead of working against DataFactory directly I work against the interface IDataFactoryFacade defined in EPiAbstraction.
1: public class PageUpdaterV3
2: {
3: private readonly EPiAbstractions.IDataFactoryFacade dataFactoryFacade;
4:
5: public PageUpdaterV3(EPiAbstractions.IDataFactoryFacade dataFactoryFacade)
6: {
7: this.dataFactoryFacade = dataFactoryFacade;
8: }
9:
10: public bool UpdatePageOnCondition(Abstractions.PageDataBase pageData, Func<Abstractions.PageDataBase, bool> predicate)
11: {
12: if (predicate(pageData))
13: {
14: dataFactoryFacade.Save(pageData.GetWrappedObject(), EPiServer.DataAccess.SaveAction.ForceCurrentVersion, EPiServer.Security.AccessLevel.NoAccess);
15: return true;
16: }
17:
18: return false;
19: }
20:
21: public bool UpdatePageWithPageNameContainingString(Abstractions.PageDataBase pageData, string pattern)
22: {
23: return UpdatePageOnCondition(pageData, (pd) => pd.PageName.Contains(pattern));
24: }
25: }
I’m now able to write a mock of the interface in my test method and pass that one in using the constructor like this:
1: [TestMethod]
2: public void A_Page_With_A_Pattern_Matching_Page_Name_Should_Be_Saved_Ver5()
3: {
4: // Arrange
5: var pageDataMock = new Moq.Mock<EPiServer.Abstractions.PageDataBase>();
6: pageDataMock.Setup(x => x.PageName).Returns("Title with a");
7:
8: var dataFactoryMock = new Moq.Mock<EPiAbstractions.IDataFactoryFacade>();
9:
10: EPiServer.Service.PageUpdaterV3 pageUpdater = new EPiServer.Service.PageUpdaterV3(dataFactoryMock.Object);
11:
12: // Act
13: var result = pageUpdater.UpdatePageWithPageNameContainingString(pageDataMock.Object, "a");
14:
15: // Assert
16: Assert.IsTrue(result);
17: }
Notice that I don’t really need to do any setup for the data factory mock since I’m not too interested in what happens when the call to Save is made.
Anyway, running the test now results in this:
Using the method on the site
To use the method on the site we now have to write code like this
1: var pageUpdater = new Service.PageUpdaterV3(EPiAbstractions.DataFactoryFacade.Instance);
2: pageUpdater.UpdatePageWithPageNameContainingString(new Abstractions.PageDataWrapper(CurrentPage.CreateWritableClone()), "a");
This can be improved using an IoC-framework, but since this post is already too long that will have to be a topic for another post.
Good post Stefan!
As long as you don't use MS Test to cheat (using private accessors) I just find it slightly laughable :P
About version 2, I think that all you need to do is to create a new property:
EPiServer.Core.PageData pd = new EPiServer.Core.PageData();
pd.Property["PageName"] = new PropertyString("a");
Assert.AreEqual("a",pd.PageName);
/ Patrik Akselsson
@Patrik: Thank you, that works very well.
/ Stefan Forsberg