Install and Configure Rails
Intro
To document is mostly for me – at least until I automate my setup defaults. However, I am glad to share and get ideas from others too. I will build a little calendar app I use with friends (it’s focused on being mobile friendly and easy to use – not a full featured calendar).
Rails Setup
Taken from:
- https://gist.github.com/alxndr/7569551
- https://www.codewithjason.com/rails-integration-tests-rspec-capybara/
- https://hackernoon.com/how-to-build-awesome-integration-tests-with-capybara-j9333y68
create the project:
# -T - skips tests; I like rspec
# -d postgresql; I like postgresql best for the db
# --skip-spring --skip-listen; Spring caches and doesn't notice all changes (even after rails restart)
# I have lost several hours not realizing Spring wasn't seeing my changes
rails new calendar -T -d postgresql --webpack=stimulus --skip-turbolinks --skip-spring
cd calendar
# in some cases you may have serveral bundlers or need to create binstubs
# gem install bundler:2.1.4
# rails app:update:bin
update the README and initialize Git
git add .
git commit -m "initial commit"
git remote add origin [email protected]:btihen/calendar.git
git push -u origin master
Add extra Gems for this project
add rspec, devise, factory_bot and stimulus_reflex
Execute the following command (or add to the Gemfile)
cat <<EOF >> Gemfile
# Project Gems
##############
# FRONT END
###########
gem "hotwire-rails" # probably not needed as of Rails 7.x
# gem "stimulus_reflex", "~> 3.3" # probably superseeded by hotwire-rails
# BACK END
##########
gem 'devise'
# DEV / TESTS
#############
group :development, :test do
gem 'awesome_print' # formats pry (& irb outputs into readable formats)
gem 'pry-rails'
gem 'pry-byebug' # Adds byebug's step debugging and stack navigation
# gem 'pry-debugger' # adds step, continue, etc (alternative to pry-byebug)
gem 'pry-stack_explorer' # easy stack traces when debugging
# more pry gems if needed at: https://spin.atomicobject.com/2012/08/06/live-and-let-pry/
gem 'factory_bot_rails'
gem 'faker'
# gem 'rspec-rails'
gem 'capybara'
gem 'rspec-rails', '~> 4.0.0'
# lets spring work with rspec
gem 'spring-commands-rspec'
end
group :test do
# easier tests (inside rspec)
gem 'shoulda-matchers'
# cucumber can test emails (rspec too?)
# gem 'email_spec'
# code coverage
gem 'simplecov'
gem 'simplecov-console'
end
EOF
Now uncomment a few Gems in the Original Gemfile
Uncomment the following to ensure ActionText and Stimulus Refelx (work properly).
gem 'image_processing', '~> 1.2'
is needed by Active Storage (ActionText needs Active Storage)
and
gem 'redis', '~> 4.0'
is needed by Stimulus Reflex (which uses Action Channels) to manage WebSockets
Install and configure base gems
now run:
bundle install
to install all the new gems and create a Gemfile.lock
Install ActiveStorage and ActionText
run the following commands:
# bundle exec rails webpacker:install
# bundle exec rails webpacker:install:stimulus
bundle exec rails active_storage:install
bundle exec rails action_text:install
bin/rails hotwire:install
bin/rails g devise:install
bin/rails g rspec:install
Rspec: Config Files
Create needed folders for our config
mkdir spec/features
# a place to put test helper code
mkdir spec/support
mkdir spec/support/features
Rspec Config file spec/rails_helper.rb
- To enable integration tests with rspec add:
require 'capybara/rspec'
belowrequire 'rspec/rails'
- To load Test helper code add:
Dir[Rails.root.join("spec/support/**/*.rb")].each { |file| require file }
belowrequire 'capybara/rspec'
- just after the ActiveRecord config and before RSpec.configure block add:
Capybara.register_driver :selenium_chrome do |app|
Capybara::Selenium::Driver.new(app, browser: :chrome)
end
Capybara.javascript_driver = :selenium_chrome
- Add the FactoryBot config in the section with:
RSpec.configure do |config|
# ...
# support for Factory Bot
config.include FactoryBot::Syntax::Methods
# setup devise login helpers in Rspec
config.include Devise::Test::IntegrationHelpers, type: :request
# allows us for force session logouts (im feature tests)
config.include Warden::Test::Helpers
end
- finally at the end of the file add support for shoulda matchers with:
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :rails
end
end
NOW spec/rails_helper.rb
should look like (its long, sometimes the full context is clearer):
# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../config/environment', __dir__)
# Prevent database truncation if the environment is production
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point!
# enables integration/feature tests using rspec
require 'capybara/rspec'
# loads custom helper test code
Dir[Rails.root.join("spec/support/**/*.rb")].each { |file| require file }
# or you could use:
# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
# Checks for pending migrations and applies them before tests are run.
# If you are not using ActiveRecord, you can remove these lines.
begin
ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
puts e.to_s.strip
exit 1
end
# configure capybara integration tests
Capybara.register_driver :selenium_chrome do |app|
Capybara::Selenium::Driver.new(app, browser: :chrome)
end
Capybara.javascript_driver = :selenium_chrome
RSpec.configure do |config|
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
config.fixture_path = "#{::Rails.root}/spec/fixtures"
# If you're not using ActiveRecord, or you'd prefer not to run each of your
# examples within a transaction, remove the following line or assign false
# instead of true.
config.use_transactional_fixtures = true
# You can uncomment this line to turn off ActiveRecord support entirely.
# config.use_active_record = false
# RSpec Rails can automatically mix in different behaviours to your tests
# based on their file location, for example enabling you to call `get` and
# `post` in specs under `spec/controllers`.
#
# You can disable this behaviour by removing the line below, and instead
# explicitly tag your specs with their type, e.g.:
#
# RSpec.describe UsersController, type: :controller do
# # ...
# end
#
# The different available types are documented in the features, such as in
# https://relishapp.com/rspec/rspec-rails/docs
config.infer_spec_type_from_file_location!
# Filter lines from Rails gems in backtraces.
config.filter_rails_from_backtrace!
# arbitrary gems may also be filtered via:
# config.filter_gems_from_backtrace("gem name")
# support for Factory Bot
config.include FactoryBot::Syntax::Methods
# setup devise login helpers in Rspec (login helpers)
config.include Devise::Test::IntegrationHelpers, type: :request
# allows us for force session logouts (im feature tests)
config.include Warden::Test::Helpers
end
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :rails
end
end
Create / Test a landing page
A simple config test before we setup devise (authentication).
- Generate a page – I don’t (generally) use helpers nor contoller or view specs - so I’ll create the landing page using the following generator:
rails g controller Landing index --no-helper --no-assets --no-controller-specs --no-view-specs
- Update Routes
config/routes.rb
with:
get 'landing/index'
root to: "landing#index"
- Add Hidden Test Content to simplify testing add:
<p hidden id='landing_index'>Landing Index</p>
- Request test:
# spec/requests/landing_request_spec.rb
require 'rails_helper'
RSpec.describe "Landings", type: :request do
describe "GET /index" do
it "returns http success" do
get "/landing/index"
expect(response).to have_http_status(:success)
expect(response.body).to include("<p hidden id='landing_index'>Landing Index</p>")
end
end
end
- Feature Test (to be sure they are working too)
# spec/features/landing_page_spec.rb
require 'rails_helper'
RSpec.describe 'Landing Page Works without a login', type: :feature do
scenario 'Visit landing Page' do
visit root_path
page_tag = find('p#landing_index', text: 'Landing Index', visible: false)
expect(page_tag).to be_truthy
end
end
Test and commit
rake db:migrate
bundle exec rspec
git add .
git commit -m "rspec: unit and feature tests configured and landing page works"
git push
Config Hotwire
Ensure the In the end the app/views/layouts/application.html.erb
looks like:
<!DOCTYPE html>
<html>
<head>
<title>Tweets</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<!-- Bootstrap 4 if interested
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">
-->
<%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_pack_tag 'application' %>
<%= yield :head %>
<%= turbo_include_tags %>
<%# stimulus_include_tags %>
</head>
<body>
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
<%= yield %>
</body>
</html>
Devise / User Config
Configure dev email for devise:
# config/environments/development.rb:
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
Create the user and migration
rails g devise user
# if you will make a custom login (probably needed to look nice)
# rails g devise:views
Adjust the migration:
class DeviseCreateUsers < ActiveRecord::Migration[6.1]
def change
create_table :users do |t|
## Database authenticatable
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
## Recoverable
t.string :reset_password_token
t.datetime :reset_password_sent_at
## Rememberable
t.datetime :remember_created_at
## Trackable
t.integer :sign_in_count, default: 0, null: false
t.datetime :current_sign_in_at
t.datetime :last_sign_in_at
t.string :current_sign_in_ip
t.string :last_sign_in_ip
## Confirmable
# t.string :confirmation_token
# t.datetime :confirmed_at
# t.datetime :confirmation_sent_at
# t.string :unconfirmed_email # Only if using reconfirmable
## Lockable
# t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
# t.string :unlock_token # Only if unlock strategy is :email or :both
# t.datetime :locked_at
t.timestamps null: false
end
add_index :users, :email, unique: true
add_index :users, :reset_password_token, unique: true
# add_index :users, :confirmation_token, unique: true
# add_index :users, :unlock_token, unique: true
end
end
Route file should now look like:
Rails.application.routes.draw do
devise_for :users
get 'landing/index'
root to: "landing#index"
end
We will update the user model with password complexity validation:
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
validate :password_complexity
def password_complexity
# Regexp extracted from https://stackoverflow.com/questions/19605150/regex-for-password-must-contain-at-least-eight-characters-at-least-one-number-a
return if password.blank? || password =~ /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{10,70}$/
errors.add :password, 'Complexity requirement not met. Length should be 10-70 characters and include: 1 uppercase, 1 lowercase, 1 digit and 1 special character'
end
end
Create the user Factory (which also uses Faker):
# spec/factories/users.rb
FactoryBot.define do
factory :user do
email { Faker::Internet.safe_email } # probably need to add index for uniqueness
password { Faker::Internet.password(min_length: 10, max_length: 50, mix_case: true, special_characters: true) }
end
trait :invalid do
email { Faker::Internet.username }
password { "hoi" }
end
end
Create the user spec (uses FactoryBot & Shoulda):
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
describe "Factory with" do
context "default parameters" do
it "creates a valid model" do
user = FactoryBot.build :user
expect(user.valid?).to be_truthy
end
end
context "invalid parameters" do
it "fails model validation" do
user = FactoryBot.build :user, :invalid
expect(user.valid?).to be_falsey
end
end
end
context "ActiveRecord / DB Tests" do
it { should have_db_column(:email) }
it { should have_db_index(:email).unique }
end
context "ActiveModel / Validations" do
it "detects a bad email" do
user = FactoryBot.build :user, email: "bill"
expect(user.valid?).to be_falsey
expect(user.errors.messages[:email]).to match_array ["is invalid"]
end
it "detects a non-compliant password" do
user = FactoryBot.build :user, password: "hoi"
expect(user.valid?).to be_falsey
expect(user.errors.messages[:password]).to match_array ["is too short (minimum is 6 characters)",
"Complexity requirement not met. Length should be 10-70 characters and include: 1 uppercase, 1 lowercase, 1 digit and 1 special character"]
end
end
end
Test setup and commit when green:
rake db:migrate
bundle exec rspec
git add .
git commit -m "devise configured, FactoryBot, Faker and Shoulda working"
git push
create user landing / profile page (autoredirect)
Test restricted logins
a basic login feature test might look like:
require 'rails_helper'
RSpec.describe 'Users Login', type: :feature do
let(:user) { FactoryBot.create :user }
after :each do
Warden.test_reset!
end
describe 'user logs in successfully' do
scenario 'and is redirected to user home page' do
user_log_in(user)
expect(current_path).to eql(auth_user_root_path)
end
end
end
Install Fonts / Icons
Fontawesome (Good with Bulma)
https://kelishrestha.medium.com/how-to-install-font-awesome-with-yarn-in-rails-6-0-c2506543c13d
yarn add @fortawesome/fontawesome-free
update application.scss
$fa-font-path: '@fortawesome/fontawesome-free/webfonts';
@import '@fortawesome/fontawesome-free/scss/fontawesome';
@import '@fortawesome/fontawesome-free/scss/solid';
@import '@fortawesome/fontawesome-free/scss/regular';
@import '@fortawesome/fontawesome-free/scss/brands';
@import '@fortawesome/fontawesome-free/scss/v4-shims';
update application.js
import "@fortawesome/fontawesome-free/js/all";
or via cdn: vhttps://fontawesome.com/how-to-use/customizing-wordpress/snippets/setup-cdn-webfont
add to application.html.erb
()
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet"
href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css"
integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p"
crossorigin="anonymous"/>
Install BULMA: a CSS Framework (if desired)
Bulma is a relatively new CSS framework. It feels like a light, streamlined alternative to Bootstrap. Bulma doesn’t include any JavaScript at all. This means some stuff just won’t work out of the box. For example, the burger menu won’t toggle without a little JavaScript help. We’ll get to that later.
yarn add bulma
Open app/javascript/packs/application.js and add the following to the top:
import '../styles'
Create app/javascript/styles.scss:
@import '~bulma/bulma';
customize bulma by adding to the top of styles.scss
file:
https://stackoverflow.com/questions/48809328/bulma-navbar-breakpoint
@import "~bulma/sass/utilities/initial-variables.sass";
$navbar-breakpoint: $tablet;
@import "~bulma/bulma.sass";
@import '~bulma/bulma';
choices are: $desktop (default 960px), $tablet (769px), $widescreen (1152px), $fullhd (1344px) variable defaults: https://bulma.io/documentation/customize/variables/ variables that can be set: https://bulma-customizer.bstash.io
A sample Bulma navbar
Open app/views/layouts/application.html.erb and add the following just above the yield line:
<%= render 'layouts/navbar' %>
Create app/views/layouts/_navbar.html.erb:
<div class="container">
<nav class="navbar">
<div class="navbar-brand">
<a class="navbar-item" href="#">
<img src="https://bulma.io/images/bulma-logo.png" width="112" height="28">
</a>
<div class="navbar-burger burger" data-target="main-nav">
<span></span>
<span></span>
<span></span>
</div>
</div>
<div id="main-nav" class="navbar-menu">
<div class="navbar-start">
<%= link_to root_url, class: 'navbar-item' do %>
<span class="icon">
<i class="far fa-gem"></i>
</span>
<span>Home</span>
<% end %>
<%= link_to home_about_url, class: 'navbar-item' do %>
<span class="icon">
<i class="far fa-star"></i>
</span>
<span>About</span>
<% end %>
</div>
</div>
</nav>
</div>
This is basically copied from the Bulma examples. It is a basic nav bar with two menu items; Home and About.
We now have all the pieces in place and can start wiring up our Stimulus controllers.
Create a Stimulus controller To keep this example simple, we’re going to create a single controller which we’ll attach to the body tag in the main layout. This controller will be responsible for rendering the Font Awesome icons (as described in a previous post) as well as handling our Bulma burger menu.
Create app/javascript/controllers/main_controller.js:
import fontawesome from '@fortawesome/fontawesome'
import icons from '@fortawesome/fontawesome-free-regular'
import { Controller } from 'stimulus'
export default class extends Controller {
initialize() {
fontawesome.library.add(icons)
}
connect() {
fontawesome.dom.i2svg()
// Get all "navbar-burger" elements
var $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Check if there are any navbar burgers
if ($navbarBurgers.length > 0) {
// Add a click event on each of them
$navbarBurgers.forEach(function ($el) {
$el.addEventListener('click', function () {
// Get the target from the "data-target" attribute
var target = $el.dataset.target;
var $target = document.getElementById(target);
// Toggle the class on both the "navbar-burger" and the "navbar-menu"
$el.classList.toggle('is-active');
$target.classList.toggle('is-active');
});
});
}
}
}
This controller imports the icons from Font Awesome when initialize is called. Every time connect is called it renders the icons and then searches for navbar burgers to attach the appropriate click events on.
Connect the controller Now we want to connect the body tag to our controller using an HTML5 data attribute.
Open app/views/layouts/application.html.erb
and add the following attribute to the <body>
tag.
<body data-controller="main">
Now it should look like:
# app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>Tweets</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">
<%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_pack_tag 'application' %>
<%= yield :head %>
<%= turbo_include_tags %>
<%# stimulus_include_tags %>
</head>
<body data-controller="main">
<%= render 'layouts/navbar' %>
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
<%= yield %>
</body>
</html>
Discourgaged - no longer necessary:
If you plan to user database_cleaner – then also see this article to finish your config:
https://medium.com/@amliving/my-rails-rspec-set-up-6451269847f9