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.
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.
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.
Here’s how to use Vimrunner to sort a file:
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:
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.
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.
Here’s an example:
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:
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:
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.
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.