November 9, 2016
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: "[email protected]" 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: "[email protected]" 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: "[email protected]", password: "somestrongpassword") visit signup_path fill_in "Email", with: "[email protected]" 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: "[email protected]", password: "somestrongpassword")} it "requires a unique email to signup" do valid_user invalid_user = User.new(email: "[email protected]", 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 %>
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.