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

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:


    public class MyCustomRule
    {
        public void Process(GetLookupSourceItemsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.IsNotNull(args.Item, "Item");
            if (SkipProcessor(args))
            {
                return;
            }

            Run(args);
        }

        public virtual bool SkipProcessor(GetLookupSourceItemsArgs args)
        {
            return
                string.IsNullOrWhiteSpace(args.Source) ||
                !(args.Source.Contains("query:") || args.Source.Contains("fast:"));
        }

        public virtual void Run(GetLookupSourceItemsArgs args)
        {
            Item item = args.Item;
            string source = args.Source;
            Database database = item.Database;

            RuleList<MyCustomRuleContext> rules = GetMyCustomRules(database);
            if (rules == null || rules.Count == 0) return;

            ProcessRules(item, source, rules, args);
        }

        public virtual void ProcessRules(Item item, string source,
                                         RuleList<MyCustomRuleContext> rules, 
                                         GetLookupSourceItemsArgs args)
        {     
            var ruleContext = new GetLookupSourceItemRuleContext
            {
                Item = item,
                ContextItem = item,
                Source = source
            };
            ruleContext.Result.AddRange(args.Result);
            rules.Run(ruleContext);
            ProcessResult(args, ruleContext);
        }
        
        public virtual void ProcessResult(GetLookupSourceItemsArgs args, MyCustomRuleContext ruleContext)
        {           
            args.Result.AddRange(ruleContext.Result);  
        }

        /// <summary>
        ///     Fetch the rule list from the content database
        /// </summary>
        /// <param name="db"></param>
        /// <returns></returns>
        public virtual RuleList<MyCustomRuleContext> GetMyCustomRules(Database db)
        {
            // load the rules from the content tree in sitecore/system/settings/rules/My Custom Rules 
        }
    }


 

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.


        [TestCase("", true)]
        [TestCase("DataSource=/sitecore/content", true)]
        [TestCase("DataSource=query:...", false)]
        [TestCase("DataSource=fast:...", false)]
        [TestCase("DataSource=./Content", true)]
        public void ShouldSkipIfNoQuery(string source, bool shouldSkip)
        {
            using (var db = new Db
            {
                new DbItem("item")
            })
            {
                Item item = db.GetItem("/sitecore/content/item");
                var args = new GetLookupSourceItemsArgs
                {
                    Item = item,
                    Source = source
                };

                var processor = new MyCustomRule();

                // act
                bool skipProcessor = processor.SkipProcessor(args);

                // assert
                skipProcessor.Should().Be(shouldSkip);
            }
        }

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.


        [Test]
        public void ShouldSkipIfNoRulesFolderFound()
        {
            using (var db = new Db
            {
                new DbItem("item")
            })
            {
                Item item = db.GetItem("/sitecore/content/item");
                var args = new GetLookupSourceItemsArgs
                {
                    Item = item,
                    Source = String.Empty
                };

                var processor = Substitute.ForPartsOf<MyCustomRule>();

                // act
                processor.Process(args);

                // assert
                processor.DidNotReceive()
                    .ProcessRules(Arg.Any<Item>(), Arg.Any<String>(),
                        Arg.Any<RuleList<MyCustomRuleContext>>(), args);
            }
        }

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.


        [Test]
        public void ShouldTryToRunRulesFound()
        {
            var settings = new DbItem("Settings")
            {
                new DbItem("Rules")
                {
                    new DbItem("My Custom Rules", ID.NewID, RuleIds.RulesContextFolderTemplateID)
                    {
                        new DbItem("Rules", ID.NewID, RuleIds.RulesFolderTemplateId)
                        {
                            new DbItem("Rule 1", ID.NewID, RuleIds.Rule)
                            {
                                new DbField("Rule")
                                {
                                    Value = @"<ruleset>  
                                                <rule uid=""{9D4698F2-61D7-4FF4-B17A-5D2CEA194E57}""> 
                                                </rule>
                                            </ruleset>"
                                }
                            },
                            new DbItem("Rule 2", ID.NewID, RuleIds.Rule)
                            {
                                new DbField("Rule")
                                {
                                    Value = @"<ruleset>  
                                                <rule uid=""{9D4698F2-61D7-4FF4-B17A-5D2CEA194E56}"">    
                                                </rule>
                                            </ruleset>"
                                }
                            }
                        }
                    }
                }
            };

            settings.ParentID = ItemIDs.SystemRoot;

            using (var db = new Db
            {
                new DbItem("test"),
                settings
            })
            {
                Item item = db.GetItem("/sitecore/content/test");

                var args = new GetLookupSourceItemsArgs
                {
                    Item = item,
                    Source = "query:/sitecore/content/"
                };


                var processor = Substitute.ForPartsOf<MyCustomRule>();

                // act
                processor.Process(args);

                //}

                // assert
                processor
                    .Received()
                    .ProcessRules(item, args.Source, Arg.Any<RuleList<MyCustomRuleContext>>(), args);
            }
        }

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.


        private DbItem BuildContent()
        {

            return new DbItem("WebSite Root", ID.NewID, new ID(Const.TemplateIds.WebsiteRootFolder))
            {
                new DbItem("Home", ID.NewID, ID.NewID)
                {
                    new DbItem("ContentItem", ID.NewID, ID.NewID)
                },
                new DbItem("Site Selection", ID.NewID, new ID(Const.TemplateIds.SelectionsFolder))
                {
                    new DbItem("Brand Colors", ID.NewID, new ID(Const.TemplateIds.BrandColors))
                    {
                        new DbItem("Green", ID.NewID, new ID(Const.TemplateIds.ListItem))
                    }
                },
                new DbItem("Keywords", ID.NewID, new ID(Const.TemplateIds.KeywordFolder))
                {
                    new DbItem("Test Keyword", ID.NewID, new ID(Const.TemplateIds.Keyword))
                }
            };
        }

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.


        private DbItem BuildRules(ID targetFolder, string token)
        {
            var abortPipeline = ID.NewID;
            var queryContainsToken = ID.NewID;
            var removeToken = ID.NewID;
            var runQueryFromItem = ID.NewID;
            var settings = new DbItem("Settings", ID.NewID)
                        {
                            new DbItem("Rules")
                            {
                                new DbItem("Definitions", ID.NewID, TemplateIDs.Folder)
                                {
                                    new DbItem("Elements", ID.NewID, TemplateIDs.Folder)
                                    {
                                        new DbItem("My Custom Rules", ID.NewID, RuleIds.ElementsFolderID)
                                        {
                                            new DbItem("Abort the pipeline", abortPipeline, RuleIds.Action)
                                            {
                                                {
                                                    "Type",
                                                    "Custom.Rules.MyCustomRules.Actions.Abort, Custom"
                                                }
                                            },
                                            new DbItem("Query Contains Token", queryContainsToken, RuleIds.Action)
                                            {
                                                {
                                                    "Type",
                                                    "Custom.Rules.MyCustomRules.Conditions.QueryContainsToken, Custom"
                                                }
                                            },
                                            new DbItem("Remove Token", removeToken, RuleIds.Action)
                                            {
                                                {
                                                    "Type",
                                                    "Custom.Rules.MyCustomRules.Actions.RemoveToken, Custom"
                                                }
                                            },
                                            new DbItem("Run Query From Item", runQueryFromItem, RuleIds.Action)
                                            {
                                                {
                                                    "Type",
                                                    "Custom.Rules.MyCustomRules.Actions.RunSimpleQueryFromItem, Custom"
                                                }
                                            }
                                        }
                                    }
                                },
                                new DbItem("My Custom Rules", ID.NewID, RuleIds.RulesContextFolderTemplateID)
                                {
                                    new DbItem("Rules", ID.NewID, RuleIds.RulesFolderTemplateId)
                                    {
                                        new DbItem("Rule 1", ID.NewID, RuleIds.Rule)
                                        {
                                            new DbField("Rule")
                                            {
                                                Value = $"<ruleset>" +
                                                        $"   <rule uid=\"{Guid.NewGuid()}\" name=\"token replacer\">" +
                                                        "       <conditions>" +
                                                        $"          <condition id=\"{queryContainsToken}\" uid=\"{Guid.NewGuid().ToString("N")}\" token=\"{token}\" />" +
                                                        "       </conditions>" +
                                                        "       <actions>" +
                                                        $"           <action id=\"{removeToken}\" uid=\"{Guid.NewGuid().ToString("N")}\" token=\"{token}\" />" +
                                                        $"           <action id=\"{runQueryFromItem}\" uid=\"{Guid.NewGuid().ToString("N")}\" itemid=\"{targetFolder.Guid}\" />" +
                                                        $"           <action id=\"{abortPipeline}\" uid=\"{Guid.NewGuid().ToString("N")}\" />" +
                                                        "       </actions>" +
                                                        "   </rule>" +
                                                        "</ruleset>"
                                            }
                                        }
                                    }
                                }
                            }
                        };

            settings.ParentID = ItemIDs.SystemRoot;
            return settings;
        }
    }

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.


        [TestCase("#selections#", "/sitecore/content/WebSite Root/Site Selection", "query:#selections#*[@@templateid='" + Const.TemplateIds.BrandColors + "']/*", "/sitecore/content/WebSite Root/Site Selection/Brand Colors/Green")]
        [TestCase("#keywords#", "/sitecore/content/WebSite Root/Keywords", "query:#keywords#*", "/sitecore/content/WebSite Root/Keywords/Test Keyword")]
        public void RulesWorksCorrectTokens(string token, string pathToTargetFolder, string query, string expectedItem)
        {
            using (var db = new Db { BuildContent() })
            {
                Item targetFolder = db.GetItem(pathToTargetFolder);
                Item contentItem = db.GetItem("/sitecore/content/WebSite Root/Home/ContentItem");
                Item testKeywordItem = db.GetItem(expectedItem);
                db.Add(BuildRules(targetFolder.ID, token));

                var args = new GetLookupSourceItemsArgs
                {
                    Item = contentItem,
                    Source = query 
                };

                var processor = new MyCustomRule();

                // act
                processor.Process(args);

                // assert
                args.Result.Should().HaveCount(1);
                args.Result.Should().Contain(testKeywordItem);
            }
        }

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 🙂

Brian Beckham

I am the President and CEO of BrainJocks. As a Sitecore MVP, I spend most of my time consulting and architecting software solutions for enterprise-level Sitecore projects. Learn more about Brian.

Add a Comment

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

Or request call back