Published 2025-01-04.
Last modified 2025-05-31.
Time to read: 3 minutes.
jekyll
collection.
RSpec is useful for unit testing Jekyll plugins.
Files
This section contains scripts, Visual Studio Code launch configurations and source code for rspec
.
I have found that it is better to put all RSpec settings in spec/spec_helper.rb
instead of using a file called .rspec
to hold settings.
require 'jekyll' # For testing Jekyll plugins based on jekyll_plugin_support: require 'jekyll_plugin_logger' require 'jekyll_plugin_support' RSpec.configure do |config| # See https://relishapp.com/rspec/rspec-core/docs/command-line/only-failures config.example_status_persistence_file_path = 'spec/status_persistence.txt' # See https://rspec.info/features/3-12/rspec-core/filtering/filter-run-when-matching/ # and https://github.com/rspec/rspec/issues/221 config.filter_run_when_matching :focus # Other values: :progress, :html, :json, CustomFormatterClass config.formatter = :documentation # See https://rspec.info/features/3-12/rspec-core/command-line/order/ config.order = :defined # See https://www.rubydoc.info/github/rspec/rspec-core/RSpec%2FCore%2FConfiguration:pending_failure_output config.pending_failure_output = :skip end
When testing Jekyll plugins based on jekyll_plugin_support
,
you might want to run rspec
from the command line.
Jekyll plugins might need to be initialized from _config.yml
,
which by convention is found in the demo/
directory.
The following Bash script makes it easy to run all rspec
tests for
Jekyll plugins based on jekyll_plugin_support
:
bin/rspec
#!/bin/bash DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" cd "$DIR/../demo" || exit bundle exec rspec \ -I ../lib \ -I ../spec \ -P ../spec/**/*_spec.rb
The following .vscode/launch.json
fragment defines two launch configurations for debugging RSpec tests using Visual Studio Code.
The first launch configuration is esssentially in the same as the above Bash script,
while the second launch configuration just runs the RSpec tests defined in the currently selected RSpec file.
.vscode/launch.json
fragment{ "args": [ "-I", "${workspaceRoot}/lib", "-I", "${workspaceRoot}/spec", "-P", "${workspaceRoot}/spec/**/*_spec.rb" ], "cwd": "${workspaceRoot}/demo", "debugPort": "0", "name": "RSpec - all", "request": "launch", "script": "${workspaceRoot}/binstub/rspec", "type": "rdbg", "useBundler": true, }, { "args": [ "-I", "${workspaceRoot}/lib", "-I", "${workspaceRoot}/spec", "${file}" ], "cwd": "${workspaceRoot}/demo", "debugPort": "0", "name": "RSpec - active spec only", "request": "launch", "script": "${workspaceRoot}/binstub/rspec", "type": "rdbg", "useBundler": true, }
The above launch configurations should be wrapped within the following JSON structure:
{
"version": "0.2.0",
"configurations": [
Your launch configurations go here
]
}
Mocking
Mocking Jekyll data structures is difficult because they are undocumented, complex and intertwined. This makes testing some Jekyll plugins more difficult than it should be. It is of course desirable to test a plugin's various methods before attempting to test it in situ. The next sections show various ways of doing that using RSpec.
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 MyClass def self.a_method(msg) "a_method says #{msg}" end end puts MyClass.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 methods of a Jekyll plugin without having to instantiate it. The simplest approach is to:
- Add
require 'spec_helper'
to the top of each RSpec file. Describe
the module whose code is to be tested.-
Mix in the 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
.
Mocks
If you need test fixtures, you can define instance methods and class methods.
Following is an example of defining or overriding instance methods in MyModule
.
It works by
re-opening
MyModule
(defined in the lib/
directory or a gem) and
defines or overrides MyModule::a_method
for testing purposes.
require 'spec_helper' module MyModule def a_method "a_method says #{msg}" end end RSpec.describe(MyModule) 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
Following is an example of defining or overriding class methods in MyModule
.
require 'spec_helper' module MyModule def a_public_method(msg) "a_public_method says #{msg}" end end class TestClass extend MyModule # 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