>

November 9, 2016

Don't waste your time, test outside in.

Testing is a super important tool in any developers bag of tricks, and doing it effectively can save you from a world of pain as you work on your application. However, there are so many ways to approach testing your application and tools to do it with. So when you're getting started, it's very easy to get lost in the noise, where should you start? What's a good first step?

A really good place for you to start is with an approach called outside in testing. Outside in testing is the approach of starting your TDD/BDD with an integration test for your application, one that will define what your application needs to perform for your user, a feature for them, for example, a user signup and login feature for your application.

A good first step for this would be writing a test, that will fail, for a user being able to sign up to your application, a scenario where they signup successfully, and also a scenario that covers your most likely failure, some kind of User data validation failure.

I would use RSpec and capybara for this, so in a request spec:

it "allows a user to signup to the site" do
  visit signup_path
  fill_in "Email", with: "chris@chrisherring.co"
  fill_in "Password", with: "somestrongpassword"
  click_button "Signup"
  expect(page).to have_content("Thanks for signing up!")
end

We now need to write the code that will make this pass, a controller, a view and a model will all play a part.

class CreateUsers < ActiveRecord::Migration
  def change
    create_table "users" do |t|
      t.string "email", unique: true
      t.string "crypted_password"

      t.timestamps
    end
  end
end

in app/models/user.rb:

class User < ActiveRecord::Base
  has_secure_password
end

we need some routes as well, to show our pages:

Rails.application.routes.draw do

  get '/signup', to: "users#new"
  post 'signup', to: "users#create"

  resource "user", to: "user#show"
end

and in app/controllers/users_controller.rb

class UsersController < ApplicationController

  def new
    @user = User.new
  end

  def create
    @user = User.new(permitted_params)
    @user.save
    redirect_to user_path, notice: "Thanks for signing up!"
  end

  def show
  end

end

and some simple views, app/views/users/new.html.erb:

<%= form_for @user, url: signup_path, method: :post do |f| %>
  <p>
    <%= f.label :email %>
    <%= f.text_field :email %>  
  </p>
  <p>
    <%= f.label :password %>
    <%= f.password_field :password %>
  </p>
  <p>
    <%= f.submit "Signup" %>  
  </p>
<% end %>

also app/views/users/show.html.erb:

<%= flash[:notice] %>

Now you have the most very basic code that will make that test pass, a user is created, and we thank them for signing up. How about also a test for a very simple failure of the signup process, an email has already been used.

We'll need to add to the feature test:

it "allows a user to signup to the site" do
  visit signup_path
  fill_in "Email", with: "chris@chrisherring.co"
  fill_in "Password", with: "somestrongpassword"
  click_button "Signup"
  expect(page).to have_content("Thanks for signing up!")
end

it "fails signup for a user who tries to use an email address that is already in user" do
  User.create(email: "chris@chrisherring.co", password: "somestrongpassword")

  visit signup_path
  fill_in "Email", with: "chris@chrisherring.co"
  fill_in "Password", with: "somestrongpassword"
  click_button "Signup"
  expect(page).to have_content("Sorry, that email has been taken.")
end

We already have most of the code that we need for this, but we have to add a validation to the User model that will handle for this since we are testing outside in, features then units, it's time to write a unit test, for our User model

require 'rails_helper'

RSpec.describe User, type: :model do

  let(:valid_user) { User.create(email: "chris@chrisherring.co", password: "somestrongpassword")}

  it "requires a unique email to signup" do
    valid_user
    invalid_user = User.new(email: "chris@chrisherring.co", password: "somestrongpassword")
    expect(invalid_user).to_not be_valid
  end
end

So, now if we run our tests, we will see that if fails, as expected, we haven't written any code to enforce this validation on our users' emails. So let's do that now in app/models/user.rb

class User < ActiveRecord::Base
  has_secure_password
  validates :email, uniqueness: true
end

and also alter our controller:

class UsersController < ApplicationController

  def new
    @user = User.new
  end

  def create
    @user = User.new(permitted_params)
    if @user.save
      redirect_to user_path, notice: "Thanks for signing up!"
    else
      flash[error] = "Sorry, that email has already been taken."
      render :new
    end
  end

  def show
  end

end

and our view:

<% if flash[:error] %>
  <div class='error'><%= flash[:error] %></div>
<% end %>

<%= form_for @user, url: signup_path, method: :post do |f| %>
  <p>
    <%= f.label :email %>
    <%= f.text_field :email %>  
  </p>
  <p>
    <%= f.label :password %>
    <%= f.password_field :password %>
  </p>
  <p>
    <%= f.submit "Signup" %>  
  </p>
<% end %>
But Why??

This is a very simple case, but it shows how we code and test features that have been fleshed out using outside in testing. By using this outside in approach to testing, we can be sure that we are only writing code that we need to run our application and deliver value to our users. Start with the feature test first, define with it what you are trying to achieve, and you can confident that you are not writing any code that you don't need, code that will need to be maintained. Make all your code effective and useful and don't waste time writing code because "I'll probably need this". Let your tests drive what you need. Test outside in.

What do you think of this approach to writing your code and tests, or are you interested in hearing how I'd approach a more complex scenario? If you want to know, send me an email, I'd love to hear your thoughts, or even help you out if you need it.