Testing Ruby with Rspec (a beginner's guide)

Preview:

Citation preview

Testing Ruby with Rspec - Vysakh Sreenivasan (vysakh0)

I was like this before

This is how I met my girlfriend

Then this happened :-/

promise me never to execute the program to see the output i.e ruby file_name.rb

Before we start testing

Pinky promise? Yes?

$ mkdir ruby_testing

$ cd ruby_testing

$ mkdir lib

Open terminal & prepare env

$ bundle init$ bundle inject rspec, 3.2$ bundle --binstubs$ rspec --init

Create Gemfile. Insert & install rspec

4 phases of testing

- Setup- Exercise - Verify- Teardown (Testing framework

does it for us, duh!)

# setupuser = User.new(name: 'yolo')

#expect

user.save

# verify

expect(user.name).not_to be_nil

Wth is expect? Hmm, it is a matcher?

Describethe method or class you want to test

# lib/user.rb

class Userend

# spec/user_spec.rbrequire ‘spec_helper’require ‘user’

RSpec.describe User doend

# lib/sum.rb

def sum(a, b) a + bend

# spec/sum_spec.rbrequire ‘spec_helper’require ‘sum’

RSpec.describe ‘#sum’ doend

Run the specs by the cmdrspec

# lib/calc.rb

class Calc def sum(a, b) a + b end end

# spec/calc_spec.rbrequire ‘spec_helper’require ‘calc’

RSpec.describe Calc do describe ‘#sum’ do endend

Tip - use Describe strictly for class & methods

- describe “ClassName” do end

- describe “#instance_method” do end

- describe “.class_method” do end

describe tells who is tested

We need something to

Tell what we are testing of it

it ‘gives sum of 2’ do end

# orit {}

require ‘spec_helper’require ‘calc’RSpec.describe Calc do describe ‘#sum’ do it “returns sum of 2 numbers” do end endend

Lets apply 4 phases of testing

require ‘spec_helper’

require ‘calc’RSpec.describe ‘Calc’ do describe ‘#sum’ do it “returns sum of 2 numbers” do calc = Calc.new # setup result = calc.sum(2, 3) # exercise expect(result).to eql(5) # verify end endend

require ‘calc’RSpec.describe Calc do describe ‘#sum’ do it “returns sum of 2 numbers” do calc = Calc.new # setup expect(calc.sum(2, 3)).to eql(5) # exercise & verify end endend

For brevity in the slides,

I’m gonna leave the require ‘spec_helper’

Tip - it statement

- don’t use “should” or “should not” in description

- say about actual functionality, not what might be happening

- Use only one expectation per example.

(lets just take a quick look)

Matchers

expect(num.odd?).to be false#=> num = 2

expect(user.email).to be_falsey#=> user.email = nil

expect(num).to be >= 3#=> num = 5

expect(str).to match /testing$/#=> str = “Welcome to testing”

expect(tiger).to be_an_instance_of(Tiger)#=> tiger.class => Tiger

expect(tiger).to be_a(Cat)#=> tiger.class.superclass => Cat

expect { sum }.to raise_error ArugmentError#=> sum(a, b)

expect(person).to have_attributes(:name => "Jim", :age => 32)#=> person.name => Jim

expect(list).to start_with(36)#=> list = [36, 49, 64, 81]

expect(name).to start_with(‘M’)#=> name = “Matz”

expect(list).to end_with(81)#=> list = [36, 49, 64, 81]

expect(name).to end_with(‘z’)#=> name = “Matz”

expect("a string").to include("str")

expect([1, 2]).to include(1, 2)

expect(:a => 1, :b => 2).to include(:a, :b)

expect(:a => 1, :b => 2).to include(:a => 1)

expect([1, 2]).not_to include(1)

expect(name).not_to start_with(‘M’)#=> name = “DHH

expect(tiger).not_to be_a(Lion)#=> tiger.class => Tiger

expect([1, 3, 5]).to all( be_odd )

# it is inclusive by defaultexpect(10).to be_between(5, 10)

# ...but you can make it exclusive: checks in range 4..9expect(10).not_to be_between(5, 10).exclusive

# ...or explicitly label it inclusive:expect(10).to be_between(5, 10).inclusive

expect([1, 2, 3]).to contain_exactly(2, 1, 3)#=> pass

expect([1, 2, 3]).to contain_exactly(2, 1)#=> fail

expect([1, 2, 3]).to match_array [2, 1, 3]#=> pass

expect([1, 2, 3]).to match_array [2, 1]#=> fail

Normal equality expectations do not work well for floating point values

expect(27.5).to be_within(0.5).of(28.0)expect(27.5).to be_within(0.5).of(27.2)

expect(27.5).not_to be_within(0.5).of(28.1)expect(27.5).not_to be_within(0.5).of(26.9)

There are aliases for matchersuse it based on the context

a_value > 3 be < 3

a_string_matching(/foo/) match(/foo/)

a_block_raising(ArgumentError) raise_error(ArgumentError)

See this gist for more aliases https://gist.github.com/JunichiIto/f603d3fbfcf99b914f86

Few matchers and their aliases

is_expected.to same as expect(subject).to

require ‘calc’

RSpec.describe Calc do it { expect(Calc).to respond_to(:sum) } end

require ‘calc’

RSpec.describe Calc do it { expect(subject).to respond_to(:sum) } end

require ‘calc’

RSpec.describe Calc do it { is_expected.to respond_to(:sum) } end

Compound matchersusing and, or

expect(str).to start_with(“V”).and end_with(“h”)

#=> str = “Vysakh”

expect(stoplight.color).to eq("red").or eq("green").or eq("yellow")

#=> stoplight.color ⇒ “yellow”

change matcher

# lib/team.rb

class Team attr_accessor :goals def score @goals += 1 end end

require ‘team’RSpec.describe Team do describe ‘#score’ do it ‘increments goals’ do team = Team.new expect { team.score }.to change(team, :goals).by(1) end endend

x = y = 0

expect { x += 1 y += 2}.to change { x }.to(1).and change { y }.to(2)

Composable matchers

s = "food"

expect { s = "barn" }.to change { s }. from( a_string_matching(/foo/) ). to( a_string_matching(/bar/) )

expect(arr).to match [ a_string_ending_with("o"), a_string_including("e") ]

#=> arr = [“bozo”, “great”]

Magical(predicate) Matchers

expect(0).to be_zero#=> 0.zero? ⇒ true

expect(2).to be_even#=> 2.even? ⇒ true

expect(me).to have_job#=> me.has_job? ⇒ true

Scenarios of methods orContexts

# lib/duh.rb

def duh(num) if num.odd? “mumbo” else “jumbo” endend

require ‘duh’RSpec.describe ‘#duh’ do it ‘says mumbo if number is odd’ do expect(duh(3)).to eq “mumbo” end it ‘says jumbo if number is not odd’ do expect(duh(4)).to eq “jumbo” endend

Never use if inside it

Instead use context

require ‘duh’RSpec.describe ‘#duh’ do context ‘when number is odd’ do it ‘says mumbo’ do expect(duh(3)).to eq “mumbo” end end context ‘when number is not odd’ do it ‘says jumbo’ do expect(duh(4)).to eq “jumbo” end endend

Tip - Context

- Always has an opposite negative case

- So, never use a single context.- Always begin with “when…”

let helper

require ‘team’RSpec.describe Team do describe ‘#score’ do

it ‘increments goals’ do team = Team.new expect(team.score).to change(Team.goals).by(1) end

end

describe ‘#matches_won’ do

it ‘gives number of matches won by the team” do team = Team.new expect(team.matches_won).to eq 0 end

endend

require ‘team’RSpec.describe Team do

let(:team) { Team.new } describe ‘#score’ do

it ‘increments goals’ do expect(team.score).to change(Team.goals).by(1) end

end

describe ‘#matches_won’ do

it ‘gives number of watches won by the team” do expect(team.matches_won).to eq 0 end

endend

def team Team.newend

let(:team) is same as

Is invoked only when it is called

before & after helper

require ‘team.’RSpec.describe Team do

before do

@team = Team.new puts “Called every time before the it or specify block” end describe ‘#score’ do

it ‘increments goals of the match’ do

expect(@team.score).to change(Team.goals).by(1) end

it ‘increments total goals of the Team’’ do

expect(@team.score).to change(Team.total_goals).by(1) end end

end

require ‘team’RSpec.describe Team do

before(:suite) do

puts “Get ready folks! Testing are coming!! :D ” end describe ‘#score’ do

it ‘increments goals of the match’ do

expect(@team.score).to change(Team.goals).by(1) end

it ‘increments total goals of the Team’’ do

expect(@team.score).to change(Team.total_goals).by(1) end end

end

Types passed to before/after

- :example (runs for each test)- :context (runs for each context)- :suite (runs for entire suite, only

once, see database cleaner gem)

Tip - Use let instead of before

- To create data for the spec examples.

- let blocks get lazily evaluated

# use this:let(:article) { FactoryGirl.create(:article) }

# ... instead of this:before { @article = FactoryGirl.create(:article) }

Tip: Use before/after for

- actions or

- when the same obj/variable needs to be used in different examples

before do @book = Book.new(title: "RSpec Intro") @customer = Customer.new @order = Order.new(@customer, @book)

@order.submitend

Use factorygirl to create test objects

Stubs

class PriceCalculator def add(product) products << product end

def products @products ||= [] end

def total @products.map(&:price).inject(&:+) endend

class Productend

describe PriceCalculator do it "allows for method stubbing" do calculator = PriceCalculator.new calculator.add(double(price: 25.4)) calculator.add(double(price: 101))

expect(calculator.total).to eq 126.4 endend

#This works even if there is no Product class is defined # in the actual program

class Product attr_reader :priceend

class PriceCalculator def add(product) products << product end

def products @products ||= [] end

def total @products.map(&:price).inject(&:+) endend

describe PriceCalculator do it "allows for method stubbing" do calculator = PriceCalculator.new calculator.add instance_double("Product", price: 25.4) calculator.add instance_double("Product", price: 101)

expect(calculator.total).to eq 126.4 endend

# throws and error if a Product class or its methods are # not defined

$ irb> require ‘rspec/mocks/standalone’> class User; end> allow(User).to receive(:wow).and_return(“Yolo”)> User.wow => “Yolo Yolo”

You can also use block to return instead and_return

allow(User).to receive(:wow) { (“Yolo”) }

3 types of return for wow method

allow(User).to receive(:wow) .and_return(“yolo”, “lol”, “3rd time”)

Diff output when running diff times

User.wow #=> yoloUser.wow #=> lolUser.wow #=> 3rd timeUser.wow #=> 3rd timeUser.wow #=> 3rd time

So, you could use it as if it is 2 different objects

2.times { calculator.add product_stub }

Diff between double & instance_double

Instance double requires- class to be defined- methods to be defined in that class. - Only then a method can be allowed to it.

Use stubs, mocks, spieswith caution

Skip and Focus tests

Say you have 3 failing tests

xit- add x to all but one failing it

blocks- xit blocks will be skipped

You can use xit or skip: true

xit “does this thing” doendit “asserts name”, skip: true doendit “asserts name”, skip: “Bored right now” doend

it “asserts name” do pendingendit “asserts name” do skipend

You can use skip/pending inside it

Say you have 20 tests, all passing but very slow

You can use fit to focus specific test

fit “asserts name” doend

#=> rspec --tag focus #=> only this block will be run

Another way to focus specific test

it “asserts name”, focus: true doend

#=> rspec --tag focus #=> only this block will be run

You can also use the same in describe or context

fdescribe “#save” doend

describe “#save”, skip: true doend

Use subject when possible

describe Article do subject { FactoryGirl.create(:article) }

it 'is not published on creation' do expect(subject).not_to be_published endend

Shared examples

RSpec.describe FacebookAPI do it "has posts" do expect(FbAPI.new("vysakh0")).to respond_to :posts end it_behaves_like("API", FbAPI.new(“vysakh0”))end

Rspec.describe TwitterAPI do it "has tweets" do expect(TwitterAPI.new("vysakh0")).to respond_to :tweets end

it_behaves_like("API", TwitterAPI.new(“vysakh0”))end

RSpec.shared_examples_for "API" do |api| it "returns a formatted hash" do expect(api.profile).to match [ a_hash_including( name: an_instance_of(String), category: an_instance_of(String), price: an_instance_of(Float)) ] endend

Shared context

RSpec.shared_context "shared stuff" do before { @some_var = :some_value } def shared_method "it works" end let(:shared_let) { {'arbitrary' => 'object'} } subject do 'this is the subject' endend

require "./shared_stuff.rb"

RSpec.describe "#using_shared_stuff'" do include_context "shared stuff"

it "has access to methods defined in shared context" do expect(shared_method).to eq("it works") endend

Custom Matchers

RSpec::Matchers.define :be_a_multiple_of do |expected| match do |actual| actual % expected == 0 endend

# usage:expect(9).to be_a_multiple_of(3)

RSpec::Matchers.define :be_a_palindrome do match do |actual| actual.reverse == actual endend

# usage:expect(“ror”).to be_a_palindrome

RSpec::Matchers.define :be_bigger_than do |min| chain :but_smaller_than, :max match do |value| value > min && value < max endend

# usage:expect(10).to be_bigger_than(5).but_smaller_than(15)

Define negated matcher

RSpec::Matchers.define define_negated_matcher :exclude, :include # rather than# expect(odd_numbers).not_to include(12)expect((odd_numbers).to exclude(12)

# user_a = User.new(“A”); user_b = User.new(“B”) # users = [user_a, user_b]expect(users).to include(user_a).and exclude(user_b)

There are lot more awesomeness!!

Carpe Diem

Recommended