Slide 1

Slide 1 text

RSpec and Testing Best Practices @samnangchhun

Slide 2

Slide 2 text

Review Quick

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

class HelloMessage def initialize(name = nil) @name = name end def result if @name "Hello %s!" % @name else "Hello World!" end end end

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

class HelloMessage DEFAULT_MESSAGE = "World" def initialize(name = DEFAULT_MESSAGE) @name = name end def result "Hello %s!" % @name end end

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

Running Specs

Slide 12

Slide 12 text

--format https://github.com/thekompanee/fuubar https://github.com/grosser/rspec-instafail

Slide 13

Slide 13 text

--tag

Slide 14

Slide 14 text

--profile

Slide 15

Slide 15 text

--fail-fast

Slide 16

Slide 16 text

.rspec 45RGETGCFUEQOOCPFNKPGEQPHKIWTCVKQPQRVKQPUHTQOHKNGUKPVJTGGFKHHGTGPVNQECVKQPU • .QECN ./.rspec-local • 2TQLGEV ./.rspec • )NQDCN ~/.rspec

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Running specs in Editors

Slide 20

Slide 20 text

Fundamental RSepc

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

Test Doubles RSepc

Slide 28

Slide 28 text

Mock Stub Fake Spy Test Double

Slide 29

Slide 29 text

Test Double 6GUVFQWDNGKUCIGPGTKEVGTOHQTCP[QDLGEVVJCVUVCPFUKPHQT CTGCNQDLGEVFWTKPICVGUV VJKPMUVWPVFQWDNGʼn  Ń45RGE/QEMFQEWOGPVCVKQP https://www.relishapp.com/rspec/rspec-mocks/docs/basics/test-doubles

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Testing Best Practices

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

Only one expectation per example Only one logical expectation per example

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

Testing Pyramid http://blog.codeclimate.com/blog/2013/10/09/rails-testing-pyramid/

Slide 50

Slide 50 text

Thank you!

Slide 51

Slide 51 text

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