Test Driven Rails - Part 2
Last time, in part 1, I was giving some advice about testing - why to test at all, which tests are valuable and which are not, when to write acceptance tests and in what cases aim for the maximum code coverage. It brought about some serious discussion about testing ideas and if you haven't read it yet, you should probably check (it) it out. Giving some general point of view about such broad topic like Test Driven Development / Behavior Driven Development is definetely not enough so I will try to apply these techniques by implementing a concrete feature. I wanted to choose some popular usecase so that most developers will have an opinion how they would approach it. In most applications you will probably need:
User Registration
It is quite common feature and it can be approached in many ways. The most popular is to some authentication gem, like Devise, which is the probably the safest and the fastest way. However, Devise might be an overkill for some cases or maybe you need highly customizable solution. How would you write an implementation fot that usecase then?
Note: the implementation below doesn't aim to be the most secure approach for that feature, it's rather for demonstration purposes. I made some non-standard design decisions for the Rails application, you may want to read one of my previous posts to get more details why this way of designing code might be beneficial.
Specification
We know that we want to implent user registration. Let's say that we want user to confirm his/her account before signing in so we will need to send some confirmation instructions. Also, let's add some admin notifications about new user being registered to make it more interesting.
To make it even better, let's assume that we will create both User
and UserProfile
during registration: User
will have just an email
and encrypted_password
attributes, UserProfile
will have country
and age
attributes. User will also have to accept some policy to register. If we want to have confirmation, we will also need some attributes for confirmation_token
, confirmation date (confirmed_at
) and let's add confirmation_instructions_sent_at
just to know, when the instructions were sent. These are just registration-specific attributes and we won't need them in most cases so let's extract them to UserRegistrationProfile
Note: when writing the implementation and the tests, the following gems were used: rails (4.0.3)
, database_cleaner (1.2.0)
, simple_form (3.0.1)
with country_select (1.3.1)
, reform (0.2.4)
, bcrypt (3.1.6)
, rspec-rails (3.0.0.beta1)
, factory_girl_rails (4.3.0)
and capybara (2.2.1)
.
Start with acceptance tests
When writing new feature we should start from acceptance tests - we will make sure that the feature works from the higher level: from the user perspective and some side effects like sending emails. So the good start will be covering user creation and sending emails to an admin and to the user. Let's write some Capybara tests:
# spec/features/user_registration_spec.rb
require 'spec_helper'
feature "User Registration" do
context "when visiting new user path" do
background do
visit new_user_path
end
context "registering with valid data" do
given(:email) { "myawesome@email.com" }
background do
fill_form_with_valid_data(email: email)
end
scenario "new user is created" do
expect do
register
end.to change(User, :count).by(1)
end
context "notifications" do
background do
register
end
scenario "confirmation email is sent to the user" do
expect(all_email_addresses).to include email
end
scenario "notification is sent to the admin" do
expect(all_email_addresses).to include "admin@example.com"
end
scenario "2 emails are sent" do
expect(all_emails.count).to eq 2
end
end
end
end
end
def fill_form_with_valid_data(args={})
email = args.fetch(:email, "email@example.com")
fill_in "email", with: email
fill_in "user_password", with: "my-super-secret-password"
fill_in "user_password_confirmation", with: "my-super-secret-password"
fill_in "age", with: 22
select "Poland", from: "country"
check "policy"
end
def register
click_button "Register"
end
I like using some helper methods, especially in acceptance tests so I wrote fill_form_with_valid_data
and register
helpers - these are just some details and I don't need to know them when reading tests. There are also some helpers like all_email_addresses
and all_emails
, which come from the MailerMacros
:
# spec/support/mailer_macros.rb
module MailerMacros
def last_email
ActionMailer::Base.deliveries.last
end
def last_email_address
last_email.to.join
end
def reset_email
ActionMailer::Base.deliveries = []
end
def reset_with_delayed_job_deliveries
ActionMailer::Base.deliveries = []
end
def all_emails
ActionMailer::Base.deliveries
end
def all_emails_sent_count
ActionMailer::Base.deliveries.count
end
def all_email_addresses
all_emails.map(&:to).flatten
end
end
If you like it, just create spec/support/mailer_macros.rb
, put the code there and in your spec_helper.rb
insert the following lines:
# spec/spec_helper.rb
config.include(MailerMacros)
config.before(:each) { reset_email }
Also, the select with country might be not clear - the collection with countries comes from country_select
gem.
We have some failing acceptance tests, it will take some time to make them all green. Now we can write some migrations:
rails generate model User email encrypted_password
rails generate model UserProfile user_id:integer age:integer country
We also need to add some database constraints, to ensure that users' emails are unique and fields are not null, the migrations would look like this:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :email, null: false
t.string :encrypted_password, null: false
t.timestamps
end
add_index :users, :email, unique: true
end
end
class CreateUserProfiles < ActiveRecord::Migration
def change
create_table :user_profiles do |t|
t.integer :age
t.string :country, null: false
t.integer :user_id, null: false
t.timestamps
end
add_index :user_profiles, :user_id
end
end
Now we have to define some routes:
# config/routes.rb
root to: "static_pages#home"
resources :users do
end
For user registration, we have REST actions: new
and create
. Let's also add some root page, currently just to get rid of default Rails page:
# app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController
def home
end
end
But how to deal with UsersController
and form for user registration? We have some fields that are not present in models (password/password_confirmation
and policy
). The popular solution would be using: accepts_nested_attributes_for :profile
and some virtual attributes. I don't really like this solution, accepts_nested_attributes_for
sometimes can really save a lot of time, especially with complex nested forms with nested_form gem. But virtual attributes are quite ugly and they make models the interfaces for forms. Much better approach is to use form objects. There's a great gem for this kind of problems: Reform
- we will use it here.
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def new
@registration_form = registration_form
end
def create
end
private
def registration_form
UserRegistrationForm.new(user: User.new, profile: UserProfile.new)
end
end
That's it for UsersController
, we will need some views and the actual form object:
# app/views/users/new.html.haml
%h1 User Registration
= simple_form_for @registration_form do |f|
= f.input :email, label: "email"
= f.input :country, label: "country", as: :country
= f.input :age, label: "age"
= f.input :password, label: "password"
= f.input :password_confirmation, label: "password confirmation"
= f.input :policy, label: "I accept the policy", as: :boolean
= f.submit "Register"
And the actual UserRegistrationForm
:
# app/forms/user_registration_form.rb
class UserRegistrationForm < Reform::Form
include Reform::Form::ActiveRecord
include Composition
property :email, on: :user
property :password, on: :nil, empty: true
property :password_confirmation, on: :nil, empty: true
property :age, on: :profile
property :country, on: :profile
property :policy, on: :nil, empty: true
validates :email, presence: true, email: true, uniqueness: { case_sensitive: false }
validates :password, presence: true, confirmation: true
validates :age, presence: true
validates :country, presence: true
validates :policy, acceptance: true, presence: true
model :user
end
Reform is not (yet) that popular in the Rails community so some things require explanation (check also the docs out). The Reform::Form::ActiveRecord
module is for uniqueness validation and the Composition
is for... composition - some properties are mapped to user and other to profile. There is also a mystical mapping with on: :nil
- these are "virtual" properties like password
, password_confirmation
and policy
- all properties must be mapped to a resource so just to satisfy Reform API I use on: :nil
as a convention, also the empty: true
option is for virtual attributes that won't be processed. And where does the email validation come from? From our custom validator, let's write some specs but before we should add /forms (and /usecases for business logic) directories to be autoloaded:
# config/application.rb
config.autoload_paths += %W(#{config.root}/app/usecases)
config.autoload_paths += %W(#{config.root}/app/forms)
# spec/usecases/email_validator_spec.rb
require 'spec_helper'
class DummyModel
include ActiveModel::Validations
attr_accessor :email
validates :email, email: true
end
describe EmailValidator do
let(:model) { DummyModel.new }
it "validates email format" do
valid_emails = %w[email@example.com name.surname@email.com
e-mail@example.com]
valid_emails.each do |email|
model.email = email
expect(model).to be_valid
end
invalid_emails = %w[email @email.com email.example.com
email@example email@example.]
invalid_emails.each do |email|
model.email = email
expect(model).not_to be_valid
end
end
end
You can probably come up with some more examples to cover email validation but these are sufficient cases. I've introduced DummyModel
here to have a generic object that can be validated so the ActiveModel::Validations
module is needed and an accessor for an email. Let's implement the actual validation:
# usecases/email_validator.rb
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
record.errors[attribute] << (options[:message] || "is not a valid email format")
end
end
end
The regexp for email validation comes from Rails guides:). It won't cover all the possibilities but the actual format of the email is an overkill.
I don't fell the need to write tests for other validations and composition for UserRegistrationForm
: it's just using very descriptive DSL, the validation are already tested in Rails.
We haven't set up the associations yet in models:
# app/models/user.rb
class User < ActiveRecord::Base
has_one :profile, class_name: "UserProfile", inverse_of: :user
validates :email, presence: true, uniqueness: { case_insensitive: false }, email: true
validates :encrypted_password, presence: true
# app/models/user_profile.rb
class UserProfile < ActiveRecord::Base
belongs_to :user, inverse_of: :profile
validates :user, :country, :age, presence: true
end
I added also validations in models. These may seem like a duplication because form object already implements them but these are validations always applicable do these models so it is a good idea to have them in models.
Let's concentrate on UsersController
and create
action. I don't really like testing controllers, especially for CRUD-like stuff, user creation still feels like CRUD but not that typical in Rails, especially when using dedicated form object. So let's test drive registration process: we are going to use UserRegistrationForm
for data aggregation and validation - if the data is valid, the user will be created by UserRegistration
service object with redirection to root path, otherwise it will render new
template.
# spec/controllers/users_controller_spec.rb
require 'spec_helper'
describe UsersController do
describe "#create" do
let(:registration_form) { instance_double(UserRegistrationForm) }
let(:user_params) { { "email" => "email@example.com" } }
let(:params) { { user: user_params } }
let(:user_registration) { instance_double(UserRegistration) }
before(:each) do
allow(UserRegistrationForm).to receive(:new) { registration_form }
allow(registration_form).to receive(:assign_attributes)
.with(user_params) { registration_form }
allow(user_registration).to receive(:register!)
.with(registration_form) { true }
allow(UserRegistration).to receive(:new) { user_registration }
end
context "valid data" do
before(:each) do
expect(registration_form).to receive(:valid?) { true }
post :create, params
end
it "executes registration" do
expect(user_registration).to have_received(:register!).with(registration_form)
end
it "redirects to root path" do
expect(response).to redirect_to root_path
end
end
context "invalid data" do
before(:each) do
expect(registration_form).to receive(:valid?) { false }
post :create, params
end
it "renders registration form" do
expect(response).to render_template :new
end
end
end
end
Well, it is not really clear, that's the problem with testing controllers and they should be as thin as possible. We need to implement the assign_attributes
method in form object to fill models' attributes with params and implement the actual UserRegistration
usecase. In tests I use instance_double
instead of simple double
to make sure I'm not stubbing non-existent methods or with wrong number of arguments - that's a great feature introduced in RSpec 3, which comes from rspec-fire gem. Also, I'm stubbing responses so that I can spy on them using have_received
method - it's much cleaner and easier to read. Compare these two examples:
before(:each) do
expect(registration_form).to receive(:valid?) { true }
post :create, params
end
it "executes registration" do
expect(user_registration).to have_received(:register!).with(registration_form)
end
it "redirects to root path" do
expect(response).to redirect_to root_path
end
and
before(:each) do
expect(registration_form).to receive(:valid?) { true }
end
it "executes registration" do
expect(user_registration).to receive(:register!).with(registration_form)
post :create, params
end
it "redirects to root path" do
post :create, params
expect(response).to redirect_to root_path
end
I really encourage you to spy on a stubbed method, I will make your tests much more readable and DRY them up.
I made also some non-standard design decisions here: why not to implement the persistence logic in the form object and use it like:
if @registration_form.persist(user_params) # populate data, perform validation and persist data if is valid
# happy paths
else
# failure path
end
For simple persistence logic I would probably go with that approach but we will also need to send some confirmation instructions, admin notifications etc., I'm not really comfortable with the idea of form object knowing something about sending notifications, persistence alone would be ok, it would be quite convenient to use but this is too complex, I would leave form object for data aggregation and validation. Let's write code for the controller:
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
@registration_form = registration_form.assign_attributes(params[:user])
if @registration_form.valid?
UserRegistration.new.register!(@registration_form)
redirect_to root_path, notice: "You have register. Please, check your email for confimartion instructions"
else
render :new
end
end
private
def registration_form
UserRegistrationForm.new(user: User.new, profile: UserProfile.new)
end
end
We need to implement assign_attributes
method (we have nice failure message thanks to instance_double
that informs us about it):
Failure/Error: allow(registration_form).to receive(:assign_attributes)
UserRegistrationForm does not implement:
assign_attributes
and UserRegistration. Let's start from test for assign_attributes
method. It looks like, besides assigning params, it should return itself:
# spec/forms/user_registration_form_spec.rb
require 'spec_helper'
describe UserRegistrationForm do
let(:user) { User.new }
let(:profile) { UserProfile.new }
subject { UserRegistrationForm.new(user: user, profile: profile) }
describe "#assign_attributes" do
it "populates models' attributes with params" do
subject.assign_attributes("email" => "email@example.com", "country" => "Poland")
expect(subject.user.email).to eq "email@example.com"
expect(subject.profile.country).to eq "Poland"
end
it "assigns profile to user" do
subject.assign_attributes({})
expect(user.profile).to eq profile
end
it "returns self" do
expect(subject.assign_attributes({})).to eq subject
end
end
end
And the code for implementation:
# app/forms/user_registration_form.rb
def assign_attributes(params)
from_hash(params)
save_to_models
self
end
It uses some Reform::Form private methods that I found in source code so this implementation might not be stable but fortunately we have it covered in tests so we will know breaking changes if it happens in next versions. And there's a gotcha here: The keys in hash must be stringified, symbols won't work (applies to 0.2.4 version of Reform).
Let's write some minimal implementation for UserRegistration
to satisfy controller's specs:
# app/usecases/user_registration.rb
class UserRegistration
def register!(aggregate)
end
end
And what the UserRegistration
should be responsible for? Let's start with persistence logic: user with it's profile must be created and the encrypted password should be assigned to the user. We will also need registration profile to be created.
# spec/usecases/user_registration_spec.rb
require 'spec_helper'
describe UserRegistration do
let(:user) { FactoryGirl.build_stubbed(:user) }
let(:profile) { FactoryGirl.build_stubbed(:user_profile) }
let(:form) { double(:form, user: user, profile: profile,
password: "password") }
subject { UserRegistration.new(encryption: encryption) }
let(:encrypted_password) { "encrypted_password" }
let(:encryption) { instance_double(Encryption,
generate_password: encrypted_password) }
context "persistence is success" do
before(:each) do
allow(user).to receive(:save!) { true }
allow(profile).to receive(:save!) { true }
allow(user).to receive(:create_registration_profile!) { true }
end
before(:each) do
subject.register!(form)
end
specify "user gets encrypted password" do
expect(user.encrypted_password).to eq encrypted_password
end
it "saves user" do
expect(user).to have_received(:save!)
end
it "creates profile for user" do
expect(profile).to have_received(:save!)
end
it "create registration profile for user" do
expect(user).to have_received(:create_registration_profile!)
end
end
context "persistence fails" do
it "raises RegistrationFailed error" do
allow(user).to receive(:save!) { raise_error ActiveRecord::RecordInvalid }
expect do
UserRegistration.new.register!(form)
end.to raise_error UserRegistration::RegistrationFailed
end
end
end
Note: keep in mind that you should write one test and then write minimal implementation to make it pass and then another test. I gave the several tests and the actual UserRegistration
in advance, just to make it easier to read and follow.
It is quite clear from the tests what should be expected from this class: creation of user, profile, registration profile and assigning encrypted password. Data aggregate (form
) is just a double
with profile and user, we don't care what it actually is, it should just implement the stubbed interface. I also use FactoryGirl and build_stubbed
method for initializing models - I find it more convenient than to use instance_double
because instance doubles don't cover attributes from database tables.
The factories for User and profiles would look like that:
# spec/factories.rb
FactoryGirl.define do
factory :user do
email "email@example.com"
# I'll explain that later, why it is that long
encrypted_password "$2a$10$bcMccS3q2egnNICPLYkptOoEyiUpbBI5Q.GAKe0or2QB7ij6yCeOa"
end
factory :user_profile do
age 22
country "Poland"
end
end
And the actual implementation:
# app/usecases/user_registration.rb
class UserRegistration
class RegistrationFailed < StandardError ; end
attr_reader :encryption
private :encryption
def initialize(options={})
@encryption = options.fetch(:encryption, Encryption.new)
end
def register!(aggregate)
user = aggregate.user
profile = aggregate.profile
user.encrypted_password = encrypted_password(aggregate.password)
ActiveRecord::Base.transaction do
begin
user.save!
profile.save!
user.create_registration_profile!
rescue ::ActiveRecord::StatementInvalid, ::ActiveRecord::RecordInvalid => e
raise_registration_error(e)
end
end
end
private
def raise_registration_error(errors)
message = "Registration Failed due to the following errors: #{errors}"
raise UserRegistration::RegistrationFailed, message
end
def encrypted_password(password)
encryption.generate_password(password)
end
end
Let's discuss some design decisions: the constructor accepts options hash so that we can inject dependencies like encryption
and to provide defaults if it's injected. The persistence logic is wrapped in transaction block so that e.g. user won't be created if profile creation fails. If it fails, RegistrationFailed
error is raised with a descriptive message. Also, the encryption
is private: we don't need it to be public.
To satisfy tests, the create_registration_profile!
must be implemented and generate_password
for encryption. Fortunately, we just need to setup associations for UserRegistrationProfile
to have create_registration_profile!
implemented. But we need to generate the model first:
rails generate model UserRegistrationProfile confirmed_at:datetime confirmation_instructions_sent_at:datetime confirmation_token user_id:integer
let's set up some database constraints in generated migration:
class CreateUserRegistrationProfiles < ActiveRecord::Migration
def change
create_table :user_registration_profiles do |t|
t.datetime :confirmed_at
t.datetime :confirmation_instructions_sent_at
t.string :confirmation_token
t.integer :user_id, null: false
t.timestamps
end
add_index :user_registration_profiles, :user_id
add_index :user_registration_profiles, :confirmation_token, unique: true
end
end
and then write the associations:
# app/models/user.rb
class User < ActiveRecord::Base
has_one :registration_profile, class_name: "UserRegistrationProfile", inverse_of: :user
end
# app/models/user_registration_profile.rb
class UserRegistrationProfile < ActiveRecord::Base
belongs_to :user, inverse_of: :registration_profile
validates :user, presence: true
end
The minimal implementation for Encryption
to make the UserRegistration
tests happy is the following:
# app/usecases/encryption.rb
class Encryption
def generate_password(phrase)
end
end
To finish the user creation we have to implement the password generation. Bcrypt and it's create
password method is a reasonable choice here. Let's write the tests:
# spec/usecases/encryption_spec.rb
require 'spec_helper'
describe Encryption do
subject { Encryption.new }
let(:password) { "password" }
let(:encypted_password) { "$2a$10$vI8aWBnW3fID.ZQ4/zo1G.q1lRps.9cGLcZEiGDMVr5yUP1KUOYTa" }
let(:password_generator) { class_double(BCrypt::Password).as_stubbed_const }
before(:each) do
allow(password_generator).to receive(:create).with(password) { encypted_password }
end
it "creates password using Bcrypt as default" do
expect(subject.generate_password(password)).to eq encypted_password
end
end
The encrypted_password
doesn't have to be that long but looks more genuine that way. The BCrypt::Password is also a class double so that we make sure we don't stub a non-existent method. And the implementation of Encryption
class:
# app/usecases/encryption.rb
class Encryption
attr_reader :password_generator
private :password_generator
def initialize(args={})
@password_generator = args.fetch(:password_generator, BCrypt::Password)
end
def generate_password(phrase)
password_generator.create(phrase)
end
end
The pattern for constructor is similar to the one from UserRegistration
. The password_generator
is also made private - the rule of thumb is that everything should be private unless it needs to be public, just to keep the interfaces clean.
Now we have the basic implementation for user creation with it's profiles. Still, we need confirmation stuff and notification to tje admin. It is beyond the UserRegistration
responsibilities, we also don't need always to a notification or confirmation instructions or to confirm user at all, just to have the interface flexible enough. Maybe we will have some additional things that will take place during registration - like third party API notification. To keep the responsibilities separate and UserRegistration
easy to use, we can implement all the additional actions as the listeners that are being passed to the constructor of UserRegistration
. Let's write specs for it first:
# spec/usecases/user_registration_spec.rb
require 'spec_helper'
describe UserRegistration do
# same code a before
context "persistence is success" do
# same code a before
# and this is new:
context "with listeners" do
let(:user_confirmation) { double(:user_confirmation,
notify: true) }
let(:admin_notification) { double(:admin_notification,
notify: true) }
before(:each) do
UserRegistration.new(user_confirmation, admin_notification,
encryption: encryption).register!(form)
end
it "notifies user_confirmation listener" do
expect(user_confirmation).to have_received(:notify).with(user)
end
it "notifies admin_notificaiton listener" do
expect(admin_notification).to have_received(:notify).with(user)
end
end
end
end
We don't actually care what the listeners are, the only requirement is that they must implement the same interface: notify
method which takes user
argument. And the implementation:
# app/usecases/user_registration.rb
class UserRegistration
class RegistrationFailed < StandardError ; end
attr_reader :encryption, :listeners
private :encryption, :listeners
def initialize(*listeners, **options)
@listeners = listeners
@encryption = options.fetch(:encryption, Encryption.new)
end
def register!(aggregate)
user = aggregate.user
profile = aggregate.profile
user.encrypted_password = encrypted_password(aggregate.password)
ActiveRecord::Base.transaction do
begin
user.save!
profile.save!
user.create_registration_profile!
rescue ::ActiveRecord::StatementInvalid, ::ActiveRecord::RecordInvalid => e
raise_registration_error(e)
end
end
notify_listeners(user)
end
private
def raise_registration_error(errors)
message = "Registration Failed due to the following errors: #{errors}"
raise UserRegistration::RegistrationFailed, message
end
def encrypted_password(password)
encryption.generate_password(password)
end
def notify_listeners(user)
listeners.each do |listener|
listener.notify(user)
end
end
end
These changes are not that noticeable but they are huge. The constructor now takes some listeners (splat) - we can pass one listener, several or none, it will always be an array. Also, the options is now a keyword argument introduced in Ruby 2.0 which makes the changes really smooth. And the new method: notify_listeners
which sends notify
message to all the listeners with user
argument.
To handle the user confirmation stuff we will need, well, UserConfirmation
and UserRegistrationAdminNotification
to handle the notifcations.
Let's start with UserConfirmation
. We need notify
method which will take care of: assigning confirmation token, which must be unique, setting date when the confirmation instructions were sent and sending the instructions. We will need some mailer here (UserConfirmationMailer
), clock (DateTime
) and something to generate token - SecureRandom
will be a good fit here with it's base64
method. Let's translate the specification to the tests:
# spec/factories.rb
FactoryGirl.define do
# same as before
factory :user_registration_profile do # this in new here
end
end
# spec/usecases/user_confirmation_spec.rb
require 'spec_helper'
describe UserConfirmation do
describe "#notify" do
let!(:user) { FactoryGirl.build_stubbed(:user,
registration_profile: FactoryGirl.build_stubbed(:user_registration_profile)) }
let(:mailer_stub) { double(:mailer, deliver: true) }
let!(:mailer) { class_double(UserConfirmationMailer,
send_confirmation_instructions: mailer_stub).as_stubbed_const }
let(:confirmation_instructions_sent_date) { DateTime.new(2014, 2, 23, 21, 0, 0)}
let(:clock) { double(:clock, now: confirmation_instructions_sent_date) }
subject { UserConfirmation.new(mailer: mailer, clock: clock) }
before(:each) do
allow(user).to receive(:save_with_profiles!)
allow(SecureRandom).to receive(:base64) { "token" }
subject.notify(user)
end
it "assigns confirmation token to user" do
expect(user.confirmation_token).to eq "token"
end
it "sends email with confirmation instructions" do
expect(mailer).to have_received(:send_confirmation_instructions).with(user)
end
it "sets date when the confirmation instructions have been sent" do
expect(user.confirmation_instructions_sent_at).to eq confirmation_instructions_sent_date
end
it "persists new data" do
expect(user).to have_received(:save_with_profiles!)
end
end
end
Like before, we should start with one test, make it pass and then write the next one. Here is the implementation for it:
# app/usecases/user_confirmation.rb
class UserConfirmation
attr_reader :mailer, :clock
private :mailer, :clock
def initialize(args={})
@mailer = args.fetch(:mailer, UserConfirmationMailer)
@clock = args.fetch(:clock, DateTime)
end
def notify(user)
assign_confirmation_token(user)
user.confirmation_instructions_sent_at = clock.now
mailer.send_confirmation_instructions(user).deliver
user.save_with_profiles!
end
private
def assign_confirmation_token(user)
begin
user.confirmation_token = SecureRandom.base64(20)
end while User.find_by_confirmation_token(user.confirmation_token).present?
end
end
The pattern for constructor is similar to the previous ones: provide the way to inject dependencies and some defaults if they are not specified so it is more flexible, less coupled and the testing becomes easier as a bonus. We have while loop to ensure the confirmation token is unique amongst users. The find_by_attribute
methods are deprecated since Rails 4.0.0 and the activerecord-deprecated_finders
will be removed from dependencies in 4.1.0 so we have to implement our own finder method. Here are also some important design decisions - we assign both confirmation_instructions_sent_at
and confirmation_token
to the user, not the registration profile. How is that? The important question is: do we need to expose that the user has registration profile? What if we change our mind and decide to put this data in "normal" profile, not registration profile? Or we didn't make a decision to create a registration profile at all in a first place and these attributes belonged to the user since the beginning and we later decided to move them to a separated table? From the UserConfirmation
perspective, it is just an implementation detail. The save_with_profiles!
is provided to make user's data persistence more convenient. We need to implement mailer as well but let's start with user's related stuff.
# spec/models/user.rb
require 'spec_helper'
describe User do
subject { User.new(email: "email@example.com",
encrypted_password: "password") }
describe ".find_by_confirmation_token" do
let!(:user) { FactoryGirl.create(:user) }
before(:each) do
FactoryGirl.create(:user_registration_profile,
confirmation_token: "token", user_id: user.id)
end
it "finds user with specified confirmation token" do
expect(User.find_by_confirmation_token("token")).to eq user
end
end
describe "#confirmation_token=" do
it "assigns confirmation token to user" do
subject.confirmation_token = "token"
expect(subject.confirmation_token).to eq "token"
end
end
describe "#confirmation_instructions_sent_at=" do
it "assigns confirmation instructions sent date to user" do
date = DateTime.now
subject.confirmation_instructions_sent_at = date
expect(subject.confirmation_instructions_sent_at).to eq date
end
end
end
The find_by_confirmation_token
finder method is pretty easy but it involves another table with registration profile so I decided to write test for it. The tests also suggest that we need readers for these attributes, not only the writers. Let's use delegate
macro from ActiveSupport for it:
# app/models/user.rb
class User < ActiveRecord::Base
# the same code as before
# new code
delegate :confirmation_token, :confirmation_instructions_sent_at, :confirmed_at,
to: :registration_profile, allow_nil: true
def self.find_by_confirmation_token(token)
joins(:registration_profile)
.where("user_registration_profiles.confirmation_token = ?", token)
.first
end
def confirmation_token=(token)
ensure_registration_profile_exists
registration_profile.confirmation_token = token
end
def confirmation_instructions_sent_at=(date)
ensure_registration_profile_exists
registration_profile.confirmation_instructions_sent_at = date
end
def save_with_profiles!
User.transaction do
save!
profile.save! if profile
registration_profile.save! if registration_profile
end
end
private
def ensure_registration_profile_exists
build_registration_profile if registration_profile.blank?
end
Before making any assignment, we have to make sure that the registration profile exists. The same applies to the persistence, which is again wrapped in a transaction. And let's implement the mailer for sending confirmation instructions:
rails generate mailer UserConfirmationMailer send_confirmation_instructions
Some basic tests to prove that the mailer actually works:
# spec/mailers/user_confirmation_mailer_spec.rb
require "spec_helper"
describe UserConfirmationMailer do
describe "#send_confirmation_instructions" do
let!(:user) { FactoryGirl.build_stubbed(:user, email: "email@example.com",
registration_profile: FactoryGirl.build_stubbed(:user_registration_profile,
confirmation_token: "token"))
}
let(:mail) { UserConfirmationMailer.send_confirmation_instructions(user) }
it "has proper subject" do
expect(mail.subject).to eq("Confirmation Instructions")
end
it "sends email to the user" do
expect(mail.to).to eq([user.email])
end
it "has link to confirm account" do
url = "/confirmations/#{user.confirmation_token}"
expect(mail.body.encoded).to match(url)
end
end
end
The setup with FactoryGirl may seem to be complex but I like doing this kind of setup manually, not to rely on predefined attributes for the factory so that I know where the data comes from. We also assume that there will be some controller action for confirmations so we will need to define routes to make the tests pass:
#app/mailers/user_confirmation_mailer.rb
class UserConfirmationMailer < ActionMailer::Base
default from: "contact@email.com"
def send_confirmation_instructions(user)
@user = user
mail(to: user.email, subject: "Confirmation Instructions")
end
end
# app/views/user_confirmation_mailer/send_confirmation_instructions.html.erb
<p>To complete the registration process, click the link below:</p>
<%= link_to "Confirm", user_confirmation_path(token: @user.confirmation_token) %>
And the route with a controller action:
# config/routes.rb
get '/confirmations/:token', to: "confirmations#confirm", as: :user_confirmation
# app/controllers/confirmations_controller.rb
class ConfirmationsController < ApplicationController
def confirm
end
end
And add the listener in UsersController
:
# app/controllers/users_controller.rb
def create
@registration_form = registration_form.assign_attributes(params[:user])
if @registration_form.valid?
UserRegistration.new(
UserConfirmation.new
).register!(@registration_form)
redirect_to root_path, notice: "You have register. Please, check your email for confimartion instructions"
else
render :new
end
end
private
def registration_form
UserRegistrationForm.new(user: User.new, profile: UserProfile.new)
end
To make all the tests happy, we need some to send a notification to the admin. It looks like UserRegistrationAdminNotification
will be just an adapter layer for NewUserAdminNotificationMailer
to provide the listener interface. The tests and the implementation are quite simple:
# spec/usecases/user_registration_admin_notification_spec.rb
require 'spec_helper'
describe UserRegistrationAdminNotification do
describe "#notify" do
let!(:user) { FactoryGirl.build_stubbed(:user) }
let(:mailer_stub) { double(:mailer, deliver: true) }
let!(:mailer) { class_double(NewUserAdminNotificationMailer,
notify: mailer_stub).as_stubbed_const }
subject { UserRegistrationAdminNotification.new(mailer: mailer) }
before(:each) do
subject.notify(user)
end
it "sends email to admin about new user being registered" do
expect(mailer).to have_received(:notify).with(user)
end
end
end
The implementation:
# app/usecases/user_registration_admin_notification.rb
class UserRegistrationAdminNotification
attr_reader :mailer
private :mailer
def initialize(args={})
@mailer = args.fetch(:mailer, NewUserAdminNotificationMailer)
end
def notify(user)
mailer.notify(user).deliver
end
end
We also need to generate the mailer with notify
method (yes, the same as for the listener but it is good enough here):
rails generate mailer NewUserAdminNotificationMailer notify
and the simple implementation to make the tests green:
# app/mailers/new_user_admin_notification_mailer.rb
class NewUserAdminNotificationMailer < ActionMailer::Base
default from: "contact@email.com"
def notify(user)
@user = user
mail(to: "admin@example.com", subject: "New User Registration")
end
end
Now the tests for the mailer and we are almost finished with the registration:
# spec/mailers/new_user_admin_notification_mailer_spec.rb
require "spec_helper"
describe NewUserAdminNotificationMailer do
describe "#notify" do
let!(:user) { FactoryGirl.build_stubbed(:user, email: "email@example.com") }
let(:mail) { NewUserAdminNotificationMailer.notify(user) }
it "sends email to the admin" do
expect(mail.to).to eq(["admin@example.com"])
end
it "has link to confimartion in the body" do
expect(mail.body.encoded).to match(user.email)
end
end
end
The views for the mailer:
# app/views/new_user_admin_notification_mailer/notify.html.erb
<p>New user with email: <%= @user.email %> has registered</p>
And the listener for UserRegistrationAdminNotification
in UserRegistrationAdminNotification.new
:
# app/controllers/users_controller.rb
def create
@registration_form = registration_form.assign_attributes(params[:user])
if @registration_form.valid?
UserRegistration.new(
UserConfirmation.new,
UserRegistrationAdminNotification.new
).register!(@registration_form)
redirect_to root_path, notice: "You have register. Please, check your email for confimartion instructions"
else
render :new
end
end
private
def registration_form
UserRegistrationForm.new(user: User.new, profile: UserProfile.new)
end
Hell yeah, all tests are happy now, we have completed the user registration feature. Let's add account confirmation feature and simple sign in. We don't need acceptance test or integration test in controller for that feature, it's pretty simple and unit test for controller would be enough. We probably need to find user by confirmation token, confirm the account and redirect to some page. Also, we should return 404 error if there's no match for confirmation token. In a real world application it would probably need some expiration date for token and other features but keep in a mind it's just for demonstration purposes, not writing the complete devise-like solution.
# spec/controllers/confirmations_controller_spec.rb
require 'spec_helper'
describe ConfirmationsController do
describe "#confirm" do
let!(:user) { FactoryGirl.build_stubbed(:user,
registration_profile: FactoryGirl.build_stubbed(:user_registration_profile,
confirmation_token: "token")) }
let(:factory) { class_double(User).as_stubbed_const }
before(:each) do
allow(factory).to receive(:find_by_confirmation_token!)
.with(user.confirmation_token) { user }
end
it "it confirms user and redirects to root path" do
expect(user).to receive(:confirm!) { true }
get :confirm, token: user.confirmation_token
expect(response).to redirect_to root_path
end
end
end
We find the user with bang method so, by convention, it raises ActiveRecord::RecordNotFound if the resource is not found - we won't write test for the failure path. Then the confirm!
method is used which needs to be implemented and redirect to root path. The implementation for the controller is the following:
# app/controllers/confirmations_controller.rb
class ConfirmationsController < ApplicationController
def confirm
user = User.find_by_confirmation_token!(params[:token])
user.confirm!
redirect_to root_path, notice: "You have confirmed you account. Now you can login."
end
end
Now we need to implement confirm!
and find_by_confirmation_token!
methods:
# spec/models/user_spec.rb
require 'spec_helper'
describe User do
subject { User.new(email: "email@example.com",
encrypted_password: "password") }
# some old code
describe "#confirm!" do
let(:date) { DateTime.new(2014, 02, 23, 22, 6, 0) }
before(:each) do
allow(DateTime).to receive(:now) { date }
subject.confirm!
end
it "assigns confirmation date with current date" do
expect(subject.confirmed_at).to eq date
end
it "persists user and the profile" do
expect(subject.registration_profile.persisted?).to eq true
expect(subject.persisted?).to eq true
end
end
end
It would be quite convenient to use confirm!
method for new user and make it persisted. I don't feel a need to write test for find_by_confirmation_token!
as it is really simple and will use find_by_confirmation_token
.
# app/models/user.rb
class User < ActiveRecord::Base
def self.find_by_confirmation_token!(token)
user = find_by_confirmation_token(token)
if user.blank?
raise ActiveRecord::RecordNotFound
else
user
end
end
def confirm!
ensure_profile_exists
registration_profile.confirmed_at = DateTime.now
save_with_profiles!
end
private
def ensure_registration_profile_exists
build_registration_profile if registration_profile.blank?
end
end
It's another method in User
model, shouldn't the models be thin? Well, it's not business logic involving some complex actions, these are just domain methods for user to handle it's own state (or the profile's state which is rather an implementation detail in this case), it looks like a model's responsibility so it's a right place to add this kind of logic, much better than using e.g. update
method on registration profile outside the models.
We completed another feature: user can confirm his/her account. There's only one feature left: sign in. Let's test drive it starting from acceptance test again: when the user exists and is confirmed, we let the user sign in, if exists but is not confirmed yet we render proper info and if the email/password combination is invalid we also want to display proper info. Capybara test for these specs may look like this:
# spec/features/sign_in_spec.rb
require 'spec_helper'
feature "Sign In" do
context "user is confirmed" do
given!(:user) { FactoryGirl.create(:user,
registration_profile: FactoryGirl.build(:user_registration_profile,
confirmed_at: DateTime.now)) }
describe "with valid data" do
background do
visit sign_in_path
fill_in_sign_in_form_with_valid_data(user)
sign_in
end
scenario "user signs in and and shows success message" do
expect(page).to have_content "You have successfully signed in"
end
scenario "after sign in user sees it's email" do
expect(page).to have_content user.email
end
end
describe "with invalid data" do
scenario "signin is prohibited and user sees error message
with wrong email/password combination" do
visit sign_in_path
fill_in_sign_in_form_with_invalid_data(user)
sign_in
expect(page).to have_content "Wrong email/password combination"
end
end
end
context "user is not confirmed" do
given!(:user) { FactoryGirl.create(:user,
registration_profile: FactoryGirl.build(:user_registration_profile)) }
background do
visit sign_in_path
fill_in_sign_in_form_with_valid_data(user)
sign_in
end
scenario "signin is prohibited and user sees info about unconfirmed account" do
expect(page).to have_content "You must confirm your account"
end
end
end
def fill_in_sign_in_form_with_valid_data(user)
fill_in "email", with: user.email
fill_in "password", with: "password"
end
def fill_in_sign_in_form_with_invalid_data(user)
fill_in "email", with: user.email
fill_in "password", with: "wrong_password"
end
def sign_in
click_button "Sign in"
end
The structure is similar to the one from registration process: there are some helper methods for filling forms and signing in. For the happy path, we also want to verify that the user is actually signed in so we will display it's email. This test will work because in User
factory in spec/factories.rb
the encrypted_password
value is an encrypted form of "password" phrase. Let's start from defining routes and creating controller for the user signin:
# config/routes.rb
resources :sessions, except: [:new]
get '/sign_in', to: "sessions#new", as: :sign_in
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
end
end
and the view layer:
# app/views/sessions/new.html.haml
= display_messages
%h1 Sign In
= simple_form_for :sign_in, url: :sessions, method: :post do |f|
= f.input :email, label: "email"
= f.input :password, label: "password"
= f.submit "Sign in"
Where does the display_messages
helper comes from? It's a simple helper for displaying flash messages which can be implemented as follows:
# app/helpers/application_helper.rb
module ApplicationHelper
def display_messages
case
when flash[:notice]
display_flash_message(flash[:notice], "alert-success")
when flash[:error]
display_flash_message(flash[:error], "alert-error")
when flash[:alert]
display_flash_message(flash[:alert], "alert-error")
end
end
def display_flash_message(message, class_name)
content_tag(:div, class: "alert centerize-text #{class_name}") do
message
end
end
end
Let's also update the root page:
# app/views/static_pages/home.html.haml
= display_messages
- if current_user
%h1 Welcome
= current_user.email
Let's stick to the convention and name the helper method with current user the current_user
. The signin process is already covered by acceptance test so we won't benefit much from writing controller's test. To keep track of current user, we will store it's id in a session. The implementation might be following:
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def create
user = User.find_by(email: sign_in_params[:email])
return wrong_combination_of_email_or_password if user.blank?
return user_not_confirmed if !user.confirmed?
if Authentication.authenticate(user.encrypted_password, sign_in_params[:password])
sign_in(user)
redirect_to root_path, notice: "You have successfully signed in"
else
wrong_combination_of_email_or_password
end
end
private
def sign_in_params
params.require(:sign_in).permit(:email, :password)
end
def sign_in(user)
session[:user_id] = user.id
end
def wrong_combination_of_email_or_password
flash.now[:error] = "Wrong email/password combination"
render :new
end
def user_not_confirmed
flash.now[:error] = "You must confirm your account"
render :new
end
end
And for the current_user
:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
helper_method :current_user
private
def current_user
@current_user ||= User.find(session[:user_id]) if session[:user_id].present?
end
end
The last step is to implement Authentication
module with authenticate method, which compares encrypted password with plain password. Let's start with a test:
# spec/usecases/authentication_spec.rb
require 'spec_helper'
describe Authentication do
describe ".authenticate" do
let(:encrypted_password) { "$2a$10$bcMccS3q2egnNICPLYkptOoEyiUpbBI5Q.GAKe0or2QB7ij6yCeOa" }
it "returns true if encrypted password is specified password" do
expect(Authentication.authenticate(encrypted_password,
"password")).to eq true
end
it "returns false if encrypted password in not specified password" do
expect(Authentication.authenticate(encrypted_password,
"wrong_password")).to eq false
end
end
end
I don't need to create an instance of Authentication
, there is no need to make it a class. And to make all the tests green we just need to implement comparison of passwords using Bcrypt:
# app/usecases/authentication.rb
module Authentication
def self.authenticate(encrypted_password, password)
BCrypt::Password.new(encrypted_password) == password
end
end
Wrapping up
That's all! All the tests now pass. And they run pretty fast (on Ruby 2.1.0), about 1.6 s. That was quite long: the user registration, confirmation and sign in features have been test drived and some not obvious design decisions were made. That gives some basic ideas how I apply Test Driven Development / Behavior Driven Development techniques in everyday Rails programming. The aim of these tests wasn't to have 100% coverage (e.g. I didn't test ActiveModel validations, using Reform DSL to make UserRegistrationForm
composition, delegations in User
model) but they give me sufficient level of confidence to assume that the application works correctly and they helped with some design choices, which is a great advantage of unit tests. When TDDing, keep in mind what Kent Beck says about his way of writing tests:
I get paid for code that works, not for tests so my philosophy is to test as little as possible to reach a given level of confidence (I suspect this level of confidence is high compared to industry standards but that could just be hubris). If I don't typically make a kind of mistake (like setting the wrong variables in a constructor), I don't test for it.