Upload
vysakh-sreenivasan
View
916
Download
5
Tags:
Embed Size (px)
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!!
- https://relishapp.com/rspec- https://github.com/reachlocal/rspec-style-
guide - https://github.com/eliotsykes/rspec-rails-
examples (rspec-rails)- http://betterspecs.org/-
Resources
Carpe Diem