Ruby on Rails: Running Tests with Guard and Docker

Guard is a great tool for quickly running your Rails app tests/specs as your develop the code. As you edit and change the files in your app, Guard can trigger tests specific to the files you are modifying.

If you're using Docker to develop your app, however, running those tests/specs can be time consuming. Spinning up a container for every run soon becomes time consuming and wastes resources.

A better approach during development is to use a container for your app that is running Guard. This container will monitor changes to your files and run your tests within itself.

Whilst there have previously been issues with Guard picking up file changes across the Docker mount (due to the underlying use of VirtualBox, for example), recent releases of Docker (for Mac) seem to have resolved the issue.

In this tutorial, you'll see how to use a Docker container to run Guard and monitor your app's code for changes.

A Basic Dockerised Rails App

Starting with an app that has some tests (minitest) or specs (rspec) in place, add the guard gem and appropriate plugins for your test suite (e.g. guard-minitest and/or guard-rspec):

# Gemfile
group :development do
  gem 'guard'
  gem 'guard-minitest'
  gem 'guard-rspec' # If you use Rspec instead of Minitest
end

If your Rails app doesn't already have a Dockerfile, the following example is a good starting point. You might need to include more packages depending on your app's specific needs. See Getting Started with Docker and Ruby on Rails for more details about how to set this up.

# Dockerfile
# /path/to/your/app/Dockerfile
FROM ruby:2.4.3

RUN apt-get update -qq && apt-get install -y nodejs

# Cache Gems
WORKDIR /tmp
ADD Gemfile .
ADD Gemfile.lock .

RUN bundle install --jobs 4

# Copy your app's code into the image
WORKDIR /usr/src/app
ADD . /usr/src/app

# Precompile assets
RUN bundle exec rails assets:precompile

# RUN apt-get install -y libreadline6 libreadline6-dev

# Expose port 3000 to other containers
ENV PORT 3000
EXPOSE $PORT

# Run the rails server
CMD rails s -b 0.0.0.0 -p $PORT

When your Dockerfile is ready, you can build (and tag) an image from it. In this tutorial, the image tag will be myapp:latest:

$ docker build -t myapp:latest .

Next, check your suite runs by running your tests/specs inside a Docker container:

$ cd /path/to/your/app
$ docker run --rm -it -v ${PWD}:/usr/src/app myapp:latest bin/rails test # or bin/rails spec

Running via Spring preloader in process 19
Run options: --seed 61489
# Running:
...
Finished in 1.462146s, 2.0518 runs/s, 6.1553 assertions/s.
3 runs, 9 assertions, 0 failures, 0 errors, 0 skips

Installing Guard

With our tests running inside Docker, it's time to set up Guard. Again, this can be done using the existing Docker image for your app:

$ docker run -it -v ${PWD}:/usr/src/app --rm myapp:latest bundle exec guard init minitest rspec

Here, I've included both the minitest and rspec guard plugins, but you might only want to include the plugin that is relevant for your app.

Your app's folder will now include a Guardfile. Open it up, and edit according to your needs. For example, if you're using Rspec, the default configuration will look like:

# Guardfile
guard :rspec, cmd: "bundle exec rspec" do
  require "guard/rspec/dsl"
  dsl = Guard::RSpec::Dsl.new(self)

  # Feel free to open issues for suggestions and improvements
  ...
end

See the README file for your plugin(s) to learn about their various configuration options.

Run Guard

With Guard configured, we can now spin up a new container that is running Guard and monitoring the app's code. Be sure to mount your code folder into the container, so that changes are applied as you make them:

$ docker run -it -v ${PWD}:/usr/src/app --rm myapp:latest bundle exec guard

The container will attach to Guard as if it was running on your host machine. For example, you can press enter to run all your tests:

[1] guard(main)> # Enter
12:09:56 - INFO - Run all
12:09:56 - INFO - Running all specs
....

Finished in 0.39492 seconds (files took 4.05 seconds to load)
4 examples, 0 failures

Now if you edit one of your test/spec files, Guard should pick up the change and automatically run your test/spec, as if it was running locally!

To exit Guard (and remove the container), just type exit or hit Ctrl+D

Adding Guard to Your Compose File

Now that we know Guard is working and monitoring file changes inside our Docker container, you can add a declaration to your Docker Compose file. This will allow you to run Guard automatically as part of your development suite when you call docker-compose up.

One caveat, though, is that we can't use Guard's interactive console when calling docker-compose up. The process does not attach to the console, and so the container immediately fails. A workaround for this is to add the --no-interactions argument to the container's command:

# docker-compose.yml
version: '3.2'

services:
  web:
    build: .
    ports:
      - '3000:3000'
    volumes:
      - .:/usr/src/app
    environment:
      - RAILS_ENV=development
    # ...

  guard:
    build: .
    volumes:
      - .:/usr/src/app
    environment:
      - RAILS_ENV=development
    command: bundle exec guard --no-bundler-warning --no-interactions

Now when you call docker-compose up, you will see the Guard container start up and waiting for notifications from the filesystem:

$ docker-compose up
guard_1  | 12:23:39 - INFO - Guard::RSpec is running
guard_1  | 12:23:39 - INFO - Guard is now watching at '/usr/src/app'
web_1    | => Booting Puma
web_1    | => Rails 5.1.4 application starting in development
web_1    | => Run `rails server -h` for more startup options
web_1    | Puma starting in single mode...
web_1    | * Version 3.10.0 (ruby 2.4.3-p205), codename: Russell's Teapot
web_1    | * Min threads: 5, max threads: 5
web_1    | * Environment: development
web_1    | * Listening on tcp://0.0.0.0:3000
web_1    | Use Ctrl-C to stop

As before, if you now edit one of your test/spec files, you'll see Guard triggers the appropriate tests:

12:24:16 - INFO - Running: spec/mailers/admin_mailer_spec.rb
guard_1  | ....
guard_1  |
guard_1  | Finished in 0.6642 seconds (files took 3.75 seconds to load)
guard_1  | 4 examples, 0 failures

With everything working, you can now continue building your app as before, safe in the knowledge that whilst it is running, Guard is monitoring your tests, and will flag any breaking tests.