Unit Tests for Liquid Templates

Facebook
Twitter
LinkedIn

As we use developer tools and syntax to write Liquid templates, it would make sense to write unit test cases. We can ensure that the templates produce the correct result using a variety of data sources. I have not seen any useful implementations to achieve this level of quality assurance, so I have created an open source library that give Liquid unit test support.

This post is the second in a series about the Liquid language.

Background

The Liquid template engine that is used in Microsoft Azure is based on the DotLiquid library.

DotLiquid is a .Net port of the popular Ruby Liquid templating language. It is a separate project that aims to retain the same template syntax as the original, while using .NET coding conventions where possible. For more information about the original Liquid project, see https://shopify.github.io/liquid/.

This library uses my .NET 6.0 port of the same library, to allow for cross-platform compilation and tooling support. If you want to use it, then install it via the Nuget package manager. Liquid template syntax is fairly straight forward. It allows us to do several powerful transformation actions, such as:
  • control flow
  • iteration
  • variables
  • templates
  • filters
  • data selectors
Below is a simple example of Liquid syntax:
				
					{% comment -%}
Short sample showing how iteration and data selectors with in Liquid
{% endcomment -%}
<ul id="products">
  {% assign products = data.payload.warehouse.products -%}
  {% for product in products -%}
    <li>
      <h2>{{product.name}}</h2>
      Only {{product.price | price }}

      {{product.description | prettyprint | paragraph }}
    </li>
  {% endfor -%}
</ul>
				
			
I recommend learning the syntax and its operators first, as well as using the online test tools:

Azure Specific Differences

A key within this project is that I created it to be as compatible and usable for Azure implementations as possible. Therefore, it is important to understand how the DotLiquid and Azure implementations of the library differ from Shopify Liquid.

  • Liquid templates follow the file size limits for maps in Azure Logic Apps.
  • When using the date filter, DotLiquid supports both Ruby and .NET date format strings (but not both at the same time). By default, it will use .NET date format strings.
  • The JSON filter from the Shopify extension filters is currently not implemented in DotLiquid.
  • The standard Replace filter in the DotLiquid implementation uses regular expression (RegEx) matching, while the Shopify implementation uses simple string matching.
  • Liquid by default uses Ruby casing for output fields and filters such as {{ some_field | escape }}. The Azure implementation of DotLiquid uses C# naming convention, in which case output fields and filters would be referenced like so {{ SomeField | Escape }}.

For further details, see the Microsoft documentation.

Unit Testing Liquid Templates

First of all, take a look at the source code repository in Github. Azure uses a set of predefined feature uses of DotLiquid. For example, an Azure Logic App mapping service uses the “content” accessor for any data submitted using a workflow action. The LiquidParser class exposes a set of SetContent methods used to either set:
  • objects (will render down to JSON)
  • JSON string
  • XML string (will parse as XDocument then to JSON)
The object can then be accessed under the “content” variable in the Liquid template.
				
					{% assign albums = content.CATALOG.CD -%}
[{%- for album in albums limit:3 %}
  {
    "artist": "{{ album.ARTIST }}",
    "title": "{{ album.TITLE}}"
  }{% if forloop.last == false %},{% endif %}
  {%- endfor -%}
]

				
			
Our object data is in this case XML, and has been added as a hierarchical selector object, here named “CATALOG”, containing an array of “CD” objects. These are loaded under the hood by parsing text to an XmlDocument then back to JSON using the LiquidParserSetContentXml method. Similarly, we can load JSON data either using a string with LiquidParser.SetContentJson, or using object serialization with LiquidParser.SetContent. Note this method’s parameter forceCamlCase allows us to ensure that camel JSON formatting is preserved in the selectors. Step by step:
  1. Create a new test project – I use the XUnit framework but it’s up to you
  2. Add a reference to the AzureLiquid Nuget package
  3. I also use the FluentAssertions package, you can optionally add this
  4. Create a resource file
  5. Add three files:
    • Liquid template
    • Source JSON or XML data file
    • Expected outcome file
  6. Add the files as file resources to your resource file and set file type to text (properties pane for the resource)
  7. Use the arrange-act-assert pattern to ensure the files work as example below.
				
					using System.Text.RegularExpressions;
using AzureLiquid;
using FluentAssertions;
using Xunit;

public class JsonLiquidTests
{
    /// <summary>
    /// Ensures loading JSON from a file and parsing with a liquid file template works.
    /// </summary>
    [Fact]
    public void EnsureJsonBodyTemplateParsing()
    {
        // Arrange
        var expected = Resources.JsonTestExpected;
        var parser = new LiquidParser()
            .SetContentJson(Resources.JsonTestContent)
            .Parse(Resources.JsonTestTemplate);

        // Act
        var result = parser.Render();

        // Assert
        result.Should().NotBeEmpty("A result should have been returned");
        CompareTextsNoWhitespace(result, expected).Should()
            .BeTrue("The expected result should be returned");
    }
    
    /// <summary>
    /// Compares two text snippets but ignores differences in whitespace.
    /// </summary>
    /// <param name="text1">The first text.</param>
    /// <param name="text2">The second text.</param>
    /// <returns><c>true</c> if the texts match, otherwise <c>false</c>.</returns>
    private static bool CompareTextsNoWhitespace(string text1, string text2)
    {
        var spaces = new Regex(@"[\s]*");
        return string.CompareOrdinal(spaces.Replace(text1, string.Empty), spaces.Replace(text2, string.Empty)) == 0;
    }
}
				
			

There are several more examples within the source code repository. If you have any questions, find any issues or have a feature request, then use the discussions section of the repository.

Leave a Reply

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