rails


Bootstrapping a Rails 5 application with Rspec and Capybara

This is a guide for my Lighthouse Labs students who asked me to demonstrate unit-testing. Whenever a I initialize a new project, I have to relearn the whole setup process, so this serves as handy primer to both my students and myself.

Assuming Rails 5 is all set up and ready to go…

Create a new project

1
2
rails new myapp -T
cd myapp

The -T option skips creation of the default test files.

Add all the testing dependencies

There are a bunch of gems I regularly use for testing:

Add them all these gems to the development/test group in your application’s Gemfile:

1
2
3
4
5
6
7
8
9
10
# Gemfile
# ...
group :development, :test do
gem 'rspec-rails', '~> 3.5'
gem 'shoulda-matchers'
gem 'capybara'
gem 'factory_girl_rails'
end

Install, of course:

1
bundle install

Configuration

Naturally, each of these new testing gems requires a bit of configuration to get working.

rspec

rspec is one of many great test suites. I like it the best, but that’s probably just because it’s the one I’m used to. It’s also great because a lot of people use it. So if you don’t know how to test something, chances are someone on StackOverflow does.

The rspec-rails gem comes with a handy generator that does a bunch of the setup work for you:

1
rails generate rspec:install

This added a new spec/ directory with a couple of helper files to your project. All your tests are stored in this directory. Run your rspec tests like this:

1
bundle exec rspec

We haven’t written any tests yet so you should see something like this:

1
2
3
4
5
No examples found.
Finished in 0.00028 seconds (files took 0.16948 seconds to load)
0 examples, 0 failures

shoulda-matchers

shoulda-matchers make testing common Rails functionality much faster and easier. You can write identical tests in rspec all by itself, but with shoulda-matcher these common tests are reduced to one line.

To set this up, you paste the following to your spec/rails_helper.rb file:

1
2
3
4
5
6
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :rails
end
end

The tests you write with shoulda-matcher get executed when you run your rspec tests.

capybara

capybara tests the totality of your application’s functionality by simulating how a real-world agent actually interacts with your app.

First, paste the following into your spec/rails_helper.rb file:

1
require 'capybara/rails'

Now, paste the following into your spec/spec_helper.rb file:

1
require 'capybara/rspec'

factory_girl_rails

This provides a nice way to spoof the models you create in your application. To use it, you need to add a couple of things to your spec/spec_helper.rb file.

First, paste the following requirement:

1
require 'factory_girl_rails'

Then, add the following to the RSpec.configure block:

1
2
3
4
5
6
7
8
9
10
# RSpec
RSpec.configure do |config|
# ...
config.include FactoryGirl::Syntax::Methods
# ...
end

Try it out

If everything is configured correctly, all the necessary test files will be created each time you generate some scaffolding. To see the options available, run

1
rails generate

This will show you a list of installed generators, including all the rspec and factory_girl stuff.

See what happens when you run

1
rails generate scaffold Agent

Take a peak in the spec/ directory now. You’ll see a bunch of boilerplate test code waiting for you to fill in the blanks with meaningful tests.

Before you run the tests, though, you’ll need to migrate the database:

1
bin/rails db:migrate RAILS_ENV=test

Now you can run the tests:

1
bundle exec rspec

You’ll see a whole bunch of pending tests like this:

1
2
3
4
5
Pending: (Failures listed here are expected and do not affect your suite's status)
1) AgentsController GET #index assigns all agents as @agents
# Add a hash of attributes valid for your model
# ./spec/controllers/agents_controller_spec.rb:40

Get testing!


Deploy a Rails app to Docker with Capistrano

These instructions follow my previous post on deploying multiple Rails apps with Passenger, Nginx, and Docker. Go read (and do) all that first.

Assumptions

As always, this guide assumes the production server is running Ubuntu 14.04 and has all the requisite software already installed (e.g.: Docker, Rails, Capistrano, etc.). Further, it is assumed that you have a system similar to the one described here, and that by following the instruction provided, you have a Rails application deployed in a Docker container. I will be setting up Capistrano for the Rails app in that container.

Set up project in local development environment

Update the previous Docker configuration files

Nginx configuration

Change the existing docker/my-app.conf to look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server {
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;
server_name example.com;
# This used to be /home/app/my-app/public;
root /home/app/my-app/current/public;
# Passenger
passenger_enabled on;
passenger_user app;
passenger_ruby /usr/bin/ruby2.2;
}

Change Dockerfile

Since Capistrano will be building the app, all those steps can be removed from the Dockerfile. It should now look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
FROM phusion/passenger-ruby22:latest
MAINTAINER Some Groovy Cat "hepcat@example.com"
# Set correct environment variables.
ENV HOME /root
ENV RAILS_ENV production
# Use baseimage-docker's init process.
CMD ["/sbin/my_init"]
# Start Nginx and Passenger
EXPOSE 80
RUN rm -f /etc/service/nginx/down
# Configure Nginx
RUN rm /etc/nginx/sites-enabled/default
ADD docker/my-app.conf /etc/nginx/sites-enabled/my-app.conf
ADD docker/postgres-env.conf /etc/nginx/main.d/postgres-env.conf
# Install the app
ADD . /home/app/my-app
WORKDIR /home/app/my-app
RUN chown -R app:app /home/app/my-app
RUN sudo -u app bundle install --deployment
RUN sudo -u app RAILS_ENV=production rake assets:precompile
# Clean up APT when done.
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

Set up Capistrano

You work on your Rails app locally and you deploy to production. From your local development environment, go to your app’s root directory and run:

1
2
cd my-app
cap install

If Capistrano is installed (gem install capistrano), you will see something similar to this:

1
2
3
4
5
6
7
mkdir -p config/deploy
create config/deploy.rb
create config/deploy/staging.rb
create config/deploy/production.rb
mkdir -p lib/capistrano/tasks
create Capfile
Capified

This produces a pre-cooked config/deploy.rb file. For the app deployed in the previous post, change it to look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
lock '3.4.0'
set :application, 'my-app'
set :repo_url, 'https://github/myprofile/my-app.git'
set :branch, 'master'
set :scm, :git
set :deploy_to, "/home/app/#{fetch(:application)}"
namespace :deploy do
desc 'Install node modules'
task :npm_install do
on roles(:app) do
execute "cd #{release_path} && npm install"
end
end
desc 'Build Docker images'
task :build do
on roles(:app) do
execute "cd #{release_path} && docker build -t #{fetch(:application)}-image ."
end
end
desc 'Restart application'
task :restart do
on roles(:app) do
execute "docker stop #{fetch(:application)} ; true"
execute "docker rm #{fetch(:application)} ; true"
execute "docker run --restart=always --name #{fetch(:application)} --expose 80 -e VIRTUAL_HOST=example.com --link postgres:postgres -d #{fetch(:application)}-image"
end
end
before :updated, 'deploy:npm_install'
after :publishing, 'deploy:build'
after :publishing, 'deploy:restart'
end

Then, in config/deploy/production.rb, modify as appropriate (it’s probably sufficient to tack this on to the end of the file):

1
server "example.com", user: "app", roles: %w{app web}

Commit and push your changes to your repository.

Set up project in production

Configuration

Before deploying with Capistrano, you need to do some configuration. Assuming that the app has already been cloned to the production machine these are the files that need adjusting:

  • my-app/config/database.yml
  • my-app/config/secrets.yml

The settings in here are not typically committed to the repository for security reasons. Assuming the Postgres configuration in the previous post, database.yml should look like this:

1
2
3
4
5
6
7
production:
<<: *default
database: my-app_production
username: postgres
password: secretp@ssword
host: <%= ENV['POSTGRES_PORT_5432_TCP_ADDR'] %>
port: <%= ENV['POSTGRES_PORT_5432_TCP_PORT'] %>

secrets.yml needs to have a secret key set for production. From your app’s home directory, run:

1
rake secret

Copy the key it produces and set it in secrets.yml:

1
2
production:
secret_key_base: PasteGeneratedKeyHere

Back in the local development environment…

From your app’s home directory:

1
cap production deploy

And now back to production…

If working with the app from the previous post, everything should be ready to go. If the site reports an error, however, you may need to setup the database in production. First, stop the Docker container:

1
docker stop my-app

Then, create and seed the database:

1
2
3
docker run --rm --link postgres:postgres my-app-image rake db:create
docker run --rm --link postgres:postgres my-app-image rake db:migrate
docker run --rm --link postgres:postgres my-app-image rake db:seed

And restart:

1
docker start my-app

Deploy multiple Rails apps with Passenger, Nginx, and Docker

Here’s the problem:

I’ve got a bunch of Rails apps, but only a handful of cloud servers. I need some of them to live on a single machine without them stepping all over each other.

Assumptions

This guide assumes the server is running Ubuntu 14.04 and has all the requisite software already installed (e.g.: Docker, Rails, etc.).

Enter Docker

Docker makes the following configuration easy to maintain:

[System Topology]

Docker is also nice because all the required containers come pre-packaged:

Nginx

First, get some SSL certificates

You’ll need one for each Rails app you wish to deploy. These can be self-signed or obtained from a Certificate Authority. To self-sign a certificate, execute the following:

1
2
3
4
5
6
mkdir certs
cd certs
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout sub.example.com.key -out sub.example.com.crt
cd ..
sudo chown -R root:root certs
sudo chmod -R 600 certs

Note the keyout and out options. The jwilder/nginx-proxy Docker image won’t pick up the certificates unless they are named in accordance with the production site’s URL and subdomain (if any). For example, if you have a certificate for example.com, the keyout and out options must be named example.com.key and example.com.crt respectively.

Obtain a certificate for each app you wish to deploy (or just get one for the purposes of this tutorial).

Then, run the Nginx docker image

Note the app username. Adjust as appropriate.

1
docker run --restart=always --name nginx-proxy -d -p 80:80 -p 443:443 -v /home/app/certs:/etc/nginx/certs -v /var/run/docker.sock:/tmp/docker.sock:ro jwilder/nginx-proxy

PostgreSQL

1
docker run --restart=always --name postgres -e POSTGRES_PASSWORD=secretp@ssword -d postgres

Rails apps

Now for the tricky part…

This configuration is meant to make deployment easy. The easiest way I’ve discovered so far involves writing a Dockerfile for the Rails app and providing Nginx some configuration files.

Save this sample Dockerfile in your app’s root directory on the server (next to the Gemfile):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Adapted from https://intercityup.com/blog/deploy-rails-app-including-database-configuration-env-vars-assets-using-docker.html
FROM phusion/passenger-ruby22:latest
MAINTAINER Some Groovy Cat "hepcat@example.com"
# Set correct environment variables.
ENV HOME /root
ENV RAILS_ENV production
# Use baseimage-docker's init process.
CMD ["/sbin/my_init"]
# Start Nginx and Passenger
EXPOSE 80
RUN rm -f /etc/service/nginx/down
# Configure Nginx
RUN rm /etc/nginx/sites-enabled/default
ADD docker/my-app.conf /etc/nginx/sites-enabled/my-app.conf
ADD docker/postgres-env.conf /etc/nginx/main.d/postgres-env.conf
# Install the app
ADD . /home/app/my-app
WORKDIR /home/app/my-app
RUN chown -R app:app /home/app/my-app
RUN sudo -u app bundle install --deployment
# TODO: figure out how to install `node` modules without `sudo`
RUN sudo npm install
RUN sudo -u app RAILS_ENV=production rake assets:precompile
# Clean up APT when done.
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

Note the ADD commands under the Configure Nginx header. These are copying configurations into the Docker image. Here I put them in the docker directory to keep them organized. From your app’s root directory:

1
mkdir docker

Now, save the following to docker/my-app.conf:

1
2
3
4
5
6
7
8
9
10
11
12
13
server {
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;
server_name example.com;
root /home/app/my-app/public;
# Passenger
passenger_enabled on;
passenger_user app;
passenger_ruby /usr/bin/ruby2.2;
}

Of course, change the server name as appropriate. Also note the /home/app directory. app is the username set up by the phusion/passenger-ruby22 image.

Next, save the following to docker/postgres-env.conf

1
2
env POSTGRES_PORT_5432_TCP_ADDR;
env POSTGRES_PORT_5432_TCP_PORT;

This is some Docker magic that preserves these Postgres environment variables.

Now, build the app’s image from the project’s root directory:

1
docker build -t my-app-image .

This command reads the Dockerfile just created and executes the instructions contained therein.

Setup, migrate, and seed the database:

1
2
3
docker run --rm --link postgres:postgres my-app-image rake db:create
docker run --rm --link postgres:postgres my-app-image rake db:migrate
docker run --rm --link postgres:postgres my-app-image rake db:seed

Finally, execute the image:

1
docker run --restart=always --name my-app --expose 80 -e VIRTUAL_HOST=example.com --link postgres:postgres -d my-app-image

If everything goes well, you will be able to see your app at example.com (or wherever).

Next

Deploy a Rails app to Docker with Capistrano


React, Jest, and CustomEvent testing

Simple problem: I need my React component to communicate an event to a JQuery plugin in Rails.

Super complicated solution: I have the component emit an event so that a JQuery event listener can know to do its thing.

Easy enough. I set to work writing tests first, but Jest keeps spitting out an error:

1
ReferenceError: CustomEvent is not defined

React and Jest don’t know about the CustomEvent constructor for some reason. I need the assurance that an event will be fired every time an agent clicks the clear button on an input box.

The component looks a little like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var Tagger = React.createClass({ displayName: 'Tagger',
/**
* Clear the tagBox input
*/
clear: function(evt) {
this.refs.tagBox.getDOMNode().value = '';
// I can't get jquery-tokeninput to play nicely. This
// event signals the listner defined in queries.coffee
window.dispatchEvent(new CustomEvent('clear-tags'));
},
// ...
render: function() {
// ...
}
});
module.exports = Tagger;

In the corresponding test file, I provide my own CustomeEvent and mock the window.dispatchEvent function:

1
2
3
4
if (!window.CustomEvent) {
CustomEvent = function(name, params){ return params;};
}
window.dispatchEvent = jest.genMockFunction();

By doing all that, I can perform meaningful tests like this:

1
2
3
4
5
6
7
8
9
10
11
12
it('should emit a clear-tags event', function() {
var tagger = TestUtils.renderIntoDocument(<Tagger model="pick" />);
expect(window.dispatchEvent.mock.calls.length).toEqual(0);
// Enter some tags
tagger.refs.tagBox.getDOMNode().value = 'bill murray, comedy, movies';
// Clear
TestUtils.Simulate.click(tagger.refs.clearButton.getDOMNode());
expect(window.dispatchEvent.mock.calls.length).toEqual(1);
});

Deploying the Rails Tutorial Sample App

I recently worked through Michael Hartl’s wonderful Ruby on Rails Tutorial as a refresher. The software implemented under his direction offers functionality that basically every modern website requires (e.g., user sign up, password retrieval, etc). That which follows documents the steps I took to deploy all the best parts of that tutorial in a production environment.

Get a server

Much of this post was ripped off from this article. They recommend Digital Ocean. I like cloudatcost.com for no other reason than because they’re cheap. For the purposes of this post, it doesn’t really matter as long as it’s installed with Ubuntu 14.04.

Add a user account

The templated Rails application is executed under this account:

1
2
3
sudo adduser deploy
sudo adduser deploy sudo
su deploy

Install Ruby

Some dependencies

1
2
sudo apt-get update
sudo apt-get install git-core curl zlib1g-dev build-essential libssl-dev libreadline-dev libyaml-dev libsqlite3-dev sqlite3 libxml2-dev libxslt1-dev libcurl4-openssl-dev python-software-properties libffi-dev

rbenv

1
2
3
4
5
cd
git clone git://github.com/sstephenson/rbenv.git .rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
exec $SHELL

ruby-build plugin

1
2
3
git clone git://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
echo 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' >> ~/.bashrc
exec $SHELL

rbenv-gem-rehash plugins

1
git clone https://github.com/sstephenson/rbenv-gem-rehash.git ~/.rbenv/plugins/rbenv-gem-rehash

Ruby

1
2
3
rbenv install 2.2.1
rbenv global 2.2.1
ruby -v

bundler

1
2
echo "gem: --no-ri --no-rdoc" > ~/.gemrc
gem install bundler

The echo command prevents documentation for each gem being installed locally.

Install NodeJS

Since it is my intention to deploy this system to a production environment, I need to use the Asset Pipeline to prep my content for distribution across the web. All that requires node.

1
2
3
sudo add-apt-repository ppa:chris-lea/node.js
sudo apt-get update
sudo apt-get install nodejs

Install Rails

1
2
gem install rails -v 4.2.0
rails -v

Nginx and Passenger

Install Phusion’s PGP key to verify packages

1
2
gpg --keyserver keyserver.ubuntu.com --recv-keys 561F9B9CAC40B2F7
gpg --armor --export 561F9B9CAC40B2F7 | sudo apt-key add -

Add HTTPS support to APT

1
sudo apt-get install apt-transport-https

Add the passenger repository

1
2
3
4
sudo sh -c "echo 'deb https://oss-binaries.phusionpassenger.com/apt/passenger trusty main' >> /etc/apt/sources.list.d/passenger.list"
sudo chown root: /etc/apt/sources.list.d/passenger.list
sudo chmod 600 /etc/apt/sources.list.d/passenger.list
sudo apt-get update

nginx and passenger

1
sudo apt-get install nginx-full nginx-extras passenger

Configure

1
sudo vim /etc/nginx/nginx.conf

Uncomment the rbenv Phusion Passenger stuff. There should be some helpful hints in the file itself:

1
2
3
4
5
6
7
8
9
10
11
##
# Phusion Passenger
##
# Uncomment it if you installed ruby-passenger or ruby-passenger-enterprise
##
passenger_root /usr/lib/ruby/vendor_ruby/phusion_passenger/locations.ini;
passenger_ruby /home/deploy/.rbenv/shims/ruby; # If you use rbenv
# passenger_ruby /home/deploy/.rvm/wrappers/ruby-2.1.2/ruby; # If use use rvm, be sure to change the version number
# passenger_ruby /usr/bin/ruby; # If you use ruby from source

Get an SSL certificate

These instructions will produce a self-signed certificate:

1
2
sudo mkdir /etc/nginx/ssl
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/ssl/nginx.key -out /etc/nginx/ssl/nginx.crt

Alternatively, validate with startssl.com for free. This document provides some excellent additional information.

Add nginx host

1
2
3
sudo touch /etc/nginx/sites-available/mydomain.conf
sudo ln -s /etc/nginx/sites-available/mydomain.conf /etc/nginx/sites-enabled/mydomain.conf
sudo vim /etc/nginx/sites-available/mydomain.conf

Write the following to the file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
server {
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;
listen 443 ssl;
server_name gofish.mobi;
# SSL
ssl_certificate /etc/nginx/ssl/nginx.crt;
ssl_certificate_key /etc/nginx/ssl/nginx.key;
# Error logs
access_log /var/log/nginx/gofish.access.log;
error_log /var/log/nginx/gofish.error.log;
# Passenger
passenger_enabled on;
rails_env production;
root /home/deploy/rails-tutorial-template/current/public;
# redirect server error pages to the static page /50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# Static assets
location ^~ /assets/ {
gzip_static on;
expires max;
add_header Cache-Control public;
}
}

Start, or restart nginx:

1
sudo service nginx restart

PostgreSQL

Install:

1
sudo apt-get install postgresql postgresql-contrib libpq-dev

Create the deploy postgres user:

1
2
3
sudo su - postgres
createuser -U postgres -d -e -E -I -P -r -s deploy
exit

You’ll need to set the database password in config/application.yml.

Configure the environment

Before deploying with capistrano, a few files have to be in place. As the deploy user:

1
2
cd
mkdir -p rails-tutorial-template/shared/config

Get a secret key

If you have a rails project nearby, you can just type in

1
rake secret

Or, you can generate one by running irb

1
irb

and executing the following instructions:

1
2
3
require 'securerandom'
SecureRandom.hex(64)
exit

Copy the string generated by the SecureRandom.hex(64) command.

application.yml

This template uses figaro to manage all the sensitive stuff that sometimes goes into environment variables. The config/application.yml file it looks for isn’t committed to the repository, so you have to create it yourself:

1
2
cd rails-tutorial-template/shared/config
vim application.yml

Copy, paste, modify, and save the following:

1
2
3
4
5
6
7
8
9
10
11
12
# General
app_name: "rails_tutorial_template"
# Email
default_from: "noreply@gofish.mobi"
gmail_username: "noreply@gofish.mobi"
gmail_password: "secretnoreplypassword"
# Production
secret_key_base: "PasteTheSecretKeyFromThePreviousStepHere"
host: "gofish.mobi"
provider_database_password: "databasepassword"

I set up an account in Gmail to handle signup verifications and password resets.

database.yml and secrets.yml

There’s no sensitive information contained in the database.yml or secrets.yml files, so these can be copied directly from github.

1
2
wget https://raw.githubusercontent.com/RaphaelDeLaGhetto/rails-tutorial-template/master/config/database.yml
wget https://raw.githubusercontent.com/RaphaelDeLaGhetto/rails-tutorial-template/master/config/secrets.yml

Clone the template

This is meant to be completed on the development machine (not the server). It is assumed that postgresql and all the other dependencies are already installed (if not, do so as above).

1
2
3
4
5
6
7
git clone https://github.com/RaphaelDeLaGhetto/rails-tutorial-template.git
cd rails-tutorial-template
bundle install
sudo npm install
rake db:setup
rake db:seed
vim config/application.yml

Then copy, paste, and save the following in the file:

1
default_from: 'noreply@example.com'

Tests should all pass

1
rake

capistrano deployment

I’m still working on making this easier. From the project’s directory on the development machine set the following in config/deploy/production.rb

1
2
# Replace 127.0.0.1 with your server's IP address!
server 'gofish.mobi', user: 'deploy', roles: %w{web app}

Then run

1
bundle exec cap production deploy --trace

The deployment should succeed, but the site will not be accessible until the database is set up. Log in to the production server as deploy:

1
2
3
ssh deploy@gofish.mobi
cd rails-tutorial-template/current
RAILS_ENV=production rake db:setup

Now, enable the deploy user to restart passenger without providing a sudo password:

1
sudo visudo

Add this to the end of the file and save:

1
deploy ALL=(root) NOPASSWD: /usr/bin/passenger-config

Back on the local machine, the deployment should now succeed:

1
bundle exec cap production deploy --trace

If everything worked out right, then the app should be accessible at the configured domain name (gofish.mobi in my case).