How To Test Ruby CLI: The Setup


One of the most interesting challenges I met during Hanami development is to write isolated integration tests. The framework ships with an extensive Command Line Interface (CLI) to generate (or destroy) code, start the server, open a Ruby (or a database) console, create/drop the database, run the migrations, precompile assets, and print informations.

Full Isolation

As I want to have integration tests to use CLI commands for real, the test suite is free of mocks/stubs. When I ran it, only real commands are executed against the shell.

In this realistic scenario, I can’t use fixtures: the cost of maintaining them is too high. Instead, each single spec generates at the runtime a new Hanami project (via hanami new) with a given set of preconditions.

You may want to use pry as gem of choice for your Hanami console, or precompile sass assets. Did you forget to create the database before to migrate? There is a test for that!

There is only one way to guarantee correctness for these specs, and it is to run them in full isolation. The ci script looks for all the specs in spec/integration and it runs them one by one via rspec path/to/my_spec.rb. This is important: each spec uses its own fresh Hanami project in a separated Ruby process.

Preparatory Steps

This is a basic integration test to verify that the CLI command hanami version will print the current Hanami version.

RSpec.describe 'hanami version', type: :cli do
  it 'prints current version' do
    with_project do
      run_command 'hanami version', "v#{Hanami::VERSION}"
    end
  end
end

When the build starts there are a lot of preparatory steps. Let’s see what happens before this spec is executed.

Hanami uses Bundler to manage development dependencies. I want to use the “under-development” version of each Hanami framework. To do that, here’s how I declare the dependency:

# Gemfile
# ...
gem "hanami-utils", github: "hanami/utils"

This specifies to use hanami-utils from the GitHub repository, instead of the latest version from Rubygems. Bundler will clone the repository and hanami-utils source code will be copied in a directory.

But in order to make the tests real, I want to install Hanami frameworks as a gem, instead of using their code from a directory.

For this purpose the ci script runs bundle package --all. This command copies all the gems (*.gem) into vendor/cache. For all the dependencies declared with :github, it copies the updated repository into vendor/cache as well.

Here’s a simplified version of Hanami Gemfile:

# Gemfile
source "https://rubygems.org"

gem "i18n"
gem "hanami-utils", github: "hanami/utils"

# ...

That Bundler command will copy vendor/cache/i18n-0.7.0.gem and vendor/cache/utils-36a630acaf9e/ (still as a directory).

Then I use another script to packages each Hanami framework into a gem (eg: vendor/cache/hanami-utils-1.0.0.beta1.gem).

After that, the script generates an index of all the vendored gems (in vendor/cache).

Spec Execution

Now that all the dependencies are installed as a gem, the build can execute the single spec.

The method with_project (see the example above) is a testing facility that does a lot of useful things. It creates a temporary directory, it changes the current Ruby directory to into the temporary one (see Dir.chdir), and it generates a new full blown Hanami project.

Vendored Gems

When you generate a new Hanami project, its Gemfile uses Rubygems as source, but because I want to use the vendored gems, with_project patches the Gemfile for the new generated project:

# tmp/aruba/bookshelf/Gemfile
source 'file://vendor/cache'

# ...

Yes, Bundler allows the source to be a directory, only if that directory has an index of all the gems. This is why the ci script generates that index as last step of the setup (see above).

At this point, with_project runs bundler install --local, so the new generated project can use the vendored gems.

Run Commands

Now it can finally yield the block inside with_project. For our example it runs the hanami version command and it verifies if the output is correct.

This set of operations are delegated to run_command. It’s a wrapper on top of Aruba, a great CLI testing framework.

This run_command method, guarantees that the Hanami commands used at the testing time, are coming from the vendored gems. It also has the responsibility of running a given command via a system call, and to verify if stdout/stderr streams have the expected output. As last verification, it checks if the exit code was successful.

Cleanup

After the block is yielded, with_project cleans the temporary directory, which wipes the Hanami project used during that test. This prevents the same project to be used twice.

Conclusion

To test Hanami CLI is complex: it requires a reliable CI build (with full isolation of each spec), and a deep knowledge of the Ruby toolchain. If this looks complex, I can tell you we’ve just scratched the surface. Next time we’ll talk about code generators.

Luca Guidi

Family man, software architect, Open Source indie developer, speaker.

Rome, Italy https://lucaguidi.com