Sunday, January 13, 2013

Sitecore Unit Testing and Auto Include Config Files

I'm working on a open source project that involves running NUnit tests against Sitecore. I had originally set these up using the CodeFlood Test Runner, which creates a simple ASPX page for running NUnit tests so they have access to HttpContext. My partner on the project, Mike Edwards, pointed out this was unnecessary for unit testing Sitecore.

As long as you move you bring over your configuration and DLLS, and make a few other minor changes (documented by Mike here), you can reference Sitecore databases and items without an HttpContext, which allows you to run Sitecore unit tests inside a normal NUnit test runner. After I set up my environment following this procedure, I was able to successfully run Mike's sample test of loading the Master DB "/sitecore" root node, but my own tests were failing because the configuration patches in "App_Config/Include" were not getting loaded by the runtime. I could grab a copy of the patched Sitecore node from /Sitecore/Admin/ShowConfig.aspx, and save it to my test project, but this seemed a cumbersome solution.

I did a little digging with ReSharper and found the method Sitecore.Configuration.Factory.GetConfiguration, which in turn calls LoadAutoIncludeFiles.  This method is passed the results of MainUtil.MapPath("/App_Config/Include"). Hmmm, is the path getting mapped incorrectly?  Looking at FileUtil.MapPath turned up this:

if ((int) path[0] == 47)
return FileUtil.MakePath
    (!HostingEnvironment.IsHosted || Context.IsUnitTesting ? 
     Sitecore.Configuration.State.HttpRuntime.AppDomainAppPath : 
     System.Web.HttpRuntime.AppDomainAppPath, path.Replace('/', '\\'), '\\');

47 is the decimal value of the forward slash character (/), so this code will get executed for a path beginning with this character.  And the code states that if Sitecore.Context.IsUnitTesting is true, use a Sitecore version of AppDomainAppPath, rather than the one from the (unavilable) System.Web.HttpRuntime.  Worth a try.  And sure enough, adding
[SetUp]
public void SetUp()
{
  Sitecore.Context.IsUnitTesting = true;
  // additional setup code
}
to my NUnit SetUp method fixed the issue.  It turns out that Sitecore's AppDomainAppPath is set to the path of the executing Sitecore.Kernell.dll inside the Sitecore.Context method SetAppDomainAppPath:
string path1 = Path.GetDirectoryName(
  Sitecore.IO.FileUtil.GetFilePathFromFileUri(
    Assembly.GetExecutingAssembly().CodeBase));
The moral of the story: ReSharper is an awfully useful tool when troubleshooting Sitecore.

Update: Well, it's actually a little more complicated.  The first test to run in my TestFixture was always failing, with this stack trace:
SetUp : System.Web.Security.MembershipCreateUserException : The password-answer
supplied is invalid.
at Sitecore.Security.Domains.Domain.CreateAnonymousUser()
at Sitecore.Pipelines.Loader.EnsureAnonymousUsers.Process(PipelineArgs args)
at (Object , Object[] )
at Sitecore.Pipelines.CorePipeline.Run(PipelineArgs args)
at Sitecore.Context.SetActiveSite(String siteName)
at Sitecore.Context.set_IsUnitTesting(Boolean value)
It turns out that when you set IsUnitTesting, Sitecore checks if the Context site is set, and if not, it sets it to "shell". The SetActiveSite command kicks off the "initialize" pipeline, whose final entry:
causes the above exception to be thrown.  This only occurs on the first test because for subsequent tests, Sitecore.Context.Site has been set to "shell," so SetActiveSite is not called. There are two ways to work around this:

1) You can wrap the call to IsUnitTesting in a try/catch block, and disregard the exception:
[SetUp]
public void SetUp()
{
  try 
  {
    Sitecore.Context.IsUnitTesting = true;
  }
  catch(Exception) 
  { //expected on first call } 
}
(Note: Referring to the specific exception requires a reference to System.Web.ApplicationServices.  There's really nothing wrong with disregarding all exceptions in a SetUp method, since that's not the logic under test.)
2) You can remove or comment out the EnsureAnonymousUsers processor from the App.config version of the Initialize pipeline.

One thing you can't do is remove the pipeline processor in a config include file.  I was halfway through writing zUnitTesting.config, with a "patch:delete" line to remove this entry, when I realized that this would not work, since patches won't work until IsUnitTesting is set to true. A classic chicken/egg problem.  D'oh!

Update 2: I did a little more digging into how Sitecore.Configuration.State.HttpRuntime.AppDomainAppPath is set. It turns out that the setter method of Sitecore.Context.IsUnitTesting issues a call to Context.ConfigureHttpRuntime, which in turn calls Context.SetAppDomainAppPath. This method checks the location of the executing assembly, and strips off all directories up to and including "\bin," giving us the project root.  If the resulting path ends with "\Sitecore.UnitTest," this is replaced with "\Sitecore.Client." This logic was most likely put in place to support Sitecore's internal unit testing efforts.



1 comment:

  1. Hi Dan,

    I am facing a very similar issue of not being able to access "/App_Config/Include", but this is in case of an Windows Service project placed inside Sitecore Website, as it references certain Sitecore dlls (to pull analytics reporting data).

    How do I overcome this issue without bringing in [SetUp] attribute. I am trying to debig this service in my local.

    Please do confirm if its a good approach to place a windows service project inside Website path.

    Thanks!

    ReplyDelete