Adding tests to an existing project

05 Jan 2023

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.

Job listings

Post a Job!

Did you enjoy reading this? Follow me on Twitter or sign up to my newsletter for more content like this!

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!

Related posts