TDD is Darned Difficult

I have been writing software for several decades; long before The Internet and these ‘new-fangled’ languages and frameworks. It is natural for me to approach a project with some of the code already in mind. If not code, then definitely pseudo-code. So, the idea of writing tests that describe what the code should do always ‘felt’ like additional unnecessary work.

The title is inspired by my affection for RAS Syndrome

After reading, for years, about Test Driven Development and Test-First Development that all the “cool kids” seem to have mastered I dedicated myself to understand what all the fuss is about.

First, I need to overcome the additional burden of learning a Testing Language. Within the Rails ecosystem we have an advantage because the testing tools are written in Ruby also. Although that still is not enough to make TDD a breeze it sure helps.

Writing software is easy. Writing tests is really difficult; for some reason

One useful approach I use when trying to absorb and assimilate new knowledge is repetition. The cycle of Build-Destroy-Repeat allows me to internalize a new concept. Since ‘Basic User Authentication’ is something that appears in more than 90% of all web applications, I chose this for my exercise with TDD in Rails. The result is this one file (below).

Yes, I have broken some rules about Rails TDD. And, there is no doubt a more experienced Rails TDD developer could make improvements on what I have built here.
But I expect a little grace considering the purpose is to get my feet wet and gain some confidence with the process. I hope, additionally, someone else approaching TDD as an initiate will find this a useful springboard.

Copy the code below into a file in the spec/features/ folder, named user_encrypted_authentication_spec.rb

require 'rails_helper'

# Using TDD to 'drive' the construction of a rudimentary user authentication system
### === Instructions ==
#  1. Create a new rails application `rails new <app_name> -d  postgresql -T -B`
#  2. Setup the 'database.yml' file with the database connection and credentials
#  3. Install RSpec:
#    $ echo 'gem "rspec-rails", :group => [:development, :test]' >> Gemfile
#    $ echo ‘gem “capybara”, :group => [:test] >> Gemfile
#    $ bundle install
#    $ rails generate rspec:install
#    $ rspec spec --format documentation


describe "User" do  
    context "is a Rails CRUD (new, create, edit, destroy) 'resource'" do
    it "**HINT: 'rails g resource users new edit'**" do
        visit new_user_path
        expect(page).to be
        visit edit_user_path(1) 
        expect(page).to be
    end
    end #context ====================================
    context "model validates a password" do
    it "**HINT:  a password field (as an accessor)**" do
        @user = User.new(password: "a password")
        expect(@user.password).to be
    end

    it "**HINT: contains a 'validates_confirmation_of' password attribute**" do
        @user = User.new(password: "a password", password_confirmation: "password")
        expect(@user.password_confirmation).to be
    end

    end #context ===================================
    context "model encrypts password using BCrypt" do
    it "**HINT: (before_save) self.password_salt = BCrypt::Engine.generate_salt / self.password_hash = BCrypt::Engine.hash_secret(password, password_salt) || 'password_hash' and 'password_salt' must be persisted attributes" do
        user = User.create(email: "aUser@company.com", password: "aPassword4N0w")
        expect(user.password_hash).to be
        expect(user.password_salt).to be
    end
    end #context ==================================

    it "**HINT: uses a 'before_save' hook to encrypt_password**" do
        user = User.create(email: "aUser@company.com", password: "aPassword4N0w")
        expect(user.password_hash).to be
        expect(user.password_salt).to be
    end

    context "model authenticates the user" do
    it "**HINT: use a static method to find the user and test that the 'password_hash' matches || user = User.where(email: email).first / if user && user.password_hash == BCrypt::Engine.hash_secret(password, user.password_salt)**" do
        User.create(email: "auser@company.com", password: "1userpassworD", password_confirmation: "1userpassworD")
        current_user = User.authenticate("auser@company.com", "1userpassworD")
        expect(current_user).not_to be_nil
    end

    it "model authenticates the user (without regard for the case of the login)" do
        User.create(email: "downcaseuser@yippee.com", password: "1password4u", password_confirmation: "1password4u")
        current_user = User.authenticate("DowncaseUser@yippee.com", "1password4u")
        expect(current_user).not_to be_nil
    end

    end #context ==========================================
    context "model disallows a duplicate entry" do
    it "**HINT: This can be accomplished with 'validate_uniqueness_of' in the model or ':unique => true' in the database migration **" do
        user_one = User.create(email: "oneuser@burp.com", password: "p@55", password_confirmation: "p@55")
        user_one.save
        user_two = User.create(email: "oneuser@burp.com", password: "w0rd", password_confirmation: "w0rd")
        expect(user_two.save).not_to be
    end
    end #context ==========================================
end

describe "User Controller" do  
    context "responds to 'new'" do
    it "**HINT: UsersController needs a 'new' action and there must be an associated view**" do
        visit new_user_path
        expect(page).to be
    end
end #context =========================================  
    context "view presents a registration form to create new user" do
    it "**HINT: new.html.erb contains a form_for User.new with the necessary fields.  This will call the UsersController 'new' action**" do
        visit new_user_path
        expect(page).to have_field('user_password')
        expect(page).to have_field('user_password_confirmation')
        expect(page).to have_button('Register')
    end
end #context =========================================  
    context "properly creates a new user" do
    it "**HINT: process the params || params.require(:user).permit(:email, :password, :password_confirmation)**" do
    #nop
    end
    it "**HINT: contains a 'create' that a) converts the email to lowercase, b) creates a 'new' @user with the params, c) saves the user - if true 'flash[:notice]' welcome message and redirect_to :root otherwise 'flash[:alert]' a 'try again' message and redirect_to :back **" do
    visit new_user_path
    fill_in 'user_email', :with => 'auser@company.com'
    fill_in 'user_password', :with => 'password123'
    fill_in 'user_password_confirmation', :with => 'password123'
    click_button("Register")
    expect(page).to have_text("Welcome")
    expect(page).to_not have_text("Error")
    end

    it "**HINT: process the params || params.require(:user).permit(:email, :password, :password_confirmation)**" do
    #nop
    end
end #context ========================================  
end

describe "Session Controller" do  
    context "provides a 'log-in' route" do
    it "**HINT: get '/log-in' => 'sessions#new' & post '/log-in' => 'sessions#create' **" do
        visit '/log-in'
    end
    it "**HINT: 'rails g controller sessions new create destroy' **" do
        visit '/log-in'
        expect(page).to be
    end
end #context ========================================  
    context "presents a login form for 'sessions/new'" do
    it "**HINT: in views/sessions/new provide a 'form_tag log_in_path' to capture 'password' with a 'submit_tag \"Log in\"' **" do
        visit log_in_path
        expect(page).to be
        expect(page).to have_field('password')
        expect(page).to have_button('Log in')
    end
end #context ========================================

    context "responds to 'log-out' (destroy)" do
    it "**HINT: requires a route get '/log-out' => 'sessions#destroy', as: :logout **" do
        visit '/log-out'
        expect(page).to be
    end
end #context ========================================

    context "processes a Log in operation" do
    it "**HINT: in create action, call @user = User.authenticate(params[:email], params[:password]) and then test @user for nil.  If valid a) flash[:notice] = 'You are logged in' / session[:user_id] = @user.id / redirect_to root otherwise flash[:alert] = 'There was a problem' / redirect_to log_in_path **" do
        @user = User.create(email: "auser@company.com", password: "password5", password_confirmation: "password5")
        visit '/log-in'
        fill_in 'email', :with => 'aUser@Company.com'
        fill_in 'password', :with => 'password5'
        click_button 'Log in'
        expect(page).to have_text('logged in')
    end
end #context ========================================  
    context "should permit a Log Out operation" do
    it "**HINT: destory action: session[:user_id] = nil / flash[:notice] = 'You have been logged out successfully!' / redirect_to root **" do
            @user = User.create(email: "aUser@company.com", password: "password5", password_confirmation: "password5")
            visit '/log-in'
            fill_in 'email', :with => 'aUser@company.com'
            fill_in 'password', :with => 'password5'
            click_button 'Log in'
            visit '/log-out'
            expect(page).to have_text('logged out')
#            expect(current_user).to be_nil
    end
end #context ========================================  
end

=begin
def current_user  
@current_user ||= User.find(session[:user_id]) if session[:user_id]
end

helper_method :current_user

=end

The activity of developing this complete spec helped me to realize (understand) an important principle of Test-First Development; that is how the test will drive the creation of the application in a systematic manner. And as a guide (or tutorial) it is my direct intention for this spec to do that. It should guide you directly from one step to the next logical step.

I anticipate this is only the first in a series of such exercises for me. As my skill and experience with TDD grows, I expect to refine this edification process.