Mike Slinn

Structuring Jekyll Plugins For Testability

Published 2025-01-04.
Time to read: 2 minutes.

This page is part of the jekyll collection.

Mocking Jekyll data structures is difficult because they are undocumented, complex and intertwined. This makes testing a Jekyll plugin difficult. It is of course desirable to test a plugin's various methods before attempting to test it in situ. This article shows a simple way of doing that.

Decouple Methods From External State

If a method utilizes instance variables from the enclosing class, then you need an instance of the class to test it. This is impractical for Jekyll plugin developers as previously mentioned.

Instead, pass all parameters to methods that you wish to test. You can enforce this by defining the methods you want to test at the class level. C++ and Java programmers know this as a static method.

Class Methods

One way to define a method in a class is to preface its name with self when defining it:

Ruby code
class TestClass
  def self.a_method(msg)
    "a_method says #{msg}"
  end
end

puts TestClass.a_method "Hi!"

The above outputs a_method says Hi!.

Please allow me to demonstrate how this knowledge can be used to improve testability of Jekyll plugins.

Class-Level Mixins

Module Methods

A similar approach is to define class-level methods by mixing in module methods.

Class-level instance methods are simple to test if they do not reference external state.

Module methods cannot normally be directly invoked. However, when a module extends a class, the module methods are mixed into the class as class-level methods. You can access them from within the class enhanced by the mixin as self.class.method_name or ClassName.method_name

A Module is a collection of methods and constants. The methods in a module may be instance methods or module methods. Instance methods appear as methods in a class when the module is included, module methods do not. Conversely, module methods may be called without creating an encapsulating object, while instance methods may not. (See Module#module_function.)

I found the above somewhat vague. This StackOverflow answer and this one are more informative.

An example should make the above clear:

Ruby
module TestModule
  def a_method(msg)
    "a_method says #{msg}"
  end
end

class TestClass
  extend TestModule # Defines TestModule methods as class-level methods
end

puts TestClass.a_method 'Hi!'

The above outputs: a_method says Hi!

Testing with RSpec

We can use this approach to test many of the class-level methods in a Jekyll plugin without having to instantiate it. The simplest approach is to:

  1. Describe the module whose code is to be tested.
  2. Mix in described module into RSpec's test environment class with extend described_class.
  3. Reference the enhanced class with the functionality you want to test as self.class.

Here is an example:

spec/testable_spec.rb
require 'spec_helper'

module TestModule
  def a_method
    "a_method says #{msg}"
  end
end

RSpec.describe(TestModule) do
  extend described_class

  it 'Invokes a_method from module' do
    result = self.class.a_method 'Hi!'
    expect(result).to eq('a_method says Hi!')
  end
end

It would be cleaner to define a test class. The slight additional complexity is readily justified for non-trivial tests:

spec/test_class_spec.rb
require 'spec_helper'

module TestModule
  def a_public_method(msg)
    "a_public_method says #{msg}"
  end
end

class TestClass
  extend TestModule # Defines class methods

  # param1 and param2 are not important for this example
  def initialize(param1, param2)
    super()
    @param1 = param1
    @param2 = param2
  end
end

RSpec.describe(TestClass) do
  let(:test_class) { described_class.new('value1', 'value2') }

  it 'Invokes TestClass.a_public_method' do
    result = described_class.class.a_public_method "Hi!"
    expect(result).to eq('a_public_method says Hi!')
  end
end

For completeness, here is my spec_helper.rb:

spec/spec_helper.rb
RSpec.configure do |config|
  # config.order = 'random'
  config.filter_run_when_matching focus: true

  # See https://relishapp.com/rspec/rspec-core/docs/command-line/only-failures
  config.example_status_persistence_file_path = 'spec/status_persistence.txt'
end
* indicates a required field.

Please select the following to receive Mike Slinn’s newsletter:

You can unsubscribe at any time by clicking the link in the footer of emails.

Mike Slinn uses Mailchimp as his marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp’s privacy practices.