Showing posts with label Testing. Show all posts
Showing posts with label Testing. Show all posts

Monday, 29 April 2019

Using a custon xUnit Theory test to load data from Solr


For one of my tests I needed the ability to be able to load JSON data taken from solr. I found the following blog entry by Andrew Lock: https://andrewlock.net/creating-a-custom-xunit-theory-test-dataattribute-to-load-data-from-json-files/ and extended what he did to make it work for Solr data.

In my version I am tightly coupling the attribute to a specific data model, as unfortunately there is not a generic version of the DataAttribute.

 public class ProductListJsonFileAttribute : JsonFileDataAttribute
    {
        public ProductListJsonFileAttribute(string filePath)
            : base(filePath)
        {
        }

        public ProductListJsonFileAttribute(string filePath, string propertyName)
            : base(filePath, propertyName)
        {
        }

        public override IEnumerable<object[]> GetData(MethodInfo testMethod)
        {
            if (testMethod == null) throw new ArgumentNullException(nameof(testMethod));

            // Get the absolute path to the JSON file
            var path = _filePath;

            if (!File.Exists(path)) throw new ArgumentException($"Could not find file at path: {path}");

            // Load the file
            var fileData = File.ReadAllText(_filePath);

            if (string.IsNullOrEmpty(_propertyName))
            {
                return new List<object[]>{new object[]{ JsonConvert.DeserializeObject<List<ProductSearchResultsItem>>(fileData) } };
            }

            // Only use the specified property as the data
            var allData = JObject.Parse(fileData);
            var data = allData[_propertyName];
            return new List<object[]> { new object[] { data.ToObject<List<ProductSearchResultsItem>>() } };
        }
    }

To use this in a test, do something like:
        [Theory]
        [ProductListJsonFile(@".\Resources\productdata.json", "docs")]
        public void TestAttribute(List<ProductSearchResultsItem> productItems)
        {
            productItems.Count.Should().BeGreaterThan(0);
        }

With the JSON file looking something like this:
{
  "docs": [
    {"ProductGroup": "Textured Saxony",
      "productgroup_sc": "Textured Saxony",
      "ProductInstanceID": "26605020",
      "Size": "12"},
    {"ProductGroup": "Textured Briton",
      "productgroup_sc": "Textured Briton",
      "ProductInstanceID": "26605021",
      "Size": "14"},
    {"ProductGroup": "Textured Flax",
      "productgroup_sc": "Textured Flax",
      "ProductInstanceID": "26605022",
      "Size": "16"}]
}

Thursday, 28 January 2016

Sitecore FakeDB and real world testing

One of the biggest issues in my opinion with Sitecore at present, is that it is relatively hard to unit test anything you build for it.  This is because none of the integral pieces (Item, Database, Security, etc.) have interfaces, are often sealed classes or have no parameter-less constructors, making it extremely hard to mock.  There are ways around this, such as stand-alone mocks Microsoft Fakes, but I have never found this particularly easy to implement and they don't work well ReSharper's test runners.

Obviously components that don't integrate with Sitecore directly (controllers for example) or where perhaps you can use Glass Mapper can be mocked out, to some extent.

I recently came across Sitecore FakeDB (https://marketplace.sitecore.net/Modules/Sitecore_FakeDb.aspx) and wondered if this might be the answer to my problems.

I wondered if we could test something substantial like a pipeline processor?

The code examples below assume that:
  • You are familiar with how to set up the configuration for Sitecore FakeDB (see the documentation tab on the above Sitecore Marketplace link).
  • You are familiar with xUnit (https://xunit.codeplex.com/)
  • You are familiar with mocking  and/or duck casting technologies.
  • You have a basic understanding of Sitecore.
I also used a combination of NSubstitute (http://nsubstitute.github.io/) and Moq (https://github.com/Moq/moq4), as Moq had issues setting up the GetItem<T> method found on Glass Mapper's SitecoreService class.

N.B. Moq kept reporting no such extension method - I don't believe that GetItem<T> is an extension method - but I needed to get going so I used NSubstitute instead.

Brief:

I want to write a test for a pipeline processor, without having to make too many changes to the existing code.

I have an item resolver; an HttpRequestProcessor that looks at the current URL passed via arguments, and makes a decision to redirect to another or not.

All HttpRequestProcessors are passed HttpRequestArgs in a Process method which contains contextual information such as the current URL, etc.

The code:

Here is my process method (the starting point for my pipeline processor):

public override void Process(HttpRequestArgs args)
{

   Assert.ArgumentNotNull(args, "args");
   if (Context.Item != null || Context.Database == null || Context.Database.Name.ToLowerInvariant() == "core" || args.Url.ItemPath.Length == 0)
    {
       return;
    }

   if (args.Url.FilePath.Contains("/sku/"))
    {
        GetSkuItem(args);
    }

   if (!args.Url.FilePath.Contains("/brands/")) return;
   Context.Item = GetBucketItem(args);
}

So the first issue I hit was how to construct the HttpRequestArgs, which is in itself is a sealed class (so cannot be mocked) and is constructed using a combination of both HttpRequest (not base) and HttpResponse (not base) both of which are hard to mock out.

This is what I came up with:

private HttpRequestArgs CreateHttpRequestArgs(string url)
{
  _redirect = new StringBuilder();

  var response = new HttpResponse(new StringWriter(_redirect));
  var request =
      new HttpRequestArgs(
          new System.Web.HttpContext(new HttpRequest("", url, ""),
          response), HttpRequestType.End);

  var dynMethod = request.GetType().GetMethod("Initialize", BindingFlags.NonPublic | BindingFlags.Instance);
      dynMethod.Invoke(request, null);
  return request;
}

The important part here is the small piece of reflection at the end, that calls the private Initialize method on the HttpRequestArgs object, which amongst other things populates the URL on the object itself.

The StringWriter above is passed a StringBuilder, which we will use later. 

Now my test methods can use this to construct the HttpRequestArgs and send it through to the processor.

So, lets start with something really simple, no database (anything else will require us to start using Sitecore FakeDB).

[Fact]
public void Null_Database_Item()
{
  var request = CreateHttpRequestArgs("http://myhost/");

  _bucketItemResolver.Process(request);

  Sitecore.Context.Item.Should().BeNull();
}

N.B. The _bucketItemResolver class is initialized in the constructor for this class

Now lets pass in context item, which should also fail at the first if in the above Process method where it checks for a null context item.

[Fact]
public void A_Context_Item()
{
   using (var db = new Db {new DbItem("Home") {{"Title", "Welcome!"}}})
   {

     var request = CreateHttpRequestArgs("http://myhost/");

     var homeItem = db.GetItem("/sitecore/content/home");
     Assert.Equal("Welcome!", homeItem["Title"]);

     Sitecore.Context.Item = homeItem;

     _bucketItemResolver.Process(request);

     Sitecore.Context.Item.Should().Be(homeItem);
    }
}

In the above code, we construct the Fake Db with a single item.  We set the context to be that item, and when the test is run the Process method checks for a null context and halts any further processing immediately because that condition fails.

A more complex test:

One of the URL checks I have, is one that looks for the following pattern: '/sku/'. 

The processor does the following if it finds a URL pattern match in the GetSkuItem method:
  1. Splits up the file-path to determine if there is a SKU number after the pattern.
  2. Does a product search with SKU against a custom index to find the product.
  3. Uses a link provider to build the URL to that product
  4. Redirects to the new URL
private void GetSkuItem(HttpRequestArgs args)
{
   try
   {
      var filepath = args.Url.FilePath;

      var splitPath = filepath.ToLowerInvariant().Split('/').RemoveWhere(p => p.Equals(string.Empty));
      var pathPaths = splitPath as string[] ?? splitPath.ToArray();
      if (!pathPaths.Any() || pathPaths.Count() <= 1) return;
      var productSearchString = pathPaths[1];
      string redirectUrl;
      var originalUrl = args.Url.FilePath;
      lock (RedirectionCache)
      {
          if (!RedirectionCache.TryGetValue(originalUrl, out redirectUrl))
          {
              var product = ProductsSearchIndex.GetProductBySku(productSearchString);
              if (product != null)
              {
                   redirectUrl = new ProductLinkProvider().GetItemUrl(product.GetItem(),
                     (UrlOptions) UrlOptions.DefaultOptions.Clone());

                   if (!string.IsNullOrWhiteSpace(redirectUrl))
                   //Add new url to the cache.
                   if (!RedirectionCache.ContainsKey(originalUrl))
                      RedirectionCache.Add(originalUrl, redirectUrl);
               }
           }
        }

        if (!string.IsNullOrWhiteSpace(redirectUrl)) // There is a redirection match
        {
            try
            {
                args.Context.Response.RedirectPermanent(redirectUrl, true);
            }
            catch (ThreadAbortException)
            {
               Log.Info(string.Format("Permanent Redirection being applied for {0} to {1}", originalUrl, redirectUrl),
                     this);
            }
            catch (NullReferenceException)
            {
               //Used for testing
               args.Context.Response.Write(redirectUrl);
               throw;
            }
         }
      }
      catch (ThreadAbortException)
      {

      }
      catch (Exception ex)
      {
         Log.Error(ex.Message, ex, this);
      }
} 

Here is the test that I wrote:

[Fact]
public void Its_A_Sku()
{
   var index = Substitute.For<Sitecore.ContentSearch.ISearchIndex>();
   Sitecore.ContentSearch.ContentSearchManager.SearchConfiguration.Indexes.Add("product_master_search_index", index);

   using (var db = new Db
           {
              new DbItem("Home") {{"Title", "Welcome!"}},
              new DbItem("Globals/Import/products/10074-test-toy")
                {
                  {"Name", "10074-test-toy"},
                  {"SKU", "10074"},
                  {"Product Name", "10074 Test Toy"},
                  {"Brand", "{989FA9CC-0522-4C6F-AECD-B11F6AC84CFB}"}
                }
            })
    {
        var homeItem = db.GetItem("/sitecore/content/home");
        Assert.Equal("Welcome!", homeItem["Title"]);

        var request = CreateHttpRequestArgs("http://myhost/sku/10074");

        var searchResultItem = Substitute.For<ProductSearchResultItem>();

        searchResultItem.GetItem()
            .Returns(db.GetItem("/sitecore/content/Globals/Import/products/10074-test-toy"));
        searchResultItem.SKU = "10074";
        searchResultItem.Product_Name = "10074 Test Toy";
        searchResultItem.Name = "10074-test-toy";

        var mockglassProduct = new Moq.Mock<GlassProduct>();

        mockglassProduct.Setup(p => p.TemplateId).Returns(IImported_ProductConstants.TemplateId.Guid);
        mockglassProduct.Setup(p => p.Product_Name).Returns(searchResultItem.Product_Name);
        mockglassProduct.Setup(p => p.Name).Returns(searchResultItem.Name);
        mockglassProduct.Setup(p => p.Brand).Returns(new Imported_Brand{Name = "Test Brand"});

        var mockSitecoreService = Substitute.For<ISitecoreService>();

        mockSitecoreService.GetItem<GlassProduct>(Arg.Any<Guid>())
                           .Returns(mockglassProduct.Object);


        ProductLinkProvider.SitecoreService = mockSitecoreService;

        index.CreateSearchContext()
            .GetQueryable<ProductSearchResultItem>()
            .Returns((new[] {searchResultItem}).AsQueryable());

        _bucketItemResolver.Process(request);

        Sitecore.Context.Item.Should().BeNull();

        _redirect.ToString().Should().Be(string.Format("/brands/{0}/{1}", mockglassProduct.Object.Brand.Name, mockglassProduct.Object.Name));
     }
}

  • I recreate the search index using Sitecore FakeDB.
  • Set up the in-memory database with two items.
  • Mock up the Glass Mapper SitecoreService to return the expected search result item.
  • Set the index to return the correct item from the in-memory DB (make sure the values are correct as linQ to Sitecore will be performed against the item).
  • Execute the processor.
  • And finally check the StringBuilder to see if it has the redirected URL in it.

 I had to make a couple of changes to the existing code to get this test to work. 

  1. I had to add the NullReferenceException catch as there was no way to get the HttpResponse which is used to do the redirect to delegate and do something else instead.  If the processor arguments used HttpResponseBase I would have been able to mock out a delegate method to call. 

    Its possible that the redirect might be able to be done with some other Sitecore Utility methods, but as these would undoubtedly be static, again it would be hard to mock them out.

    I took the opportunity here to populate the StringBuilder with a value - not the neatest but at least I can test a result.

  2. In the ProductLinkProvider class we use the SitecoreService.  Unfortunately this is resolved from DI using Kernel.Resolve (found on a static helper class), so I had to add a property to the class, which I could use in testing, to set up my mocked version of the service and have the code check for a valid service in that property. 

    N.B. If I had had time, I may have been able to rewrite the provider so that the SitecoreService was injected in properly.
Conclusions

All in all I am very happy with Sitecore FakeDb and will be using it to write more comprehensive tests in the future.  I give it two thumbs up!

If in the future Sitecore can make some minor changes with testing in mind, then that would be the icing on the cake.

Saturday, 2 January 2016

Testing in software development and parallels with the real world

I have often thought that we can see many parallels to software development in other professions and walks of life.

The other day we had a plumber come out to our house to fit a water purification system (in the crawl).  At some point during the install he mentioned that he would need to fit a pressure reducing valve as the water pressure in the house was too high for his equipment - for the sake expediency I agreed.  At the time I was surprised that after completing the install he never once set foot in the house.  He handed over his invoice and went on his way, the only thing he said was that if there were any issues please call the office.

A little while later I went to run a faucet (tap) in the house and was surprised (but with hindsight perhaps not really!) that the water came out with a dribble.  I tried the showers and again the pressure was so low that there was no way you could wash under flow of water.  I called their office, and the because the plumber was on another job, the owner had to come (1 hour round trip) spend 15 minutes adjusting (and testing!) to finally get it right.  

What struck me here, was that this was not the first time that this pattern of events had happened to me when calling out a trade to the house.  I am also sure after speaking to friends that this has happened frequently for others as well.  I later found out there are regulations that lay out the kind of tests a plumber should be performing - most importantly to ensure that someone doesn't get scolded!  So why were these not followed?  Do they need to be scripted out?

I suspect that the plumber had done this so many times, that he felt that his assumptions were as good as doing the tests.  Sound Familiar? - But as we know assumptions are the mothers of all mistakes!*

But I know from experience that if I have a set tests laid out before me that require a single click to run, I will use them, and more often than not I will add to and update them as and when necessary.  I also know that developers shy away from writing tests, often siting that it takes too much time.  But is that really their call? Feels to me that we need a culture shift...

The implications for my plumber, also apply to developers:
  1. Customer/end-user gets frustrated/annoyed and may not use them again
  2. Business has additional expense to fix the issue
  3. Business owner/key staff are taken away from running his business
  4. Legal implications if injured, and so on.
I have come across many businesses where the software development teams behave just like my plumber.  They churn out code with little or no testing with a subconscious expectation that QA or (more expensively) the business will test their code.   They make assumptions about how things should operate and rarely have any concrete evidence (unit tests) that their code does in fact work!  

Any business that builds software and relies solely on QA/UAT as their only line of defense will end up with a high turn over of senior staff, software that doesn't perform/doesn't work period/fails frequently, and lazy programmers.

If you get:
  • Your software developers to spend the extra time writing unit tests.
  • Implement a CI platform
  • Have a UAT environment that as closely as possible matches production 
  • Have developers/deployment teams prepared to smoke test deploys based on a set of scripts.
  • Have scripts prepared for UAT
Then:
  • Issues will be caught earlier in the process
  • UAT becomes an easy step-by-step process - a sign off.
  • You reduce the cost to the business 
  • You reduce the strain on the most important people in your business.
  • And more importantly less failures once the code is in production
*Paraphrasing the Marcus Penn (Everett McGill) from the movie Under Siege 2