Skip to main content

Back-End Development

Unit Testing Custom Rules, Actions, and Conditions with FakeDb – Part 3 – Testing Rule Execution

Testing Code@1x.jpg

Wow … this is embarrassing … parts 1 and 2 of this series were sooooooo looooooong ago. But, better late than never :). One of our developers is working on a new rule type implementation, so I thought it would be a good idea to finish this series with Part 3 – Testing Rule Execution.

In parts 1 and 2, we focused on testing custom conditions and actions. These conditions and actions could be built for any rule type – including the pre-existing rule types that are supplied by Sitecore. In this blog, we will focus only on testing new rule types – that is, a rule type that you have created for execution.

When are custom rules executed? Well, that’s up to you. And the answer is, “It depends.” Specifically, it depends on what type of rule you have created, and what it does.  Usually custom rules are executed via a pipeline processor that you inject into an existing Sitecore (or existing custom) pipeline.

For our example, the MyCustomRule rule type is injected via a processor that is added into the getLookupSourceItems Sitecore pipeline.

How Custom Rule Types Can Be Executed in Sitecore

In order to tell Sitecore to execute your new custom rule type, you must add some code. Here’s an example of some code that executes a custom rule type via the GetLookupSourceItem pipeline:

1public class MyCustomRule
2{
3public void Process(GetLookupSourceItemsArgs args)
4{
5Assert.ArgumentNotNull(args, "args");
6Assert.IsNotNull(args.Item, "Item");
7if (SkipProcessor(args))
8{
9return;
10}
11 
12Run(args);
13}
14 
15public virtual bool SkipProcessor(GetLookupSourceItemsArgs args)
16{
17return
18string.IsNullOrWhiteSpace(args.Source) ||
19!(args.Source.Contains("query:") || args.Source.Contains("fast:"));
20}
21 
22public virtual void Run(GetLookupSourceItemsArgs args)
23{
24Item item = args.Item;
25string source = args.Source;
26Database database = item.Database;
27 
28RuleList<MyCustomRuleContext> rules = GetMyCustomRules(database);
29if (rules == null || rules.Count == 0) return;
30 
31ProcessRules(item, source, rules, args);
32}
33 
34public virtual void ProcessRules(Item item, string source,
35RuleList<MyCustomRuleContext> rules,
36GetLookupSourceItemsArgs args)
37{
38var ruleContext = new GetLookupSourceItemRuleContext
39{
40Item = item,
41ContextItem = item,
42Source = source
43};
44ruleContext.Result.AddRange(args.Result);
45rules.Run(ruleContext);
46ProcessResult(args, ruleContext);
47}
48 
49public virtual void ProcessResult(GetLookupSourceItemsArgs args, MyCustomRuleContext ruleContext)
50{
51args.Result.AddRange(ruleContext.Result);
52}
53 
54/// <summary>
55///     Fetch the rule list from the content database
56/// </summary>
57/// <param name="db"></param>
58/// <returns></returns>
59public virtual RuleList<MyCustomRuleContext> GetMyCustomRules(Database db)
60{
61// load the rules from the content tree in sitecore/system/settings/rules/My Custom Rules
62}
63}

And now, back to unit testing

So we see that the code to execute the rules follows a pattern –

  1. Load the rules
  2. Build a context
  3. Run the rules
  4. Process the result

We’ve already seen the actions and conditions, so we won’t get into that level of detail again; but within these 4 tasks, there are a lot of things to do, including testing for error conditions.

The unit tests I have constructed for this fall into the following categories: testing if the processor should be skipped, ensuring the rule execution runs under the right conditions and ensuring the results are processed as expected.

Should the Processor Be Skipped

For this type of rule, there are several assumptions. I need to know the item that is requesting the GetLookupSource pipeline be executed. Second, I need to see if the source property of the item field being processed includes some type of query.

1[TestCase("", true)]
2[TestCase("DataSource=/sitecore/content", true)]
3[TestCase("DataSource=query:...", false)]
4[TestCase("DataSource=fast:...", false)]
5[TestCase("DataSource=./Content", true)]
6public void ShouldSkipIfNoQuery(string source, bool shouldSkip)
7{
8using (var db = new Db
9{
10new DbItem("item")
11})
12{
13Item item = db.GetItem("/sitecore/content/item");
14var args = new GetLookupSourceItemsArgs
15{
16Item = item,
17Source = source
18};
19 
20var processor = new MyCustomRule();
21 
22// act
23bool skipProcessor = processor.SkipProcessor(args);
24 
25// assert
26skipProcessor.Should().Be(shouldSkip);
27}
28}

Are There Rules to Run?

Next, I’ll check to see if there are rules to execute, and if so, is the ProcessResult method executed. In the first test, if the rules cannot be loaded from the database, the processor should never try to process them.

1[Test]
2public void ShouldSkipIfNoRulesFolderFound()
3{
4using (var db = new Db
5{
6new DbItem("item")
7})
8{
9Item item = db.GetItem("/sitecore/content/item");
10var args = new GetLookupSourceItemsArgs
11{
12Item = item,
13Source = String.Empty
14};
15 
16var processor = Substitute.ForPartsOf<MyCustomRule>();
17 
18// act
19processor.Process(args);
20 
21// assert
22processor.DidNotReceive()
23.ProcessRules(Arg.Any<Item>(), Arg.Any<String>(),
24Arg.Any<RuleList<MyCustomRuleContext>>(), args);
25}
26}

In the second test, I will create the expected rule folder and some dummy rules. The rules won’t be good enough to actually work, but they are fine to make sure that condition is satisfied and to ensure that the rules are attempted to be processed.

1[Test]
2public void ShouldTryToRunRulesFound()
3{
4var settings = new DbItem("Settings")
5{
6new DbItem("Rules")
7{
8new DbItem("My Custom Rules", ID.NewID, RuleIds.RulesContextFolderTemplateID)
9{
10new DbItem("Rules", ID.NewID, RuleIds.RulesFolderTemplateId)
11{
12new DbItem("Rule 1", ID.NewID, RuleIds.Rule)
13{
14new DbField("Rule")
15{
16Value = @"<ruleset>
17<rule uid=""{9D4698F2-61D7-4FF4-B17A-5D2CEA194E57}"">
18</rule>
19</ruleset>"
20}
21},
22new DbItem("Rule 2", ID.NewID, RuleIds.Rule)
23{
24new DbField("Rule")
25{
26Value = @"<ruleset>
27<rule uid=""{9D4698F2-61D7-4FF4-B17A-5D2CEA194E56}"">
28</rule>
29</ruleset>"
30}
31}
32}
33}
34}
35};
36 
37settings.ParentID = ItemIDs.SystemRoot;
38 
39using (var db = new Db
40{
41new DbItem("test"),
42settings
43})
44{
45Item item = db.GetItem("/sitecore/content/test");
46 
47var args = new GetLookupSourceItemsArgs
48{
49Item = item,
50Source = "query:/sitecore/content/"
51};
52 
53var processor = Substitute.ForPartsOf<MyCustomRule>();
54 
55// act
56processor.Process(args);
57 
58//}
59 
60// assert
61processor
62.Received()
63.ProcessRules(item, args.Source, Arg.Any<RuleList<MyCustomRuleContext>>(), args);
64}
65}

Man, this is a long post … anywho …

Did the Rules Process as Expected?

The final tests are to confirm that everything worked as expected. In other words, when actions were queued up based on conditions, did they get processed and injected back into the results as expected?

For this test, I will enlist the use of 2 helper methods. These will build some content in the tree with FakeDb that is needed including defining some actual references to conditions and actions I have created that are specific to this type of rule.

The first method sets up some content I need for my test: an expected web root for a site, a “selections” folder and a “keywords” folder. I plan to use this to test a sample ruleset.

1private DbItem BuildContent()
2{
3 
4return new DbItem("WebSite Root", ID.NewID, new ID(Const.TemplateIds.WebsiteRootFolder))
5{
6new DbItem("Home", ID.NewID, ID.NewID)
7{
8new DbItem("ContentItem", ID.NewID, ID.NewID)
9},
10new DbItem("Site Selection", ID.NewID, new ID(Const.TemplateIds.SelectionsFolder))
11{
12new DbItem("Brand Colors", ID.NewID, new ID(Const.TemplateIds.BrandColors))
13{
14new DbItem("Green", ID.NewID, new ID(Const.TemplateIds.ListItem))
15}
16},
17new DbItem("Keywords", ID.NewID, new ID(Const.TemplateIds.KeywordFolder))
18{
19new DbItem("Test Keyword", ID.NewID, new ID(Const.TemplateIds.Keyword))
20}
21};
22}

The second is a sample ruleset including a condition (does this source property contain a token) and some sample actions (remove the token, run a new query from a separate item and abort the pipeline). Finally, let’s add in the test.

1private DbItem BuildRules(ID targetFolder, string token)
2{
3var abortPipeline = ID.NewID;
4var queryContainsToken = ID.NewID;
5var removeToken = ID.NewID;
6var runQueryFromItem = ID.NewID;
7var settings = new DbItem("Settings", ID.NewID)
8{
9new DbItem("Rules")
10{
11new DbItem("Definitions", ID.NewID, TemplateIDs.Folder)
12{
13new DbItem("Elements", ID.NewID, TemplateIDs.Folder)
14{
15new DbItem("My Custom Rules", ID.NewID, RuleIds.ElementsFolderID)
16{
17new DbItem("Abort the pipeline", abortPipeline, RuleIds.Action)
18{
19{
20"Type",
21"Custom.Rules.MyCustomRules.Actions.Abort, Custom"
22}
23},
24new DbItem("Query Contains Token", queryContainsToken, RuleIds.Action)
25{
26{
27"Type",
28"Custom.Rules.MyCustomRules.Conditions.QueryContainsToken, Custom"
29}
30},
31new DbItem("Remove Token", removeToken, RuleIds.Action)
32{
33{
34"Type",
35"Custom.Rules.MyCustomRules.Actions.RemoveToken, Custom"
36}
37},
38new DbItem("Run Query From Item", runQueryFromItem, RuleIds.Action)
39{
40{
41"Type",
42"Custom.Rules.MyCustomRules.Actions.RunSimpleQueryFromItem, Custom"
43}
44}
45}
46}
47},
48new DbItem("My Custom Rules", ID.NewID, RuleIds.RulesContextFolderTemplateID)
49{
50new DbItem("Rules", ID.NewID, RuleIds.RulesFolderTemplateId)
51{
52new DbItem("Rule 1", ID.NewID, RuleIds.Rule)
53{
54new DbField("Rule")
55{
56Value = $"<ruleset>" +
57$"   <rule uid=\"{Guid.NewGuid()}\" name=\"token replacer\">" +
58"       <conditions>" +
59$"          <condition id=\"{queryContainsToken}\" uid=\"{Guid.NewGuid().ToString("N")}\" token=\"{token}\" />" +
60"       </conditions>" +
61"       <actions>" +
62$"           <action id=\"{removeToken}\" uid=\"{Guid.NewGuid().ToString("N")}\" token=\"{token}\" />" +
63$"           <action id=\"{runQueryFromItem}\" uid=\"{Guid.NewGuid().ToString("N")}\" itemid=\"{targetFolder.Guid}\" />" +
64$"           <action id=\"{abortPipeline}\" uid=\"{Guid.NewGuid().ToString("N")}\" />" +
65"       </actions>" +
66"   </rule>" +
67"</ruleset>"
68}
69}
70}
71}
72}
73};
74 
75settings.ParentID = ItemIDs.SystemRoot;
76return settings;
77}
78}

So the final test supplied tests the result expected and ensures that the pipeline args are modified, which in my case is the expected result.

1[TestCase("#selections#", "/sitecore/content/WebSite Root/Site Selection", "query:#selections#*[@@templateid='" + Const.TemplateIds.BrandColors + "']/*", "/sitecore/content/WebSite Root/Site Selection/Brand Colors/Green")]
2[TestCase("#keywords#", "/sitecore/content/WebSite Root/Keywords", "query:#keywords#*", "/sitecore/content/WebSite Root/Keywords/Test Keyword")]
3public void RulesWorksCorrectTokens(string token, string pathToTargetFolder, string query, string expectedItem)
4{
5using (var db = new Db { BuildContent() })
6{
7Item targetFolder = db.GetItem(pathToTargetFolder);
8Item contentItem = db.GetItem("/sitecore/content/WebSite Root/Home/ContentItem");
9Item testKeywordItem = db.GetItem(expectedItem);
10db.Add(BuildRules(targetFolder.ID, token));
11 
12var args = new GetLookupSourceItemsArgs
13{
14Item = contentItem,
15Source = query
16};
17 
18var processor = new MyCustomRule();
19 
20// act
21processor.Process(args);
22 
23// assert
24args.Result.Should().HaveCount(1);
25args.Result.Should().Contain(testKeywordItem);
26}
27}

If you read this far, you must be crazy or an MVP or you have some really bad problems with your own custom rule types. Either way – let me know … I always like reading comments. Especially from the crazy people.

Finally – special thanks to several others who contributed to this solution – we are using Sitecore FakeDb, NSubstitute and Fluent Assertions to write these nUnit tests 🙂

Thoughts on “Unit Testing Custom Rules, Actions, and Conditions with FakeDb – Part 3 – Testing Rule Execution”

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Brian Beckham

As a Sitecore MVP, Brian spends most of his time consulting and architecting software solutions for enterprise-level Sitecore projects.

More from this Author

Follow Us