I finally got around to auto-testing the code snippets in my blog posts. As of today, the process of writing a blog post with code snippets goes like this:
Step 1: Run the background services
First I’ll run Foreman locally while in my blog’s dir. Foreman starts up three processes in the background: a Jekyll server, my résumé autorenderer, and a snippets autotester (the topic of this post). Each subprocess’ stdout and stderr are printed by Foreman, with each line properly prefixed such that it says which process printed out which line:
21:01:04 blog.1 | started with pid 88969 21:01:04 resume.1 | started with pid 88970 21:01:04 snippets.1 | started with pid 88971 21:01:04 resume.1 | make: Nothing to be done for 'resume'. 21:01:05 blog.1 | Configuration file: /Users/mark/src/github.com/relaxdiego/relaxdiego.github.com/config.yml 21:01:05 blog.1 | Source: /Users/mark/src/github.com/relaxdiego/relaxdiego.github.com 21:01:05 blog.1 | Destination: /Users/mark/src/github.com/relaxdiego/relaxdiego.github.com/site 21:01:05 blog.1 | Incremental build: disabled. Enable with --incremental 21:01:05 blog.1 | Generating... 21:01:06 blog.1 | done in 1.51 seconds. 21:01:06 blog.1 | Auto-regeneration: enabled for '/Users/mark/src/github.com/relaxdiego/relaxdiego.github.com' 21:01:07 blog.1 | Server address: http://127.0.0.1:4000 21:01:07 blog.1 | Server running... press ctrl-c to stop.
Step 2: Write a blog post
I then create a new file under my _posts
directory, writing content in the
usual way except for the code snippet. For that part, I’m going to write an include
directive. For example, if I wanted to include a Python snippet in this
post, I’d write the following:
{% highlight python linenos %} {% include code-snippets/autotest-post/s01_hello_world.py %} {% endhighlight %}
That tells Jekyll to include the contents of _includes/code-snippets/autotest-post/s01_hello_world.py
inside this blog post when it renders it in HTML form. If I save the blog
post now, the blog.1
subprocess would fail since the code snippet file
I want to include doesn’t exist yet. So I’m going to hold off on saving
it for now.
Step 3: Create the test for the code snippet
Just because we’re writing code snippets doesn’t mean we abandon our TDD principles. We’re not savages! So then I’m going to write the test for the snippet first and, for this example, it’ll just be:
1
2
3
4
5
# _includes/code-snippets/autottest-post/s01_hello_world_test.py
from s01_hello_world import hello_world
assert hello_world() == "Hello, world!"
When I save this file now, we’re going to immediately see Foreman spew a test failure in the terminal:
22:03:45 snippets.1 | Events: OwnerModified Created AttributeModified IsFile Updated 22:03:45 snippets.1 | Snippet: /\_includes/code-snippets/autotest-post/s01_hello_world_test.py 22:03:45 snippets.1 | Type: Python 22:03:45 snippets.1 | Tester: autotest-post/s01_hello_world_test.py 22:03:45 snippets.1 | pwd: /Users/mark/src/github.com/relaxdiego/relaxdiego.github.com 22:03:45 snippets.1 | + python /Users/mark/src/github.com/relaxdiego/relaxdiego.github.com/\_includes/code-snippets/autotest-post/s01_hello_world_test.py 22:03:45 snippets.1 | Hello world! 22:03:45 snippets.1 | Traceback (most recent call last): 22:03:45 snippets.1 | File "/Users/mark/src/github.com/relaxdiego/relaxdiego.github.com/\_includes/code-snippets/autotest-post/s01_hello_world_test.py", line 1, in module 22:03:45 snippets.1 | from s01_hello_world import hello_world 22:03:45 snippets.1 | ImportError: cannot import name 'hello_world' from 's01_hello_world' (/Users/mark/src/github.com/relaxdiego/relaxdiego.github.com/\_includes/code-snippets/autotest-post/s01_hello_world.py) 22:03:45 snippets.1 | FAILED
The key part of the error there is this: ImportError: cannot import name 'hello_world' from 's01_hello_world'
which makes sense because we’ve written the tests but we haven’t written
the actual code that we’re testing. TDD FTW, baby!
Step 4: Create the snippet
Nothing else to do, right? I’m going to write the snippet file as follows:
1
2
3
4
5
# _includes/code-snippets/autottest-post/s01_hello_world.py
def hello_world():
return "Hello, world!"
When we save that, we see this output from Foreman:
22:14:46 snippets.1 | Events: IsFile Renamed 22:14:46 snippets.1 | Snippet: /\_includes/code-snippets/autotest-post/s01_hello_world.py 22:14:46 snippets.1 | Type: Python 22:14:46 snippets.1 | Tester: autotest-post/s01_hello_world_test.py 22:14:46 snippets.1 | pwd: /Users/mark/src/github.com/relaxdiego/relaxdiego.github.com 22:14:46 snippets.1 | + python /Users/mark/src/github.com/relaxdiego/relaxdiego.github.com/\_includes/code-snippets/autotest-post/s01_hello_world_test.py 22:14:46 snippets.1 | PASSED
Sweeet!
At this point, when I save this blog post’s .md
file, Jekyll will happily
include the snippet file’s contents into the rendered HTML, neither knowing nor
caring that the file was fully tested by this awesome setup. And that’s
totally OK because I want to keeps things decoupled this way.
Step 5: Run all tests as a final sweep
Just like the usual TDD workflow, I run all the snippet tests as a final step before pushing my changes:
for snippettest in _includes/code-snippets/**/*_test.*; do script/test-snippet "$snippettest"; done
Right now script/test-snippet
only supports Python but nothing’s stopping it
from supporting other languages down the line. It only takes a bit of my time
and energy.
Talk is cheap. Show me the code!
Amen to that! Alright, if you want to see how this magic is done, you should
start with my Procfile
since that’s what Foreman reads when it starts up. Check out the command that
gets executed by the snippets
entry and then just fall into the rabbit hole
from there!
Enjoy. :-)
Oh and, of course, all code snippets in this blog post are available on Github!