Adding tests to an existing project
Develop the right mindset for Rails security
Avoid shipping vulnerable code by learning how to prevent security issues in your Rails applications.
Get the course for $99I 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.