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.