Use the left/right arrow keys to navigate.

Do Rspec

Forgotten features of Rspec 1.0

http://bogdan.github.com/dorspec

Bogdan Gusiev

2011.04

Where we came from?

TestUnit - very straight unit test framework. Unit testing was used for ages.

Unit test consists of:

  • Workflow
  • Assertions

Where should we go?

Describe and test business logic
in agile development proccess

We should remember that

  • This presentation cover only AGILE PROCESS
    • Some of the thoughts here not applicable to elder projects
  • Ruby or Rails is based on Ruby which is object oriented programming language.

Describing the behavior in Rspec

Every unit test is a workflow

  1. Step 1: Construct user with valid attributes
  2. Step 2: Call save method
  3. Assertion: Email confirmation letter is delivered.

Every spec is a requirement that consists of:

  • Subject
    • New user with valid attributes
  • Context
    • When saved
  • Behavior
    • Confirmation letter should be delivered

Subject is a code block

Desinged to be a convention.

Rather than create instance variables with different names

Describe subject in rspec

Example:

describe User do
    subject { User.new(@valid_attributes) }
    # or
    subject { Factory.build(:user)

    describe ".activated named scope" do
        subject { User.activated }
    end
end

Default subject is: described_class.new

And that's for a reason.

Describing behavior

Benefits of subject:

  • Delegate expectation to subject
    • all should goes to subject by default
  • Delegate custom methods to subject (#its)
    • we can specify behavior of some subject property

Matchers:

  • Built-in
  • Custom

Expectation usage example

describe User do
    subject { build_user_with_factory }

    it { should be_valid }

    its(:profile) { should_not be_completed } # 2.0
    # subject.profile.completed?.should be_true

    its("tasks.first") {should be_a(FindFriendsTask)} # 2.0
    # subject.tasks.first.is_a?(FindFriendsTask).should be_true 

end

New property emerged

Description-less example

Examples description is a maintenance point we are trying to reduce

Matchers

Internal matchers:

  • be_*
    • Accepts any method with question mark and it's arguments
  • include
  • ==

External libraries:

  • Shoulda
  • Remarkable

Beware: copypaste is not BDD (and is not programming)!

Describe the context

This is where the problem is!

Rspec is designed to describe context-oriented logic

Business logic is always context oriented

Describe non context-oriented logic in rspec has no benefits

What is context dependent logic?

Context-oriented software - from less context dependent to very context dependent:

  • Utility functions
    • All math and any other function that doesn't data from nowhere except it's arguments.
  • Proxy like objects and software
    • Sometimes have an inner state
  • Stateful software
    • databases and libraries that wrap them
  • Business logic
    • That is what most of us do

Rspec techniques to describe context

  • Tools
    • #let
    • #context
  • Techniques
    • nesting context blocks
    • Dynamic context
    • Context callbacks

#let function for dynamic context

designed for lazy loading objects to the context as they needed

Some source code

    def let(name, &block) 
        define_method name do 
            @assignments ||= {} 
            @assignments[name] ||= instance_eval(&block) 
        end 
    end
    
    def let!(name, &block) 
        let(name, &block) 
        before { __send__(name) } 
    end
    
    # sampe principle of lazy load for subject
    
    def subject
        if defined?(@original_subject)
            @original_subject
        else
            @original_subject = instance_eval(&self.class.subject)
        end
    end
 

#let basic usage example

describe User do
    describe ".activated named scope" do

        subject { User.activated }

        let(:activated_user) { <factory> }
        let(:not_activated_user) { <factory> }

        it {should include(activated_user)
        it {should_not include(not_activated_user)
    end
end 

#let calls are chainable

let(:user) { ... }
let(:community) { ... }

let(:membership) {
    Membership.new(
        :user => user, :community => community
    ) 
} 

Bang version of #let

Will create user and at once load it to the context

let!(:user) { ... }

Nested #context

Designed to test non-linear workflow

Nested context to test workflow

describe Ticket do

  context "after save!" do

    it { should be_new }

    context "after approved  by admin" do
      it {should be_approved} 
    end

    context "after declined by admin" do
      it { should be_declined }
    end

  end
end 

New property

Spec Outline

#context + #let =

Pattern Matches + Context Callbacks

"Pattern matching" (c) S. Boiko

Use to describe utitlity functions

describe BooleanExpression, ".run" do

    subject {BooleanExpression.run(string)}

    context "with '&' and both true" do
        let(:string) { "true & true" }
        it {should be_true }
    end

    context "with '&' and one false" do
        let(:string) { "false & true" }
        it {should be_false }
    end

end 

Most complicated example in this presentation

    

          4



      2  
        3   5   7




              6
    1             8
    describe Product do
    
        subject { Factory.create(:product) }
        
        context "after save" do
        
        before { 
            subject.owner.confirmed = _confirmed
            subject.save!
        }
        
        context "when owner is confirmed" do
            let(:_confirmed) { true }
            it { should be_delivered }
        end
        
        context "when owner is not confirmed" do
            let(:_confirmed) { false }
            it { should_not be_delivered }
        end
    
    end
  1. Meet first example
  2. Seek for before block and call it
  3. Find subject call
  4. Call subject block
  5. Come back to before and discover _confirmed call
  6. Call _confirmed block
  7. Come back to before block and finish execution
  8. Run the matcher

Look and feel before and after

    context "match scheduler" do

    before(:each) do
      school = Factory.create(:school)
      @team = Factory.create(:team, :school => school)
      @student1 = Factory.create(:student, :team => @team, :school => school)
      @student2 = Factory.create(:student, :team => @team, :school => school)
      @competition = Factory.create(:competition, :team => @team)
    end

    describe "upcoming_matches" do
      it "should return upcoming match for particular student" do
        @student1.upcoming_matches.first.id.should == 
          @student2.upcoming_matches.first.id
      end
    end

    describe "next_opponent" do
      it "should return correct opponent for student" do
        @student2.next_opponent.should == @student1
        @student1.next_opponent.should == @student2
      end
    end

    describe "current_match" do
      it "should return started match for student" do
        @student1.current_match.should be_nil
        @competition.start!
        @competition.matches.first.id.should == @student1.current_match.id
      end
    end

  end   
    context "student" do

    let(:school) { Factory.create(:school) }

    let(:team) { Factory.create(:team, :school => school) }
    
    subject do
      Factory.create(:student, :team => team, :school => school)
    end

    it { should be_valid }

    context "match scheduler" do

      before { subject } 

      let!(:opponent) do
        Factory.create(:student, :team => team, :school => school)
      end

      let!(:competition) { Factory.create(:competition, :team => team) }

      its(:upcoming_matches) { should_not be_empty }
      its(:next_opponent)    { should == opponent } 
      its(:current_match)    { should be_nil } 
      its(:next_match)       { should == opponent.next_match }

      context "competition started" do
        before { competition.start! }
        its(:current_match) {should == competition.matches.first}
      end

      context "after_destroy" do
        before { subject.destroy }
        its(:team) { should_not be_destroyed }
      end

    end
  end 
Come back to:

Custom matchers

A class with very clean api recognized by Rspec

Custom matcher that test validation

describe User do 
    it { should accept_values_for(
        :email, "john@example.com", "lambda@gusiev.com"
    )}
    it { should_not accept_values_for(
        :email, "invalid", nil, "a@b", "john@.com"
    )} 
end

https://github.com/bogdan/accept_values_for

There is a lot information on the web how to do that.

Shared examples group

Rspec counterpart to Ruby .include

SEG example

Use shared examples group to reuse tests

class User
    include CommunityMember
end

describe User
    it_should_behave_like "CommunityMember"
    #Quotes can be remove in 2.0
end

shared_examples_group "CommunityMember" do
    context do
        it { should ..... }
    end
end 

Be abstract!

In order to make your tests reusable you need to:

  • Subject is the same
  • #described_class is described class
  • All context staff get shared.
  • 2.0 there is automatic subcontext for example group

Shared examples group example

shared_examples_for "Traits::Dictionary::Core" do
  describe "as Traits::Dictionary::Core" do
    it {should_not accept_values_for(:title, nil)}

    describe ".options_for_select" do
      let!(:object) do 
        Factory.create(described_class.to_s.underscore)
      end
      subject { described_class.options_for_select }
      it { should == [[object.title, object.id]]}
    end
  end
end

Power of SEG

We can do all kinds of staff in SEG:

  • Delegate matchers and methods to subject
  • Create an insance of decribed class with factory
  • Call class methods

Summary

Rspec book approach

Read World

The following aspects are not touched in the book:

  • Deadline
  • Budget
  • Client: "I want it right now"

The schema not considered for Agile development.

And very good for elder projects.

My approach

The development lifecycle consists of the following steps:

  • Draft the code of the feature you plan to implement
  • Spec the code you wrote
  • Fix the issues in code and spec
  • Refactor code and spec

Acceptance tests are used only after UI gets stable.

Always remember

We are agile. Your code might live not more than one day.

Summary: a lot of text

In order to describe business logic in agile development process we use the best tool designed for that - Rspec, that offers cool api:

  • Subject
  • Matchers
  • Context
  • Let
  • Shared examples group

That let implement the following ideas:

  • Description-less examples
  • Non-linear workflow spec
  • Dynamic context
  • Context callbacks
  • Pattern matching

Examples