Unit Testing Sitecore Components Part 2: Encapsulate Logic
In the previous post of this series I detailed 2 principals which can help with making Sitecore components more testable and reusable. These were “keeping the business logic out of the view” and “keeping the Item
class out of the model”. In this post I’ll detail several more principals to continue improving the code.
This post is part of a series covering the principals I showed during my virtual SUGCON presentation on unit testing Sitecore components in April of this year (2020). The following is a list of the posts belonging to the series so far, along with the principals covered in the post:
Post not found: logicless-view-itemless-model- Principal: Keep business logic out of the view.
- Principal: Keep
Item
out of the model.
- Principal: Avoid implicit data
- Principal: Avoid statics
- How to mock the
Sitecore.Data.Items.Item
class. - How to mock field values of a mocked item.
- Recap
- Resources
In this post, I’ll cover 2 more principals:
- Principal: Encapsulate logic.
- Principal: Use Dependency Injection and Abstractions
Example Rendering
I’ll continue to use the same view rendering which I started refactoring in the previous post. Here’s what I ended up with at the end:
1 | @model UnitTestingSitecoreComponents.Web.RenderingModels.EntryCategoriesRenderingModel |
And the rendering model:
1 | public class EntryCategoriesRenderingModel : RenderingModel |
And the POCO category model exposed on the Categories
property:
1 | namespace UnitTestingSitecoreComponents.Web.Models |
The view and the Category
class are nice and clean and I don’t need to refactor them any further. Additional refactorings will focus on the EntryCategoriesRenderingModel
class.
Encapsulate Logic
When applying the “Keep business logic out of the views” principal to the example rendering I moved any business logic from the view into the model, to get it out of the view. This is covered in the Post not found: logicless-view-itemless-model previous post. However the logic shouldn’t remain in the model. I can easily test a rendering model; I can instantiate it in test code, call the Initialize
method and validate the values of the properties which have been populated. Although the rendering model is testable the business logic it contains cannot be reused in other code. The logic could only be used with other views, and only other views that didn’t require any additional data. What if I needed to generate the list of categories for an entry during some data export operation?
To make the business logic reusable, I want to encapsulate it into a separate class. That class can then be used by any code.
I’ll go ahead and encapsulate the logic into a class named EntryTaxonomy
.
1 | public class EntryTaxonomy |
1 | public class EntryCategoriesRenderingModel : RenderingModel |
If I were to jump in and start writing tests for EntryCategoriesRenderingModel
now, they would be more complicated than they need to be. Although I’ve encapsulated the business logic into a separate class which can be tested separately, I would still need to go through mocking all the Sitecore data which EntryTaxonomy
requires.
Use Dependency Injection and Abstractions
With EntryCategoriesRenderingModel
in it’s current state, it can only ever work with the EntryTaxonomy
implementation above. This is because EntryCategoriesRenderingModel
creates it’s own instance of EntryTaxonomy
to use. In a testing context, we want to be able to control the instances which the code under test is collaborating with so we can mock their behaviour, rather than having to use real instances and behaviour. This is done using dependency injection. Dependency injection is a practice in which we pass any dependencies to the class which needs them. In this case, I’ll pass the EntryTaxonomy
class instance into the constructor of EntryCategoriesRenderingModel
.
1 | public class EntryCategoriesRenderingModel : RenderingModel |
To be able to mock EntryTaxonomy
for my tests, the class must have the appropriate virtual members, and at the moment it has no virtual members. Instead of passing the concrete EntryTaxonomy
class to EntryCategoriesRenderingModel
, I’ll abstract the relevant methods to an interface and pass that in instead. This allows me to pass any implementation of the interface into EntryCategoriesRenderingModel
whether it be the real EntryTaxonomy
implementation or a mocked implementation.
1 | public interface IEntryTaxonomy |
1 | public class EntryTaxonomy : IEntryTaxonomy |
1 | public class EntryCategoriesRenderingModel : RenderingModel |
Now that I’m passing an abstract IEntryTaxonomy
instance into EntryCategoriesRenderingModel
I can change it’s implementation, even in the production code, without having to change the EntryCategoriesRenderingModel
code. That allows me to switch out EntryTaxonomy
with a different implementation of IEntryTaxonomy
. Let’s say I wanted to change to a taxonomy class which ordered the categories by name, or by popularity.
I’ve got a few more things I need to do to make this work in Sitecore. Firstly, I need to register EntryTaxonomy
in the services registry so Sitecore DI can inject it. I’ll do that through a configuration patch file:
1 |
|
The default Sitecore MVC model locator doesn’t currently (as of Sitecore 9.3) support dependency injection, so I’ll need to extend it to do so:
1 | namespace UnitTestingSitecoreComponents.Web.Presentation |
And I’ll need to replace the default model locator with the one above during the Initialize
pipeline. I can do that with an initialize
pipeline processor:
1 | namespace UnitTestingSitecoreComponents.Web.Pipelines.Initialize |
Lastly, I’ll register the RegisterModelLocator
pipeline processor using a config patch file so it runs as part of the Initialize
pipeline:
1 |
|
Onto the Tests
I’m not going to worry about testing EntryTaxonomy
just yet. That will be for a future post in this series. Those tests will be more complex because I’ll need to mock Sitecore items and field values.
You may notice that IEntryTaxonomy
doesn’t require the use of Sitecore items, thus the tests for EntryCategoriesRenderingModel
will be much simpler. All we have to do is mock IEntryTaxonomy
with the behaviour we want to test.
I’ll start with a simple parameter test to ensure the IEntryTaxonomy
instance isn’t null, as EntryCategoriesRenderingModel
cannot operate without an instance of IEntryTaxonomy
. For the tests I’ll be using xUnit and nSubstitute.
1 | [ ] |
Now to test the different behaviours I expect from calling GetCategories
on IEntryTaxonomy
. In these tests I’ll be calling the Initialize
method, which requires an instance of Rendering
to be passed. If we don’t populate the Item
property of the rendering instance the base Initialize
implementation will attempt to locate the context item, which will end up failing because we’re not calling this method from inside a Sitecore web request. So I’ll need to mock an item to satisfy it. I’ll add the following utility method at the bottom of the test class.
1 | private Item CreateItem() |
Back to testing the Initialize
method. In the first case, no categories are returned, so I expect the Categories
property to be empty:
1 | [ ] |
In the second case, categories are returned from IEntryTaxonomy
, so I expect the Categories
property to contain them:
1 | [ ] |
Conclusion
At this point, the view, and the rendering model are in a good state, and even tested with minimal fuss. By encapsulating the business logic into a separate class that logic can be reused in other locations. By using dependency injection and abstracting the encapsulated logic, we’ve made it possible to substitute in different implementations of the business logic, whether they be real implementations that access Sitecore data, or they could be mocked implementations that return what we want for the purposes of a test.
In the Post not found: avoid-static-members next post we’ll look at how to improve the EntryTaxonomy
class further.
Comments are closed
loading...