Published 2025-01-04.
Time to read: 2 minutes.
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:
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.
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 extend
s 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.
or ClassName.
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:
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:
Describe
the module whose code is to be tested.- Mix in described module into RSpec's test environment class with
extend described_class
. - Reference the enhanced class with the functionality you want to test as
self.class
.
Here is an example:
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:
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
:
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