Automated tests are not only good for dynamic code. You can (and probably should) test your static web sites as well. Here is an example of me testing the Fedora Developer website using Ruby, RSpec and Capybara.

First, let’s briefly stress out why the testing is really needed. Fedora Developer is statically built with Jekyll and the content itself is created by many contributors. As you can imagine, things can break pretty easily. It’s one thing to test the layout and things you are in control of, but another when you want to make sure that people are creating new content as expected.

One of the things we want to make sure of is the correct internal linking of other pages. Not everybody immediately knows how to write a relative Markdown link for the website that is going to be generated later. Even as a reviewer I might be looking it up. Solution? Write tests and run them before the deployment of the new version of the website.

Since Jekyll already implies that Ruby is installed and available, let’s write a Capybara/Webkit specs similarly as if we would test a Rails web application. In the website root we add a new spec directory with the following spec_helper.rb :

# This spec needs rubygem-rack, rubygem-capybara and rubygem-rspec installed. # Run as `rspec spec/` in the project root directory. require 'rack' require 'capybara' require 'capybara/dsl' require 'capybara/session' require 'capybara/rspec' require_relative './shared_contexts.rb' class JekyllSite attr_reader :root , :server def initialize ( root ) @root = root @server = Rack :: File . new ( root ) end def call ( env ) path = env [ 'PATH_INFO' ] # Use index.html for / paths if path == '/' && exists? ( 'index.html' ) env [ 'PATH_INFO' ] = '/index.html' elsif ! exists? ( path ) && exists? ( path + '.html' ) env [ 'PATH_INFO' ] += '.html' end server . call ( env ) end def exists? ( path ) File . exist? ( File . join ( root , path )) end end # Setup for Capybara to test Jekyll static files served by Rack Capybara . app = Rack :: Builder . new do map '/' do use Rack :: Lint run JekyllSite . new ( File . join ( File . dirname ( __FILE__ ), '..' , '_site' )) end end . to_app Capybara . default_selector = :css Capybara . default_driver = :rack_test Capybara . javascript_driver = :webkit RSpec . configure do | config | config . include Capybara :: DSL # Make sure the static files are generated `jekyll build` unless File . directory? ( '_site' ) end

What are we doing here you ask?

We are transforming our Jekyll site to be a Rack application (Rack is Ruby HTTP interface).

We are using Rack::Builder to create our Capybara application.

to create our Capybara application. We are telling Capybara to use :webkit driver.

driver. Finally we are configuring RSpec to simply use Capybara DSL (that we use in the actual tests).

You might have noticed two more things.

We are requiring shared context. Our shared context tests the header, search box, and footer that is present on every generated page. Here is the source:

# What every single page should contain RSpec . shared_examples_for 'Page' do it "has top-level menu" do expect ( page ). to have_css ( "#logo-col a[href~='/']" ) expect ( page ). to have_link ( "Start a project" , href: '/start.html' ) expect ( page ). to have_link ( "Get tools" , href: '/tools.html' ) expect ( page ). to have_link ( "Languages & databases" , href: '/tech.html' ) expect ( page ). to have_link ( "Deploy and distribute" , href: '/deployment.html' ) expect ( page ). to have_css ( "ul.nav li" , count: 4 ) end it "has footer" do expect ( page ). to have_css ( ".footer" ) # 4 sections: About, Download, Support, Join expect ( page ). to have_css ( ".footer h3.widget-title" , count: 4 ) # Footer links expect ( page ). to have_link ( "About Developer Portal" , href: '/about.html' ) expect ( page ). to have_link ( "Fedora Magazine" , href: 'https://fedoramagazine.org' ) expect ( page ). to have_link ( "Torrent Downloads" , href: 'https://torrents.fedoraproject.org' ) expect ( page ). to have_link ( "Forums" , href: 'https://fedoraforum.org/' ) expect ( page ). to have_link ( "Planet Fedora" , href: 'http://fedoraplanet.org' ) expect ( page ). to have_link ( "Fedora Community" , href: 'https://fedoracommunity.org/' ) expect ( page ). to have_css ( ".footer a" , count: 27 ) expect ( page ). to have_css ( ".footer p.copy" , text: /© [0-9]+ Red Hat, Inc. and others./ ) end end # Search page does not contain form#search RSpec . shared_examples_for 'Page with search box' do it "has a search box next to the top-level navigation" do expect ( page ). to have_css ( "form#search" ) expect ( page ). to have_css ( "form#search input" ) expect ( page ). to have_css ( "form#search button" ) end end

We are also making sure to run jekyll build to generate the site (we expect and work with standard Jekyll _site directory).

With all of this setup we can write a regular Capybara test:

require 'spec_helper' # Array of all generated pages site = File . join ( File . dirname ( __FILE__ ), '..' , '_site' , '**' , '*.html' ) PAGES = Dir . glob ( site ). map { | p | p . gsub ( /[^_]+\/_site(.*)/ , '\\1' ) } PAGES . each do | p | describe p do it_behaves_like 'Page' it_behaves_like 'Page with search box' unless p == '/search.html' before :each do visit p end it 'has only valid internal hyperlinks' do page . all ( :css , 'a' ). each do | link | next if link . text == '' || link [ :href ]. match ( /(http|\/\/).*/ ) page . find ( :xpath , link . path ). click expect ( page . status_code ). to be ( 200 ), "expected link ' #{ link . text } ' to work" visit p end end end end

This test will iterate over all our pages, visit them, use our shared context thanks to it_behaves_like call, and finally run any further tests.

In the test example above all the relative links found on the page will be visited by Capybara (we are clicking on them with click() ). If such page does not exist, we fail our RSpec test with expectation.

After all of this in place, the idea is to just run rspec spec in the project root directory:

$ rspec spec ............................................................................................................................................................................................................................................................................................F...............F.......F................... Failures: 1 ) /tools/docker/about.html has only valid internal hyperlinks Failure/Error: expect ( page.status_code ) .to be ( 200 ) , "expected link '#{link.text}' to work" expected link 'configuring Docker' to work # ./spec/pages_spec.rb:20:in `block (4 levels) in <top (required)>' # ./spec/pages_spec.rb:17:in `block (3 levels) in <top (required)>' 2 ) /tools/docker/compose.html has only valid internal hyperlinks Failure/Error: expect ( page.status_code ) .to be ( 200 ) , "expected link '#{link.text}' to work" expected link 'Getting started with Docker on Fedora' to work # ./spec/pages_spec.rb:20:in `block (4 levels) in <top (required)>' # ./spec/pages_spec.rb:17:in `block (3 levels) in <top (required)>' 3 ) /tools/vagrant/about.html has only valid internal hyperlinks Failure/Error: expect ( page.status_code ) .to be ( 200 ) , "expected link '#{link.text}' to work" expected link 'Vagrant with libvirt' to work # ./spec/pages_spec.rb:20:in `block (4 levels) in <top (required)>' # ./spec/pages_spec.rb:17:in `block (3 levels) in <top (required)>' Finished in 7.8 seconds ( files took 0.36521 seconds to load ) 328 examples, 3 failures

Oh my, links are broken! Let’s fix them first…

On the Fedora Developer website this call is part of the deploy script:

$ ./deploy.sh Running specs... ........................................................................................................................................................................................................................................................................................................................................ Finished in 8.17 seconds ( files took 0.37417 seconds to load ) 328 examples, 0 failures Checking dependencies... ruby-2.2.3-44.fc22.x86_64 rubygem-liquid-3.0.1-1.fc22.noarch rubygem-actionview-4.2.0-2.fc22.noarch Uploading site from _site/, check that the content is current about.html 100% 9713 9.5KB/s 00:00 about.md 100% 0 0.0KB/s 00:00 ...

Trust me, this feels so much better. Finding the issues before the site is deployed is important both for your visitors and your good night sleeps!

Published on 24 February 2020 . Tags: ruby jekyll rspec testing howto

Any comments? Write me a DM on Twitter.