Back to Repositories

Testing Liquid Template Filter Integration in Shopify/liquid

This integration test suite validates Liquid template filters functionality, focusing on custom filter implementation, chaining, and context handling. The tests ensure proper filter behavior across different scenarios and filter combinations.

Test Coverage Overview

The test suite provides comprehensive coverage of Liquid’s filter system, including:

  • Custom filter creation and registration
  • Filter method overriding and precedence
  • Built-in filter functionality (size, join, sort, etc.)
  • Filter argument handling and validation
  • Global vs local filter scope

Implementation Analysis

The testing approach utilizes Minitest framework with modular filter definitions and context manipulation. Tests demonstrate both simple and complex filter scenarios, including method overriding, argument passing, and filter chaining patterns specific to Liquid template processing.

Technical Details

Testing infrastructure includes:

  • Minitest as the testing framework
  • Custom filter modules (MoneyFilter, CanadianMoneyFilter, SubstituteFilter)
  • Liquid::Context for template rendering environment
  • TestObject class for object handling tests

Best Practices Demonstrated

The test suite exemplifies solid testing practices including:

  • Isolated test cases with clear setup
  • Comprehensive edge case coverage
  • Proper error handling validation
  • Modular filter organization
  • Clear test naming conventions

shopify/liquid

test/integration/filter_test.rb

            
# frozen_string_literal: true

require 'test_helper'

module MoneyFilter
  def money(input)
    format(' %d$ ', input)
  end

  def money_with_underscore(input)
    format(' %d$ ', input)
  end
end

module CanadianMoneyFilter
  def money(input)
    format(' %d$ CAD ', input)
  end
end

module SubstituteFilter
  def substitute(input, params = {})
    input.gsub(/%\{(\w+)\}/) { |_match| params[Regexp.last_match(1)] }
  end
end

class FiltersTest < Minitest::Test
  include Liquid

  module OverrideObjectMethodFilter
    def tap(_input)
      "tap overridden"
    end
  end

  def setup
    @context = Context.new
  end

  def test_local_filter
    @context['var'] = 1000
    @context.add_filters(MoneyFilter)

    assert_equal(' 1000$ ', Template.parse("{{var | money}}").render(@context))
  end

  def test_underscore_in_filter_name
    @context['var'] = 1000
    @context.add_filters(MoneyFilter)
    assert_equal(' 1000$ ', Template.parse("{{var | money_with_underscore}}").render(@context))
  end

  def test_second_filter_overwrites_first
    @context['var'] = 1000
    @context.add_filters(MoneyFilter)
    @context.add_filters(CanadianMoneyFilter)

    assert_equal(' 1000$ CAD ', Template.parse("{{var | money}}").render(@context))
  end

  def test_size
    assert_template_result("4", "{{var | size}}", { "var" => 'abcd' })
  end

  def test_join
    assert_template_result("1 2 3 4", "{{var | join}}", { "var" => [1, 2, 3, 4] })
  end

  def test_sort
    assert_template_result("1 2 3 4", "{{numbers | sort | join}}", { "numbers" => [2, 1, 4, 3] })
    assert_template_result(
      "alphabetic as expected",
      "{{words | sort | join}}",
      { "words" => ['expected', 'as', 'alphabetic'] },
    )
    assert_template_result("3", "{{value | sort}}", { "value" => 3 })
    assert_template_result('are flower', "{{arrays | sort | join}}", { 'arrays' => ['flower', 'are'] })
    assert_template_result(
      "Expected case sensitive",
      "{{case_sensitive | sort | join}}",
      { "case_sensitive" => ["sensitive", "Expected", "case"] },
    )
  end

  def test_sort_natural
    # Test strings
    assert_template_result(
      "Assert case Insensitive",
      "{{words | sort_natural | join}}",
      { "words" => ["case", "Assert", "Insensitive"] },
    )

    # Test hashes
    assert_template_result(
      "A b C",
      "{{hashes | sort_natural: 'a' | map: 'a' | join}}",
      { "hashes" => [{ "a" => "A" }, { "a" => "b" }, { "a" => "C" }] },
    )

    # Test objects
    @context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
    assert_equal('A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context))
  end

  def test_compact
    # Test strings
    assert_template_result(
      "a b c",
      "{{words | compact | join}}",
      { "words" => ['a', nil, 'b', nil, 'c'] },
    )

    # Test hashes
    assert_template_result(
      "A C",
      "{{hashes | compact: 'a' | map: 'a' | join}}",
      { "hashes" => [{ "a" => "A" }, { "a" => nil }, { "a" => "C" }] },
    )

    # Test objects
    @context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
    assert_equal('A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context))
  end

  def test_strip_html
    assert_template_result("bla blub", "{{ var | strip_html }}", { "var" => "<b>bla blub</a>" })
  end

  def test_strip_html_ignore_comments_with_html
    assert_template_result(
      "bla blub",
      "{{ var | strip_html }}",
      { "var" => "<!-- split and some <ul> tag --><b>bla blub</a>" },
    )
  end

  def test_capitalize
    assert_template_result("Blub", "{{ var | capitalize }}", { "var" => "blub" })
  end

  def test_nonexistent_filter_is_ignored
    assert_template_result("1000", "{{ var | xyzzy }}", { "var" => 1000 })
  end

  def test_filter_with_keyword_arguments
    @context['surname'] = 'john'
    @context['input']   = 'hello %{first_name}, %{last_name}'
    @context.add_filters(SubstituteFilter)
    output              = Template.parse(%({{ input | substitute: first_name: surname, last_name: 'doe' }})).render(@context)
    assert_equal('hello john, doe', output)
  end

  def test_override_object_method_in_filter
    assert_equal("tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter]))

    # tap still treated as a non-existent filter
    assert_equal("1000", Template.parse("{{var | tap}}").render!('var' => 1000))
  end

  def test_liquid_argument_error
    source = "{{ '' | size: 'too many args' }}"
    exc = assert_raises(Liquid::ArgumentError) do
      Template.parse(source).render!
    end
    assert_match(/\ALiquid error: wrong number of arguments /, exc.message)
    assert_equal(exc.message, Template.parse(source).render)
  end
end

class FiltersInTemplate < Minitest::Test
  include Liquid

  def test_local_global
    with_global_filter(MoneyFilter) do
      assert_equal(" 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil))
      assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: CanadianMoneyFilter))
      assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: [CanadianMoneyFilter]))
    end
  end

  def test_local_filter_with_deprecated_syntax
    assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, CanadianMoneyFilter))
    assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter]))
  end
end # FiltersTest

class TestObject < Liquid::Drop
  attr_accessor :a
  def initialize(a)
    @a = a
  end
end