Day 42.5 – TDD with Gems

Let’s talk about gems! What is a gem? Well, a gem is basically just a nicely packaged collection of code that can be easily plugged right into a Rails project to gain some functionality. Gems are great! Let’s look at some examples…

One of the first gems I started using is called “Hirb.” Hirb is a lightweight miniature framework for creating table views in the terminal. It’s probably easier to show what it does than describe it, so imagine you want to query a list of users in the Rails console. You can run a command like users = User.all and you get something like this:

Screen Shot 2017-05-28 at 12.47.19 AM.png

Not exactly the most readable thing ever. So, let’s go to our gemfile and add the line gem 'hirb'. Bundle install, then enter the Rails console again and run Hirb.enable. Now try the same command as before and this pops out instead:

Screen Shot 2017-05-28 at 12.36.06 AM

Much better, right? Hirb has some other cool features for making the console prettier and easier to navigate, but I pretty much just use it for the table view.

So that’s an example of a super simple gem. In the case of Hirb, all we had to do next was enable it in the console and it did its thing. The next few gems we’ll take a look at are a bit more involved. Let’s start by assuming the project we’ll be working on will need a User model and some basic login and registration features we want to test. We want our User to have a secure password, so we’re going to use an encryption gem, BCrypt, to help us out with that. BCrypt is actually already in our gemfile as a comment, so all we have to do is uncomment it: gem 'bcrypt', '~> 3.1.7'

Next, when we generate our User model, instead of making the password field a string, we need to give it type digest. We can generate our practice User model like so:

rails g model User first_name:string last_name:string email:string password:digest

Now, if you look at your /app/models/user.rb file, you should see the helper method has_secure_password automatically included. That helper method allows us to use BCrypt super conveniently. In order to hash our users’ passwords, all we have to do is pass in a password_confirmation on registration, and BCrypt will save the hashed password in the password_digest column. Then, to authenticate a user, we have the handy .authenticate() method, which will either return false if the authentication fails or the user object if it passes. Let’s mess around a bit in the console to demonstrate:

#creating/registering a new user
user = User.new(first_name:"John", last_name:"Doe", email:"john@gmail.com", password:"password")
user.save #should return false, we're missing the password_confirmation
user.password_confirmation = "password"
user.save #should return true, John Doe is now in the database with a hashed password

#authenticating a user
user = User.find_by_email("john@gmail.com")
user.authenticate("incorrect_password") #returns false
user.authenticate("password") #returns user (the User object)

So now we’ve secured our users’ passwords super easily with BCrypt. All we had to do to set this up was include the BCrypt gem in our gemfile and give our user model a password_digest column! We still have some work left to go before we have a suitable login and registration process though. It’s time to get into some TDD gems!

First let’s talk about RSpec. RSpec is a robust gem that facilitates the creation of automated tests in Ruby/Rails. To get started with RSpec, we need to add gem 'rspec-rails' to our gemfile. After bundle install, we need to run another command, rails generate rspec:install. This command will generate our /spec/ folder along with our first rspec test files! Go take a look in /spec/models/user_spec.rb and you should see something like this:

require 'rails_helper'
RSpec.describe User, type: :model do
  pending "add some examples to (or delete) #{__FILE__}"
end

In this first test file, we have a “describe” block with one test pending. That pending test is just a placeholder, so let’s replace it with some real tests. We’ll need to use some RSpec syntax, so he’s a quick run-through of what we’ll be using:

  • context "..." do – starts a block that groups tests with same prefixes
  • it "..." do – starts a test block, contains test code and assertions
    • expect(...).to – first half of an expect statement (assertion)QQ, test code goes inside
    • eq(...) – used when expecting output to equal something, ex:
      • expect("A").to eq("A")
    • be_valid – runs ruby .isValid? method and checks for true return
      • expect(user).to be_valid
    • be_invalid – opposite of be_valid

Now let’s see RSpec in action. First things first, we’ll replace that pending test with two context blocks:

RSpec.describe User, type: :model do
  context 'with valid attributes' do
  end

  context 'with invalid attributes' do
  end
end

We want to make sure our User model saves properly with valid inputs, so let’s add some tests for that.

context 'with valid attributes' do
  it 'should save' do
    new_user = User.new(first_name:"Joe", last_name:"Doe", email:"joe@gmail.com", password:"asdf", password_confirmation:"asdf")
    expect(new_user.save).to eq(true)
  end

  it 'automatically encrypts password' do
    new_user = User.create(first_name:"Joe", last_name:"Doe", email:"joe@gmail.com", password:"asdf", password_confirmation:"asdf)
    expect(new_user.password_digest.present?).to eq(true)
  end 
end

Okay, we now have a couple tests for our User model. Let’s try running them from the console with rspec spec/models/user_spec.rb! We should see 0 failures because we already know we can save users and we know that we already have BCrypt set up. Smooth sailing so far, but wait a minute, our code does not look super dry. These are just the first two tests and things are already getting a little repetitive and gross. Before continuing with more tests, let’s fix this problem with another gem called Factory Girl!

Factory Girl is a handy gem that allows us to create shortcuts for generating model objects. Instead of writing out User.new(first_name... last_name... etc.) every time I want to generate a test user, I would like to be able to just run a command like build(:user). So let’s get to it! To begin, we need to add gem 'factory_girl_rails' to our gemfile and run bundle install again (might also need to run rails generate rspec:install if you don’t see a /factories/ folder in your /spec/ folder). Next, take a look in the /spec/rails_helper.rb file and add the following line under RSpec.configure do|config|. This will allow us to use FactoryGirl methods in our RSpec tests:

config.include FactoryGirl::Syntax::Methods

 Now go take a peek at the freshly created /spec/factories/users.rb! This “factory” is where we can define default values for each field on our model. Then, when we want to generate a model object, we can either use build(:user) (equivalent of User.new()), or create(:user) (equivalent of User.create()). If we want to create a user with a different values for specific fields, we can do so by passing in only the values we want to change from the default: build(:user, first_name: "Jill", email: "jill@gmail.com"). Let’s update our factory so that our default test user is John Doe:

FactoryGirl.define do
  factory :user do
    first_name "John"
    last_name "Doe"
    email "john@gmail.com"
    password "password"
    password_confirmation "password"
  end
end

Now that we have our factory ready to start pumping out John Does, let’s refactor the tests from earlier (in /spec/models/user_spec.rb):

context 'with valid attributes' do
  it 'should save' do
    expect(build(:user).save).to eq(true)
  end

  it 'automatically encrypts password' do
    expect(create(:user).password_digest.present?).to eq(true)
  end 
end

Now that’s nice and DRY. Run the tests again and you should still see 0 failures since all we did was replace the manually generated new_user with FactoryGirl’s method build(:user). Let’s move on to the next context block and make some tests for scenarios with invalid user data.

context 'with invalid attributes' do
  it 'should not save if first_name is blank' do
    expect(build(:user, first_name:"").save).to eq(false)
  end

  it 'should not save if email is blank' do
    expect(build(:user, email:"").save).to eq(false)
  end 
end

Give these a run and we should see something new in the terminal. Two failures! Looks like these two new tests are failing because the user is saving even though the first_name or email are blank! So how can we make these tests pass? Validate presence of course! Add the line validates :first_name, :email, presence: true to the model class in /app/models/user.rb. Now, if we run the tests again we should have 0 failures!

With this you should be able to get the general idea of how to write tests with RSpec for model validations. What we have here is a good start, but we need to write a lot more tests and validations before we’ll have a fully finished User model. There is one more gem that I want to mention in this post though, because it’s super great for TDD feature testing. The gem is called Capybara and it allows us to write tests that simulate how a user would interact with our page. Here’s an example test just for a little taste:

feature 'Logout feature' do
  scenario 'logs out user and redirects to login page' do
    click_button 'Log out'
    expect(page).to have_current_path '/sessions/new'
  end
end

Yeah, there’s all sorts of fancy stuff you can do in both RSpec and Capybara, but this post has been taking me too long to get over with already. I could probably write an entire post just about TDD with Capybara but I’d rather get back to current events at the Dojo. I may be almost a week behind again, but I’ll catch up! It’s already project week right now, lol! Stay tuned for the exciting events of Day 43!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s