specdown

Build Status

Write your README in markdown, and execute it with specdown.

Why?

When you write a README for a library, a class, a command, etc., you're forced to stop and consider your user:

  • how are they going to use it?
  • what's the API
  • how am I going to convince them to use my library?

What if you write the README first, before writing your code? This is the premise of README Driven Development. See Tom Preston-Werner's blog post on the topic for a quick introduction to all of its benefits.

Duplication

One pain point I've encountered with README Driven Development is duplication between my tests and my documentation. Quite often, I'll end up spending a good deal of time translating most of my README's into executable tests in Cucumber or RSpec. Not only does this lower my productivity, it also forces me to maintain information about my code in two places: the documentation, and the tests.

Wouldn't it be great if your documentation and your tests were one and the same? For me, this was the promise of Cucumber, a tool I still use and love. However, I find that the repetitive nature of Gherkin, along with the hidden nature of the step definitions, mitigates against the likelihood that my feature files will actually serve as the primary documentation for my project. Readers will tune out when asked to read a page full of repetitive "Given/When/Then" scenarios, or they'll be forced to look elsewhere for the information they need because the step definitions hide the API.

Installation

Right now, specdown only supports Ruby. Next, I'll write a javascript implementation. Then, I don't know what language. Regardless, the goal is that you could use specdown with any programming language you desire.

To install the specdown ruby gem, simply:

$ gem install specdown

It comes with a specdown command. Try running it. Doesn't matter where.

Usage

Let's write a simple test in (github-flavored) markdown, and execute it with specdown. Create a "specdown" directory, then save the following text into a file inside of it. I'll assume you're calling it "example.markdown":

# Our first test!

This is our very first test. It's going to blow your mind.

```ruby
raise "WTF?" unless 1 == 1
```

Ok, if you've been following along, then ls -R should return the following directory structure:

$ ls -R

  specdown/
    example.markdown

Great. Now run the specdown command:

    $ specdown

        .

        1 markdown
        1 test
        1 success
        0 failures

Booya!

How does it work?

specdown loads any "markdown" files it can find inside the "specdown" directory, parses them into trees, then performs exhaustive depth-first searches on the trees to execute the code.

Let's update our README to help illustrate this:

# Our first test!

This is our very first test. It's going to blow your mind.

```ruby
raise "WTF?" unless 1 == 1
```

## A Subsection

In this section, we're going to create a variable.

```ruby
name = "moonmaster9000"
```

### A sub subsection

In this subsection, we have access to anything created or within scope in parent sections:

```ruby
raise "name not in scope" if !defined? name
```

## Another Subsection

In this subsection, we don't have access to the "name" variable. Think of your markdown as a tree.

```ruby
raise "name in scope" if defined? name
```

Read through that. I'm giving you some important scoping hints in it.

Save it, run it.

$ specdown

    ..

    1 markdown
    2 tests
    0 failures

Notice how the headers in your markdown form a tree?

                #Our first test!
                    /    \
                   /      \
                  /        \
                 /          \
                /            \
               /              \
              /                \
      ##A Subection       ##Another Subsection
            /
           /
          /
    ###A sub subsection

Specdown turned that tree into two tests. The first test (#Our first test! --> ##A Subsection --> ###A sub subsection):

raise "WTF?" unless 1 == 1
name = "moonmaster9000"
raise "name not in scope" if !defined? name

Here's what the second test looked like (#Our first test! --> ##Another Subsection)

raise "WTF?" unless 1 == 1
raise "name in scope" if defined? name

Non-executing code blocks

It's likely that in the process of writing your documentation tests, you'll want to add some code into your markdown that you don't want executed. Perhaps it's code in a different language, or perhaps you're showing off some command line functionality.

Specdown only executes fenced codeblocks specifically flagged as ruby. Thus, if you want to add some code to your markdown that shouldn't be executed, then just don't specifically flag it as Ruby:

# Non-Executing Code Blocks Example

Here's an example of a non-executing code block:

    $ cd /

Here's another example of a non-executing code block:

```javascript
console.log("I'm javascript, so I won't execute.");
```

A third example:

```
I'm not flagged as anything, so I won't execute.
```

## Executing codeblocks

The only way to make a code block execute is to specifically flag it as Ruby

```ruby
puts "I execute!"
```

Setting up your test environment

Similar to the cucumber testing framework: if you put a ruby file somewhere inside your "specdown" directory, specdown will find it and load it.

Configuring the Expectation / Assertion framework

As of version 0.1.0, specdown supports both RSpec expectations and Test::Unit assertions.

Specdown will default to RSpec expectations, but if it can't find the "rspec" gem installed on your system, it will fall back to Test::Unit assertions.

You can also configure Specdown manually to use RSpec expectations or Test::Unit assertions.

RSpec expectations

Create a "support" directory inside your specdown directory, and add an env.rb file containing the following Ruby code:

Specdown::Config.expectations = :rspec

You can now use RSpec expectations in your tests.

Using Test::Unit::Assertions

Create a "specdown/support/env.rb" file in your app, then add the following to it:

Specdown::Config.expectations = :test_unit

You can now use Test::Unit::Assertions inside your tests.

Test hooks (before/after/around)

You can create test hooks that run before, after, and around tests. You can create global hooks, or hooks that run only for specific specdown files.

Global hooks

To create a global before hook, use the Specdown.before method:

Specdown.before do
  puts "I run before every single test!"
end

That before hook will - you guessed it - RUN BEFORE EVERY SINGLE TEST.

Similary, you can run some code after every single test via the Specdown.after method:

Specdown.after do
  puts "I run after every single test!"
end

Whenever you need some code to run before and after every single test, use the Specdown.around method:

Specdown.around do
  puts "I run before _AND_ after every single test!"
end

Scoping your hooks to specific markdown files

You might, at times, want hooks to run only for certain files.

You can pass filenames (or regular expressions) to the Specdown.before, Specdown.after, and Specdown.around methods. The hooks will then execute whenever you execute any markdown file with matching filenames.

Specdown.before "somefile.markdown", /^.*\.database.markdown$/ do
  puts "This runs before every test within 'somefile.markdown', and
        before every test in any markdown file whose filename ends 
        with '.database.markdown'"
end

specdown command line

You can run specdown -h at the command line to get USAGE and options.

If you run specdown without any arguments, specdown will look for a "specdown" directory inside your current working directory, then search for any markdown files inside of it, and also load any ruby files inside of it.

Running specific files (or directories)

If you want to run just a single file or a set of files, or a directory of files, simply pass them on the command line:

$ specdown specdown/test.markdown
$ specdown specdown/unit_tests specdown/simple.markdown specdown/integration_tests/

Overriding the default root directory

You can use the -r flag to specify the root of the specdown directory (it defaults to "specdown/").

$ specdown test.markdown -r specdown_environment/

Colorization

By default, specdown will output colorized terminal output. If you'd rather the output not be colorized, you can use the -n or --non-colorized switch:

$ specdown -n

You can also turn off colorization in your env.rb by setting the reporter to Specdown::TerminalReporter:

Specdown::Config.reporter = Specdown::TerminalReporter

The reporter defaults to Specdown::ColorTerminalReporter.

Report format: short or condensed

Currently, we offer two report formats: short, or condensed. Short offers only the most basic information, whereas condensed will provide you with summary details per file.

You can toggle between the two either by setting switches at the command line:

$ specdown -f short
$ specdown --format=short
$ specdown -f condensed
$ specdown --format=condensed

You can also configure this in your env.rb by setting Specdown::Config.format to either :short or :condensed:

Specdown::Config.format = :condensed

The default is :short.

TODO

This library is still very new, but I am rapidly adding features to it. Here's what is on the immediate horizon:

  • allow flagged text in the markdown to execute code, like a cucumber step definition
  • offer the option of outputing the actual markdown while it executes, instead of "..F....FF......"
  • Better stack traces / reporting