Adding tests to an existing project
I created a little gem recently to help me automate server provisioning. Because it was just a prototype, and I didn’t know what the result would be, I haven’t written a single test for the gem, but now, it reached a point where others might use it and contribute to it, so having a test suite would be a great help, so I need to start adding tests, and I’d like to share how I approach this task.
Since the gem is already working, I will go from high-level to low-level tests. I am in a lucky position because since I wrote this code, I know how it works, but if it were someone else’s work, I would find the entry point of it and start adding tests from there.
Prepper is a pretty simple tool, I built it on top of SSHKit, and it has a command line
interface, which creates an instance of the Prepper::Runner
class with the provided configuration, which is ruby code, evaluates that code on the instance, and calls the run method:
module Prepper
class Runner
attr_accessor :host, :packages, :commands, :user, :port
def self.new(config)
runner = new
runner.new
runner
end
def initialize(config)
@packages = []
@commands = []
@user = "root"
@port = 22
instance_eval config
end
...
end
end
First, let’s write a test to verify that this method successfully executes. I need to create a test file in test/prepper/runner_test.rb
:
require "test_helper"
class RunnerTest < Minitest::Test
def test_it_runs
assert Prepper::Runner.new("")
end
end
This test passes, so let’s write tests for each config setter method of the runner:
def test_it_sets_host
code = <<-CODE
server_host "test.com"
CODE
runner = Prepper::Runner.new(code)
assert_equal("test.com", runner.host)
end
def test_it_sets_user
code = <<-CODE
server_user "ubuntu"
CODE
runner = Prepper::Runner.new(code)
assert_equal('ubuntu', runner.user)
end
def test_it_sets_port
code = <<-CODE
server_port 999
CODE
runner = Prepper::Runner.new(code)
assert_equal(999, runner.port)
end
def test_it_sets_ssh_options
code = <<-CODE
ssh_options({ forward_agent: false })
CODE
runner = Prepper::Runner.new(code)
assert_equal({forward_agent: false}, runner.instance_variable_get("@ssh_options"))
end
There is also a server_hash
method, which is passed to SSHKit, and built from the result of the above setter methods. Let’s write a test to make sure it is set correctly:
def test_server_hash
code = <<-CODE
server_host "test.com"
server_port 999
server_user "ubuntu"
ssh_options({ forward_agent: false })
CODE
runner = Prepper::Runner.new(code)
assert_equal(
{
hostname: 'test.com',
user: 'ubuntu',
port: 999,
ssh_options: {forward_agent: false}
},
runner.server_hash
)
end
We are making progress! Let’s see what else is going on in the runner.
Let’s write a simple test for add_command
first:
def add_command(command, opts = {})
package = Package.new("base", opts)
package.runner = self
opts[:user] ||= "root"
opts[:within] ||= "/"
package.commands << Command.new(command, opts)
@packages << package
end
If we look at the method definition, we can see it creates a package with the name “base” and forwards the options, then it adds a command to the package with the command string passed, and sets the user
in the command’s options to “root”, and the within
to “/” by default, so we will test that these happen
def test_add_command_adds_a_package_with_a_command
code = <<-CODE
add_command "ls /"
CODE
runner = Prepper::Runner.new(code)
refute_empty runner.packages
assert_equal 1, runner.packages.size
assert_equal 'base', runner.packages.first.name
package_options = runner.packages.first.instance_variable_get("@opts")
assert_equal 'root', package_options[:user]
assert_equal '/', package_options[:within]
end
This test passes, but we should also verify that we can override user
and within
:
def test_add_command_can_override_user_and_within
code = <<-CODE
add_command "ls", user: "ubuntu", within: "/home/ubuntu"
CODE
runner = Prepper::Runner.new(code)
package_options = runner.packages.first.instance_variable_get("@opts")
assert_equal 'ubuntu', package_options[:user]
assert_equal '/home/ubuntu', package_options[:within]
end
And now, we can test the package
method:
def package(name, opts = {}, &block)
@packages << Package.new(name, opts.merge(runner: self), &block)
end
It creates a package with the given name, passes the block, and adds it to the list of packages. We can write a simple test to verify the package is created with the correct name; the command we create in the block is added and added to the list of packages, and the runner of the package is set:
def test_package_registers_a_package
code = <<-CODE
package "list root" do
add_command "ls /"
end
CODE
runner = Prepper::Runner.new(code)
assert_equal 1, runner.packages.size
assert_equal "list root", runner.packages.first.name
assert_equal 1, runner.packages.first.commands.size
assert_equal runner, runner.packages.first.runner
end
Now we have some high-level tests for the core of the gem, so we will start adding tests for the Command and Package classes and the various “tools” the gem has.
I might cover that in the following article. Thanks for your attention. I hope you enjoyed the article.
Did you enjoy reading this? Sign up to the Rails Tricks newsletter for more content like this!
Or follow me on Twitter
I run an indie startup providing vulnerability scanning for your Ruby on Rails app.
It is free to use at the moment, and I am grateful for any feedback about it.If you would like to give it a spin, you can do it here: Vulnerability Scanning for your Ruby on Rails app!