115
Testing Ruby with Rspec - Vysakh Sreenivasan (vysakh0)

Testing Ruby with Rspec (a beginner's guide)

Embed Size (px)

Citation preview

Page 1: Testing Ruby with Rspec (a beginner's guide)

Testing Ruby with Rspec - Vysakh Sreenivasan (vysakh0)

Page 2: Testing Ruby with Rspec (a beginner's guide)

I was like this before

Page 3: Testing Ruby with Rspec (a beginner's guide)
Page 4: Testing Ruby with Rspec (a beginner's guide)

This is how I met my girlfriend

Page 5: Testing Ruby with Rspec (a beginner's guide)

Then this happened :-/

Page 6: Testing Ruby with Rspec (a beginner's guide)
Page 7: Testing Ruby with Rspec (a beginner's guide)
Page 8: Testing Ruby with Rspec (a beginner's guide)

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

Before we start testing

Page 9: Testing Ruby with Rspec (a beginner's guide)

Pinky promise? Yes?

Page 10: Testing Ruby with Rspec (a beginner's guide)

$ mkdir ruby_testing

$ cd ruby_testing

$ mkdir lib

Open terminal & prepare env

Page 11: Testing Ruby with Rspec (a beginner's guide)

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

Create Gemfile. Insert & install rspec

Page 12: Testing Ruby with Rspec (a beginner's guide)

4 phases of testing

Page 13: Testing Ruby with Rspec (a beginner's guide)

- Setup- Exercise - Verify- Teardown (Testing framework

does it for us, duh!)

Page 14: Testing Ruby with Rspec (a beginner's guide)

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

#expect

user.save

# verify

expect(user.name).not_to be_nil

Page 15: Testing Ruby with Rspec (a beginner's guide)

Wth is expect? Hmm, it is a matcher?

Page 16: Testing Ruby with Rspec (a beginner's guide)

Describethe method or class you want to test

Page 17: Testing Ruby with Rspec (a beginner's guide)

# lib/user.rb

class Userend

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

RSpec.describe User doend

Page 18: Testing Ruby with Rspec (a beginner's guide)

# lib/sum.rb

def sum(a, b) a + bend

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

RSpec.describe ‘#sum’ doend

Page 19: Testing Ruby with Rspec (a beginner's guide)

Run the specs by the cmdrspec

Page 20: Testing Ruby with Rspec (a beginner's guide)
Page 21: Testing Ruby with Rspec (a beginner's guide)

# 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

Page 22: Testing Ruby with Rspec (a beginner's guide)
Page 23: Testing Ruby with Rspec (a beginner's guide)

Tip - use Describe strictly for class & methods

- describe “ClassName” do end

- describe “#instance_method” do end

- describe “.class_method” do end

Page 24: Testing Ruby with Rspec (a beginner's guide)

describe tells who is tested

We need something to

Tell what we are testing of it

Page 25: Testing Ruby with Rspec (a beginner's guide)

it ‘gives sum of 2’ do end

# orit {}

Page 26: Testing Ruby with Rspec (a beginner's guide)

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

Page 27: Testing Ruby with Rspec (a beginner's guide)

Lets apply 4 phases of testing

Page 28: Testing Ruby with Rspec (a beginner's guide)

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

Page 29: Testing Ruby with Rspec (a beginner's guide)

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

Page 30: Testing Ruby with Rspec (a beginner's guide)

For brevity in the slides,

I’m gonna leave the require ‘spec_helper’

Page 31: Testing Ruby with Rspec (a beginner's guide)

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.

Page 32: Testing Ruby with Rspec (a beginner's guide)
Page 33: Testing Ruby with Rspec (a beginner's guide)

(lets just take a quick look)

Matchers

Page 34: Testing Ruby with Rspec (a beginner's guide)

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

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

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

Page 35: Testing Ruby with Rspec (a beginner's guide)

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

Page 36: Testing Ruby with Rspec (a beginner's guide)

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

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

Page 37: Testing Ruby with Rspec (a beginner's guide)

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”

Page 38: Testing Ruby with Rspec (a beginner's guide)

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)

Page 39: Testing Ruby with Rspec (a beginner's guide)

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

Page 40: Testing Ruby with Rspec (a beginner's guide)

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

Page 41: Testing Ruby with Rspec (a beginner's guide)

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

Page 42: Testing Ruby with Rspec (a beginner's guide)

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)

Page 43: Testing Ruby with Rspec (a beginner's guide)

There are aliases for matchersuse it based on the context

Page 44: Testing Ruby with Rspec (a beginner's guide)

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

Page 45: Testing Ruby with Rspec (a beginner's guide)

is_expected.to same as expect(subject).to

Page 46: Testing Ruby with Rspec (a beginner's guide)

require ‘calc’

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

Page 47: Testing Ruby with Rspec (a beginner's guide)

require ‘calc’

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

Page 48: Testing Ruby with Rspec (a beginner's guide)

require ‘calc’

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

Page 49: Testing Ruby with Rspec (a beginner's guide)

Compound matchersusing and, or

Page 50: Testing Ruby with Rspec (a beginner's guide)

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

#=> str = “Vysakh”

Page 51: Testing Ruby with Rspec (a beginner's guide)

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

#=> stoplight.color ⇒ “yellow”

Page 52: Testing Ruby with Rspec (a beginner's guide)

change matcher

Page 53: Testing Ruby with Rspec (a beginner's guide)

# lib/team.rb

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

Page 54: Testing Ruby with Rspec (a beginner's guide)

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

Page 55: Testing Ruby with Rspec (a beginner's guide)

x = y = 0

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

Page 56: Testing Ruby with Rspec (a beginner's guide)

Composable matchers

Page 57: Testing Ruby with Rspec (a beginner's guide)

s = "food"

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

Page 58: Testing Ruby with Rspec (a beginner's guide)

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

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

Page 59: Testing Ruby with Rspec (a beginner's guide)

Magical(predicate) Matchers

Page 60: Testing Ruby with Rspec (a beginner's guide)

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

Page 61: Testing Ruby with Rspec (a beginner's guide)

Scenarios of methods orContexts

Page 62: Testing Ruby with Rspec (a beginner's guide)

# lib/duh.rb

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

Page 63: Testing Ruby with Rspec (a beginner's guide)

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

Page 64: Testing Ruby with Rspec (a beginner's guide)

Never use if inside it

Instead use context

Page 65: Testing Ruby with Rspec (a beginner's guide)

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

Page 66: Testing Ruby with Rspec (a beginner's guide)

Tip - Context

- Always has an opposite negative case

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

Page 67: Testing Ruby with Rspec (a beginner's guide)

let helper

Page 68: Testing Ruby with Rspec (a beginner's guide)

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

Page 69: Testing Ruby with Rspec (a beginner's guide)

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

Page 70: Testing Ruby with Rspec (a beginner's guide)

def team Team.newend

let(:team) is same as

Is invoked only when it is called

Page 71: Testing Ruby with Rspec (a beginner's guide)

before & after helper

Page 72: Testing Ruby with Rspec (a beginner's guide)

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

Page 73: Testing Ruby with Rspec (a beginner's guide)

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

Page 74: Testing Ruby with Rspec (a beginner's guide)

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)

Page 75: Testing Ruby with Rspec (a beginner's guide)

Tip - Use let instead of before

- To create data for the spec examples.

- let blocks get lazily evaluated

Page 76: Testing Ruby with Rspec (a beginner's guide)

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

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

Page 77: Testing Ruby with Rspec (a beginner's guide)

Tip: Use before/after for

- actions or

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

Page 78: Testing Ruby with Rspec (a beginner's guide)

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

@order.submitend

Page 79: Testing Ruby with Rspec (a beginner's guide)

Use factorygirl to create test objects

Page 80: Testing Ruby with Rspec (a beginner's guide)

Stubs

Page 81: Testing Ruby with Rspec (a beginner's guide)

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

Page 82: Testing Ruby with Rspec (a beginner's guide)

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

Page 83: Testing Ruby with Rspec (a beginner's guide)

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

Page 84: Testing Ruby with Rspec (a beginner's guide)

You can also use block to return instead and_return

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

Page 85: Testing Ruby with Rspec (a beginner's guide)

3 types of return for wow method

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

Page 86: Testing Ruby with Rspec (a beginner's guide)

Diff output when running diff times

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

Page 87: Testing Ruby with Rspec (a beginner's guide)

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

2.times { calculator.add product_stub }

Page 88: Testing Ruby with Rspec (a beginner's guide)

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.

Page 89: Testing Ruby with Rspec (a beginner's guide)

Use stubs, mocks, spieswith caution

Page 90: Testing Ruby with Rspec (a beginner's guide)

Skip and Focus tests

Page 91: Testing Ruby with Rspec (a beginner's guide)

Say you have 3 failing tests

Page 92: Testing Ruby with Rspec (a beginner's guide)

xit- add x to all but one failing it

blocks- xit blocks will be skipped

Page 93: Testing Ruby with Rspec (a beginner's guide)

You can use xit or skip: true

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

Page 94: Testing Ruby with Rspec (a beginner's guide)

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

You can use skip/pending inside it

Page 95: Testing Ruby with Rspec (a beginner's guide)

Say you have 20 tests, all passing but very slow

Page 96: Testing Ruby with Rspec (a beginner's guide)

You can use fit to focus specific test

fit “asserts name” doend

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

Page 97: Testing Ruby with Rspec (a beginner's guide)

Another way to focus specific test

it “asserts name”, focus: true doend

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

Page 98: Testing Ruby with Rspec (a beginner's guide)

You can also use the same in describe or context

fdescribe “#save” doend

describe “#save”, skip: true doend

Page 99: Testing Ruby with Rspec (a beginner's guide)

Use subject when possible

Page 100: Testing Ruby with Rspec (a beginner's guide)

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

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

Page 101: Testing Ruby with Rspec (a beginner's guide)

Shared examples

Page 102: Testing Ruby with Rspec (a beginner's guide)

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

Page 103: Testing Ruby with Rspec (a beginner's guide)

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

Page 104: Testing Ruby with Rspec (a beginner's guide)

Shared context

Page 105: Testing Ruby with Rspec (a beginner's guide)

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

Page 106: Testing Ruby with Rspec (a beginner's guide)

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

Page 107: Testing Ruby with Rspec (a beginner's guide)

Custom Matchers

Page 108: Testing Ruby with Rspec (a beginner's guide)

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)

Page 109: Testing Ruby with Rspec (a beginner's guide)

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

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

Page 110: Testing Ruby with Rspec (a beginner's guide)

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)

Page 111: Testing Ruby with Rspec (a beginner's guide)

Define negated matcher

Page 112: Testing Ruby with Rspec (a beginner's guide)

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)

Page 113: Testing Ruby with Rspec (a beginner's guide)

There are lot more awesomeness!!

Page 115: Testing Ruby with Rspec (a beginner's guide)

Carpe Diem