---
title: Write a Vim Plugin with TDD
teaser: Write a Vim plugin with confidence using TDD.
tags: vim,tdd
author: Gabe Berke-Williams
published_on: 2014-08-15
---

My colleague [Chris Toomey] and I are writing a Vim plugin called [vim-spec-runner]
that runs RSpec and JavaScript tests. We're test-driving it and learned a
lot.

[Chris Toomey]: https://twitter.com/christoomey
[vim-spec-runner]: https://github.com/gabebw/vim-spec-runner/

## Vimrunner

We're using the excellent [vimrunner] Ruby gem. It provides a Ruby interface to
run arbitrary commands in Vim by hooking into [Vim's client-server
architecture].  If Vim was compiled with `+clientserver`, you can launch a Vim
process that acts as a command server, with a client that sends commands to it.
Vimrunner launches a Vim server and then sends your commands to it.

[Vim's client-server architecture]: http://vimdoc.sourceforge.net/htmldoc/remote.html#client-server

We discovered that terminal Vim doesn't work with vimrunner, but [MacVim] works
perfectly. Vimrunner will pick up MacVim if it's installed, so all you have to
do is `brew install macvim`. If you're using Linux/BSD, it might work out of the
box, but we haven't tried it.

[MacVim]: https://code.google.com/p/macvim/

Here's how to use Vimrunner to sort a file:

```ruby
VimRunner.start do |vim|
  vim.edit "file.txt"
  vim.command "%sort"
end
```

Vimrunner also comes with some neat RSpec helpers. You can look at
vim-spec-runner's [full spec_helper], but here are the important bits:

```ruby
require "vimrunner"
require "vimrunner/rspec"

ROOT = File.expand_path("../..", __FILE__)

Vimrunner::RSpec.configure do |config|
  config.reuse_server = true

  config.start_vim do
    vim = Vimrunner.start
    vim.add_plugin(File.join(ROOT, "plugin"), "spec-runner.vim")
    vim
  end
end
```

First, we require `vimrunner` and `vimrunner/rspec`, for RSpec support. Then we
set the `ROOT` constant to the directory containing the `plugin` directory. We
then configure Vimrunner's RSpec integration:

* `config.reuse_server = true`: Use the same Vim instance for every spec.
* `config.start_vim`: This block is used whenever Vimrunner needs a new Vim
  instance. We're using it to always add our plugin to the vim instance.

[vimrunner]: https://github.com/AndrewRadev/vimrunner
[full spec_helper]: https://github.com/gabebw/vim-spec-runner/blob/master/spec/spec_helper.rb

## Customizing RSpec

We use RSpec's [custom matchers] quite a bit in our specs. They reduce the
amount of code we have to write and make our tests much more readable.

[custom matchers]: https://www.relishapp.com/rspec/rspec-expectations/v/3-0/docs/custom-matchers/define-matcher

Here's an example:

```ruby
it "does not create a mapping if one already exists" do
  using_vim_without_plugin do |clean_vim|
    clean_vim.edit "my_spec.rb"
    clean_vim.command "nnoremap <Leader>x <Plug>RunCurrentSpecFile"
    load_plugin(clean_vim)

    expect(clean_vim).to have_no_normal_map_from("<Leader>a")
  end
end
```

`using_vim_without_plugin` and `load_plugin` are both plain Ruby methods defined
in our [main spec file]. Here's the [`have_no_normal_map_from` matcher]:

[main spec file]: https://github.com/gabebw/vim-spec-runner/blob/master/spec/plugin/spec_runner_spec.rb
[`have_no_normal_map_from` matcher]: https://github.com/gabebw/vim-spec-runner/blob/master/spec/support/matchers/have_no_normal_map_from.rb

```ruby
RSpec::Matchers.define :have_no_normal_map_from do |expected_keys|
  match do |vim_instance|
    mapping_output(vim_instance, expected_keys) == 'No mapping found'
  end

  failure_message_for_should do |vim_instance|
    "expected no map for '#{expected_keys}' but it maps to something"
  end

  def mapping_output(vim_instance, expected_keys)
    vim_instance.command "nmap #{expected_keys}"
  end
end
```

The outer block variable, `expected_keys`, is what we pass to `expect`, while
the block variable for `match` is what we pass to the `have_no_normal_map_from`
method.

We define `failure_message_for_should` so that if there is a mapping, we get a
useful, human-formatted error message.

## Travis CI

It took some work, but we got our plugin running on [Travis CI]. Vimrunner
needs a Vim that was compiled with `+clientserver`, so we install the
`vim-gnome` Ubuntu package. Vimrunner also needs an X server, so we use [xvfb]
to start up a [headless] X environment. Here's the result:

```yml
before_install:
  - "sudo apt-get update"
  - "sudo apt-get install vim-gnome"
  - "vim --version"
install: bundle
script: xvfb-run bundle exec rspec --format documentation
```

We use the `--format documentation` option to RSpec so we can see exactly which
test failed on Travis. You can see our [full .travis.yml file here] and a
[sample test run here].

[Travis CI]: https://travis-ci.org/
[xvfb]: http://en.wikipedia.org/wiki/Xvfb
[headless]: http://en.wikipedia.org/wiki/Headless_system
[full .travis.yml file here]: https://github.com/gabebw/vim-spec-runner/blob/master/.travis.yml
[sample test run here]: https://travis-ci.org/gabebw/vim-spec-runner/builds/26970237

## What's next

Try out [vim-spec-runner]! If you want to add tests to your own Vim plugin,
check out the full [spec file][spec-file].

[vim-spec-runner]: https://github.com/gabebw/vim-spec-runner
[spec-file]: https://github.com/gabebw/vim-spec-runner/blob/master/spec/plugin/spec_runner_spec.rb
