BDD testing on Bridgetown
Automated testing is a great way to get confidence that you're not about to break production, and even on a personal site like this one it can be valuable for speeding up changes. I also love behaviour-driven-development and test frameworks that make it easy to read the tests like requirements, so let's put it into practice!
Bridgetown is a static site generator written in Ruby, which comes with a simple
minitest plugin. The bridgetown docs describe how to install the testing plugin, but the
defaults and examples are all assert
style, and assert
is hard to read. Let's fix that!
Why minitest?
I normally like rspec, but bridgetown has automatic integration of minitest. When writing tests, isn't important to keep everything fast so you can speed up your edit-test feedback loop as much as possible. Minitest is able to run very quickly because it's small and focussed, so it's a good choice to get confidence in my code without slowing me down!
Install the Minitest Plugin
The basic install looks like:
bin/bridgetown configure minitesting
bundle install --with test
That will add gems to the gemfile and set up some nice defaults.
Run the Default Tests
bundle exec bridgetown test
If all goes well, you'll see some green dots indicating success!
Switch to BDD tests
The main thing with BDD is to test like a user, decoupling your test from the implementation details. This can often be done using Cypress or another e2e test suite, but I want that at the unit test level too.
Basically, assert
is gross. Let's be better.
Add a simple spec test
In the test_homepage.rb
file that got scaffolded for us, add a new tautology test:
it "should have a category list" do
_(true).must_equal true
end
That should successfully pass when you re-run the tests, and fail when you change it to false
! That means the plumbing all works and we can write our actual tests.
Checking for DOM elements
Rails::Dom::Testing
really only works when it's got an implicit document_root defined on the test example object.
Bridgetown helpfully sets that up for us in the test/helper.rb
file, but it doesn't read as BDD when we assert against it.
Instead, we'll use a more explicit format, basically following the exercise and verify steps of the four-phase test pattern. We've got an easy way to select sets of elements using CSS selectors:
it "should have a body tag" do
body = css_select("body")
_(body).wont_be_empty
end
Note that css_select
returns a collection, which bit me at first, which is why you ALWAYS do a red-green check on your tests and make sure they can fail!
Checking for Visible Text
Who wants to build tests that depend on a particular DOM structure? I want to be able to add a div without having to rebuild my tests - let's decouple the tests from the implementation details!
Most of the time, the behaviour we care about is based on finding specific text on the page, visible to the user. And if that text is missing, we want a useful-for-humans failure message, instead of a serialized nokogiri tree.
To accomplish that, I added an extra method into test/helper.rb
:
def document_text
@document_root.to_str.gsub(/\s+\n+\s*/, "\\n")
end
This takes the root nokogiri node, renders it as a string without HTML tags, then collapses down all of the newlines and whitespace. This produces output that looks a lot like what you get from capaybara text matchers.
Here's a nice simple test using that to look for a particular visible heading:
it "should have a section for categories" do
_(document_text).must_match "Categories"
end
Accessing the Bridgetown Site Object
Since I'm working with a site generator (Bridgetown), it's useful to run some tests against the site
object rather than the rendered output too.
In this case, we're writing a single test case that loops over the data and verifies all of the entries, rather than having to write lots of duplicated blocks!
Let's use it to make sure my category data from the src/_data folder is loaded:
describe "Category Pages" do
let(:categories) do
site.data.categories.categories
end
it "should have multiple items in the category list" do
_(categories.size).must_be :>=, 5
end
it "should have string values" do
categories.each do |category|
_(category[0]).must_be_instance_of String
_(category[1]).must_be_instance_of String
end
end
end
Running the Tests On Deploy
Bridgetown automatically added the minitest suite as part of the deploy, which is great! Unfortunately it added it at the END, after my custom VPS_deploy builder step.
Setting the bridgetown hook priority changes it so that the tests are loaded earlier/later, but the minitest test run still happens at the end. :confused:
After digging into it, that's because I was using minitest/autorun, which automatically sets things up to run "on process exit", which in my case is the end of the overall rake process.
I got around this by removing the require of minitest/autorun
from test/helper, and editing the TestOutput
plugin to manually run minitest inline. By raising an exception when the test suite fails, I can stop the deploy rake task and avoid shipping broken code! :success:
Here's the modified test output hook:
# frozen_string_literal: true
module TestOutput
unless Bridgetown.env.development?
# set as priority:high so it runs first
Bridgetown::Hooks.register_one :site, :post_write, priority: :high do
require "nokogiri"
Dir["test/**/*.rb"].each { |file| require_relative("../#{file}") }
# Manually run tests INLINE, and block the deploy on failure.
tests_passed = Minitest.run
raise "Test Suite failed - stopping everything" unless tests_passed
rescue LoadError
Bridgetown.logger.warn "Testing:", "To run tests, you must first run `bundle install --with test`"
end
end
end
Summary
With only a few additional helpers, it's possible to decouple your minitest specs and have them follow BDD, which means less rewriting down the road!
And wow, this covered a lot of my favourite testing topics:
- Getting confidence in changes
- Behaviour-Driven Development (BDD)
- Fast edit-test feedback loops
- Four-phase testing pattern
- Decoupling tests from implementation
- Red-green testing
- "bulk" tests to avoid duplication
- automatically running tests as part of your deploy script ("CI")