RogerBW's Blog

A test harness for my Discourse plugin 19 October 2020

I have ended up maintaining a plugin for Discourse. As I got it, it had no tests. Fixing that was fun.

Discourse is largely written in Ruby on Rails, and its plugins need to be in Ruby. Fair enough; it's a procedural language, if more object-flavoured than many, and getting up a bit of fluency isn't hard.

This particular thing is a dice roller for forum-based boardgames and RPGs. Discourse didn't have one to start with, and though it's now grown a built-in one it's pretty basic. So originally I installed one that seems to be generally well-regarded; but that version has some quite major bugs in it (for example, it will reject a roll if the number of dice has a zero in it), and it repeats itself too much for my liking (the regexp definition of "what a request for a roll looks like" is in three different places). So I forked it, amended it a bit and put it on my own github space.

But testing it on Discourse is tricky. To get it into the live server I end up doing a full rebuild, which takes a while. I keep a toybox Discourse instance in a virtual machine at home specifically for this kind of thing; there I can push the code into the right place, then stop and restart to get it reloaded, but this still takes noticeable numbers of seconds (20+). What I want is a fast test wrapper that will exercise the various functions and make sure they're doing sensible things as I'm working on the core code, before I throw it at Discourse for final testing. Save code, run tests, see what's wrong, fix code, repeat.

Ruby has a test system; but the plugin is designed to run within Ruby on Rails. Its general form looks like

after_initialize do

  def # various functions I want to test
  end
  
  on(:post_created) do |post, params|
    # more code I want to test
  end
  
end

OK. So the def parts are easy enough; I define my own function in the test wrapper

def after_initialize
  yield
end

which means "when you meet the after_initialize keyword, run the block of code it's introducing". At that point, the functions get defined, and I can poke at them individually.

But what about on? That clearly introduces a block of code, but I don't want to run that block at this moment; I want to capture it and run it later at my whim. This ended up being:

$onblock=Proc.new { }

def on(post,&block)
  $onblock=block
end

defining a global $onblock which holds the code. I also define a Post class with the few methods of a real post that this plugin cares about. Then I can test it with, for example:

post=Post.new('[roll 3d6]')
srand(1602262750)
$onblock.call(post)
assert_match(/USERNAME asked for a die roll:.*`3d6: 4 \+ 2 \+ 3 = 9`/m,post.raw)

(Note the random seeding for a fixed output; this probably makes the code more fragile, because if the random algorithm changes all my tests will suddenly fail, but it will at least let me know if any part of it isn't doing the right thing.)

So now when I'm extending the plugin I can write the tests and make sure it's doing what it should almost instantly, at the command line, before I start poking it into an actual Discourse installation and doing the second stage testing of it there.

(And I've used it already, because I've just added a new subsystem for the special dice needed by the Genesys RPG – exercising it with tests before I tried it on real Discourse.)

Comments on this post are now closed. If you have particular grounds for adding a late comment, comment on a more recent post quoting the URL of this one.

Search
Archive
Tags 1920s 1930s 1940s 1950s 1960s 1970s 1980s 1990s 2000s 2010s 2300ad 3d printing action advent of code aeronautics aikakirja anecdote animation anime army astronomy audio audio tech base commerce battletech bayern beer boardgaming book of the week bookmonth chain of command children chris chronicle church of no redeeming virtues cold war comedy computing contemporary cornish smuggler cosmic encounter coup covid-19 crime crystal cthulhu eternal cycling dead of winter doctor who documentary drama driving drone ecchi economics en garde espionage essen 2015 essen 2016 essen 2017 essen 2018 essen 2019 essen 2022 essen 2023 essen 2024 existential risk falklands war fandom fanfic fantasy feminism film firefly first world war flash point flight simulation food garmin drive gazebo genesys geocaching geodata gin gkp gurps gurps 101 gus harpoon historical history horror hugo 2014 hugo 2015 hugo 2016 hugo 2017 hugo 2018 hugo 2019 hugo 2020 hugo 2021 hugo 2022 hugo 2023 hugo 2024 hugo-nebula reread in brief avoid instrumented life javascript julian simpson julie enfield kickstarter kotlin learn to play leaving earth linux liquor lovecraftiana lua mecha men with beards mpd museum music mystery naval noir non-fiction one for the brow opera parody paul temple perl perl weekly challenge photography podcast politics postscript powers prediction privacy project woolsack pyracantha python quantum rail raku ranting raspberry pi reading reading boardgames social real life restaurant reviews romance rpg a day rpgs ruby rust scala science fiction scythe second world war security shipwreck simutrans smartphone south atlantic war squaddies stationery steampunk stuarts suburbia superheroes suspense television the resistance the weekly challenge thirsty meeples thriller tin soldier torg toys trailers travel type 26 type 31 type 45 vietnam war war wargaming weather wives and sweethearts writing about writing x-wing young adult
Special All book reviews, All film reviews
Produced by aikakirja v0.1