Upgrade to Pro — share decks privately, control downloads, hide ads and more …

RSpec and Testing Best Practices

RSpec and Testing Best Practices

This talk was first presented at Yoolk on the 7th August 2015.

Samnang Chhun

August 07, 2015
Tweet

More Decks by Samnang Chhun

Other Decks in Programming

Transcript

  1. RSpec.describe "HelloMessage" do context "when passing name" do it "returns

    hello message with passing name" do result = HelloMessage.new("Samnang").result expect(result).to eq("Hello Samnang!") end end context "without passing name" do it "returns the default of hello message" do result = HelloMessage.new.result expect(result).to eq("Hello World!") end end end
  2. class HelloMessage def initialize(name = nil) @name = name end

    def result if @name "Hello %s!" % @name else "Hello World!" end end end
  3. skip RSpec.describe "A feature" do xit "skip with xit" do

    end skip "skip with inline method call" do end it "skip with tag skip", skip: "give a reason" do end it "skip with method call" do skip("give a reason") end end
  4. pending RSpec.describe "A feature" do it "pending with tag pending",

    pending: 'give a reason' do end pending "pending with inline method call" do end it "pending with method call" do pending("give a reason") end end
  5. let ō .C\[GXCNWCVKQP OGOQK\CVKQP ō 4GETGCVGFGCEJGZCORNG RSpec.describe User do describe

    "#locked?" do let(:user) { User.new(cards: cards) } context "when user no chards" do let(:cards) { [] } # examples end end end
  6. subject ō .C\[GXCNWCVKQP OGOQK\CVKQP ō +ORTQXG4GCFCDKNKV[ ō &4; RSpec.describe User,

    type: :model do describe "validations" do subject { build(:user) } it { is_expected.to validate_presence_of(:email) } it { is_expected.to validate_uniqueness_of(:email) } it { is_expected.to validate_length_of(:username).is_at_least(3) } it { is_expected.to validate_uniqueness_of(:username) } end end
  7. Build in matchers # Object equivalence expect(actual).to eq(expected) # Comparisons

    expect(actual).to be > expected expect(actual).to match(/expression/) expect(actual).to be_within(delta).of(expected) # Types/classes/response expect(actual).to be_instance_of(expected) expect(actual).to be_kind_of(expected) expect(actual).to respond_to(expected) # Expecting errors expect { ... }.to raise_error expect { ... }.to raise_error(ErrorClass) https://www.relishapp.com/rspec/rspec-expectations/docs/built-in-matchers
  8. Custom matchers https://www.relishapp.com/rspec/rspec-expectations/docs/custom-matchers RSpec::Matchers.define :be_a_multiple_of do |expected| match do |actual|

    actual % expected == 0 end end RSpec.describe 9 do it { is_expected.to be_a_multiple_of(3) } end RSpec.describe 9 do it { is_expected.not_to be_a_multiple_of(4) } end
  9. Composing matchers describe BackgroundWorker do it 'puts enqueued jobs onto

    the queue in order' do worker = BackgroundWorker.new worker.enqueue(:klass => "Class1", :id => 37) worker.enqueue(:klass => "Class2", :id => 42) expect(worker.queue).to match [ a_hash_including(:klass => "Class1", :id => 37), a_hash_including(:klass => "Class2", :id => 42) ] end end
  10. Compound Expectations class StopLight def color %w[ green yellow red

    ].shuffle.first end end RSpec.describe StopLight, "#color" do let(:light) { StopLight.new } it "is green, yellow or red" do expect(light.color).to eq("green").or eq("yellow").or eq("red") end it "passes when using boolean OR | alias" do expect(light.color).to eq("green") | eq("yellow") | eq("red") end end
  11. Allowing messages RSpec.describe UsersController do describe "GET :show" do it

    "assigns :user" do user = double(:user) allow(User).to receive(:find).with("1").and_return(user) get :show, id: 1 expect(assigns[:user]).to eq(user) end end end
  12. Expecting messages RSpec.describe MonthlySubscriptionCharger do context "when user has active

    subscription" do it "charges user with their plan" do user = double(:user, active_subscription?: true) creditcard_charger = double(:creditcard_charger) expect(creditcard_charger).to receive(:charge).with(user) monthly_charger = MonthlySubscriptionCharger.new(user, creditcard_charger) monthly_charger.call end end end
  13. Expecting messages class MonthlySubscriptionCharger def initialize(user, creditcard_charger) @user = user

    @creditcard_charger = creditcard_charger end def call if @user.active_subscription? @creditcard_charger.charge(@user) end end end
  14. Spies RSpec.describe MonthlySubscriptionCharger do context "when user has active subscription"

    do it "charges user with their plan" do user = double(:user, active_subscription?: true) creditcard_charger = double(:creditcard_charger, charge: true) monthly_charger = MonthlySubscriptionCharger.new(user, creditcard_charger) monthly_charger.call expect(creditcard_charger).to have_received(:charge).with(user) end end end
  15. Spies RSpec.describe MonthlySubscriptionCharger do context "when user has active subscription"

    do it "charges user with their plan" do user = double(:user, active_subscription?: true) creditcard_charger = double(:creditcard_charger).as_null_object monthly_charger = MonthlySubscriptionCharger.new(user, creditcard_charger) monthly_charger.call expect(creditcard_charger).to have_received(:charge).with(user) end end end
  16. Spies RSpec.describe MonthlySubscriptionCharger do context "when user has active subscription"

    do it "charges user with their plan" do user = double(:user, active_subscription?: true) creditcard_charger = spy(:creditcard_charger) monthly_charger = MonthlySubscriptionCharger.new(user, creditcard_charger) monthly_charger.call expect(creditcard_charger).to have_received(:charge).with(user) end end end
  17. How to describe your behaviors RSpec.feature "Widget management", :type =>

    :feature do scenario "User creates a new widget" do visit "/widgets/new" fill_in "Name", :with => "My Widget" click_button "Create Widget" expect(page).to have_text("Widget was successfully created.") end end
  18. How to describe your behaviors RSpec.describe User do describe "#full_name"

    do it "return full name of user" do user = User.new(first_name: 'Samnang', last_name: 'Chhun') result = user.full_name expect(result).to eq('Samnang Chhun') end end end
  19. Use contexts RSpec.describe User do describe "#full_name" do context "when

    user missing first name" do # examples end context "when user has middle name" do # examples end end end
  20. Four-Phase Test test do setup exercise verify teardown end it

    "encrypts the password" do user = User.new(password: 'password') user.save expect(user.encrypted_password).not_to be_nil end
  21. Let’s Not RSpec.describe User do describe "#valid?" do let(:user) {

    User.new(attributes) } context "when passing valid attributes" do let(:attributes) { { first_name: "Samnang", last_name: "Chhun" } } it "returns true" do expect(user.valid?).to eq(true) end end context "when passing invalid attributes" do let(:attributes) { { first_name: nil, last_name: "Chhun" } } it "returns false" do expect(user.valid?).to eq(false) end end end end
  22. Let’s Not RSpec.describe User do describe "#valid?" do context "when

    passing valid attributes" do it "returns true" do user = build_user(first_name: "Samnang", last_name: "Chhun") expect(user.valid?).to eq(true) end end context "when passing invalid attributes" do it "returns false" do user = build_user(first_name: nil, last_name: "Chhun") expect(user.valid?).to eq(false) end end def build_user(attributes) User.new(attributes) end end end
  23. Define custom matchers RSpec::Matchers.define :have_access_to do |beta_feature| match do |user|

    FeatureToggle.has_access?(user, beta_feature) end end RSpec.describe User do subject { build_stubbed(:beta_user) } it { is_expected.to have_access_to :mystery_machine } end
  24. Only one expectation per example RSpec.describe "HelloMessage" do it "returns

    hello message" do result_passing_name = HelloMessage.new("Samnang").result result_without_passing_name = HelloMessage.new.result expect(result_passing_name).to eq("Hello Samnang!") expect(result_without_passing_name).to eq("Hello World!") end end
  25. Only one expectation per example RSpec.describe "HelloMessage" do context "when

    passing name" do it "returns hello message with passing name" do result = HelloMessage.new("Samnang").result expect(result).to eq("Hello Samnang!") end end context "without passing name" do it "returns the default of hello message" do result = HelloMessage.new.result expect(result).to eq("Hello World!") end end end
  26. Avoid touching DB RSpec.describe User do describe "#full_name" do it

    "return full name of user" do user = FactoryGirl.create(:user, first_name: "Samnang", last_name: "Chhun") expect(user.full_name).to eq("Samnang Chhun") end end end
  27. Avoid touching DB RSpec.describe User do describe "#full_name" do it

    "return full name of user" do user = User.new(first_name: “Samnang", last_name: "Chhun") expect(user.full_name).to eq("Samnang Chhun") end end end
  28. References • https://www.relishapp.com/rspec/ • http://betterspecs.org/ • https://robots.thoughtbot.com/lets-not • https://robots.thoughtbot.com/four-phase-test •

    https://robots.thoughtbot.com/speed-up-tests-by-selectively-avoiding-factory-girl • http://blog.codeclimate.com/blog/2013/10/09/rails-testing-pyramid/ • http://martinfowler.com/articles/mocksArentStubs.html • https://github.com/thoughtbot/guides/tree/master/best-practices#testing