BDD testing on Bridgetown

webdev bridgetown testing

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:

References