Blog

Crafting Rspec Steps with step_eval and DRYing them with a Helper: Part 1

By matthias

Speccing steps with step_eval, implementing them and finally DRYing up.

StepSpecr wraps the step performing functionality in a single method. Through calling step_eval in an example the specified step will be evaluated in the context of the example. So you can do this:

1
2
3
4
5
6
7
8
9

  describe "Given a step that sets @number to 7" do
    it "should set number to 7" do
      @number = 1
      step_eval "Given a step that sets @number to 7", :spec
      @number.should == 7
    end
  end
  

Getting started

The script snippets that follows are executable rspec examples. You'll need to have installed rspec and rails as gems.

One trivial passing example to verify the functionality.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  
  require 'rubygems'
  require 'active_support'
  require 'spec'
  require 'spec/matchers'
  require 'spec/story'
  require 'spec/story/step'

  # This is the function that performs the step.

  def step_eval(stepname, stepgroupname)
    type = stepname.split(/\s+/).first.to_s.downcase.to_sym
    stepname = stepname.split(/\s+/)[1,100].join(" ")
    stepgroup = steps_for stepgroupname
    step = stepgroup.find(type, stepname)
    if step == nil
      raise Spec::Expectations::ExpectationNotMetError.new("Didn't find step: '#{stepname}'") 
    end
    args = step.parse_args(stepname)
    step.perform self, *args
  end



  # Here are the step implementations

  steps_for :model do
  end



  # Here is the spec

  describe "description" do
    it "should description" do

    end
  end

Run it from TextMate:

Writing a first example

We want to have a step that takes a number and a name and that will create so much models (specified by name) as specified by number. Like this:

Given $count $models

Let's write an example: Given 1 article

1
2
3
4
5
6
7
8
9
10

  describe "Given $count $models" do
   it "should create 1 article" do
     class Article
     end
     Article.should_receive(:create).once
     step_eval "Given 1 article", :model
   end
 end 
 

Here is the executable version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

 require 'rubygems'
 require 'active_support'
 require 'spec'
 require 'spec/matchers'
 require 'spec/story'
 require 'spec/story/step'

 # This is the function that performs the step.

 def step_eval(stepname, stepgroupname)
   type = stepname.split(/\s+/).first.to_s.downcase.to_sym
   stepname = stepname.split(/\s+/)[1,100].join(" ")
   stepgroup = steps_for stepgroupname
   step = stepgroup.find(type, stepname)
   if step == nil
     raise Spec::Expectations::ExpectationNotMetError.new("Didn't find step: '#{stepname}'") 
   end
   args = step.parse_args(stepname)
   step.perform self, *args
 end



 # Here are the step implementations

 steps_for :model do
 end



 # Here is the spec

 describe "Given $count $models" do
   it "should create 1 article" do
     class Article
     end
     Article.should_receive(:create).once
     step_eval "Given 1 article", :model
   end
 end
 
 

Run it:

The runner didn't find the step. We have to declare it in the stepgroup:
1
2
3
4
5
6
7

 steps_for :model do
   
   Given "$count $models" do |count, model|
   end
   
 end

Executable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

require 'rubygems'
require 'active_support'
require 'spec'
require 'spec/matchers'
require 'spec/story'
require 'spec/story/step'

# This is the function that performs the step.

def step_eval(stepname, stepgroupname)
  type = stepname.split(/\s+/).first.to_s.downcase.to_sym
  stepname = stepname.split(/\s+/)[1,100].join(" ")
  stepgroup = steps_for stepgroupname
  step = stepgroup.find(type, stepname)
  if step == nil
    raise Spec::Expectations::ExpectationNotMetError.new("Didn't find step: '#{stepname}'") 
  end
  args = step.parse_args(stepname)
  step.perform self, *args
end



# Here are the step implementations

steps_for :model do
   
   Given "$count $models" do |count, model|
   end
   
 end



# Here is the spec

describe "Given $count $models" do
  it "should create 1 article" do
    class Article
    end
    Article.should_receive(:create).once
    step_eval "Given 1 article", :model
  end
end

Run it:

Implementing the step

The Mock (Article) expected to receive create. Let's call create on him:

1
2
3
4
5
6
7
8

  steps_for :model do

     Given "$count $models" do |count, model|
       Article.create
     end

   end

Executable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

require 'rubygems'
require 'active_support'
require 'spec'
require 'spec/matchers'
require 'spec/story'
require 'spec/story/step'

# This is the function that performs the step.

def step_eval(stepname, stepgroupname)
  type = stepname.split(/\s+/).first.to_s.downcase.to_sym
  stepname = stepname.split(/\s+/)[1,100].join(" ")
  stepgroup = steps_for stepgroupname
  step = stepgroup.find(type, stepname)
  if step == nil
    raise Spec::Expectations::ExpectationNotMetError.new("Didn't find step: '#{stepname}'") 
  end
  args = step.parse_args(stepname)
  step.perform self, *args
end



# Here are the step implementations

steps_for :model do
   
   Given "$count $models" do |count, model|
     Article.create
   end
   
 end



# Here is the spec

describe "Given $count $models" do
  it "should create 1 article" do
    class Article
    end
    Article.should_receive(:create).once
    step_eval "Given 1 article", :model
  end
end

Run:

Green Light!

Writing a second example

Our 'Given $count $models' should not only create 1 article every time we perform it. It should also create 2 memberships, 3 assets or 17 lists. Let's write another example to specify this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  
  describe "Given $count $models" do
    it "should create 1 article" do
      class Article
      end
      Article.should_receive(:create).once
      step_eval "Given 1 article", :model
    end

    it "should create 17 lists" do
      class List
      end
      List.should_receive(:create).exactly(17).times
      step_eval "Given 17 lists", :model
    end
  end

Executable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
  require 'rubygems'
  require 'active_support'
  require 'spec'
  require 'spec/matchers'
  require 'spec/story'
  require 'spec/story/step'

  # This is the function that performs the step.

  def step_eval(stepname, stepgroupname)
    type = stepname.split(/\s+/).first.to_s.downcase.to_sym
    stepname = stepname.split(/\s+/)[1,100].join(" ")
    stepgroup = steps_for stepgroupname
    step = stepgroup.find(type, stepname)
    if step == nil
      raise Spec::Expectations::ExpectationNotMetError.new("Didn't find step: '#{stepname}'") 
    end
    args = step.parse_args(stepname)
    step.perform self, *args
  end



  # Here are the step implementations

  steps_for :model do

     Given "$count $models" do |count, model|
       Article.create
     end

   end



  # Here is the spec

  describe "Given $count $models" do
    it "should create 1 article" do
      class Article
      end
      Article.should_receive(:create).once
      step_eval "Given 1 article", :model
    end

    it "should create 17 lists" do
      class List
      end
      List.should_receive(:create).exactly(17).times
      step_eval "Given 17 lists", :model
    end
  end

Run the examples:

The failure message is 'undefined method `create' for Article:Class'. This is because in the second example the expectation is that the List class receives create but is the step implementation we are statically calling Article.create.

Implementing the step

We need to evaluate a constant that is specified by a string:

1
2
3
4
5
6
7
8
9
10

  steps_for :model do

     Given "$count $models" do |count, model|
       klass = eval "#{model.singularize.camelize}"
       klass.create
     end

   end
 
Let's try this. Heres the

Executable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

  require 'rubygems'
  require 'active_support'
  require 'spec'
  require 'spec/matchers'
  require 'spec/story'
  require 'spec/story/step'

  # This is the function that performs the step.

  def step_eval(stepname, stepgroupname)
    type = stepname.split(/\s+/).first.to_s.downcase.to_sym
    stepname = stepname.split(/\s+/)[1,100].join(" ")
    stepgroup = steps_for stepgroupname
    step = stepgroup.find(type, stepname)
    if step == nil
      raise Spec::Expectations::ExpectationNotMetError.new("Didn't find step: '#{stepname}'") 
    end
    args = step.parse_args(stepname)
    step.perform self, *args
  end



  # Here are the step implementations

  steps_for :model do

     Given "$count $models" do |count, model|
       klass = eval "#{model.singularize.camelize}"
       klass.create
     end

   end



  # Here is the spec

  describe "Given $count $models" do
    it "should create 1 article" do
      class Article
      end
      Article.should_receive(:create).once
      step_eval "Given 1 article", :model
    end

    it "should create 17 lists" do
      class List
      end
      List.should_receive(:create).exactly(17).times
      step_eval "Given 17 lists", :model
    end
  end

Run:

This is obvious: create has to be called 'count' times:
1
2
3
4
5
6
7
8
9
10

    steps_for :model do

       Given "$count $models" do |count, model|
         klass = eval "#{model.singularize.camelize}"
         count.to_i.times { klass.create }
       end

     end
   

Executable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
   require 'rubygems'
   require 'active_support'
   require 'spec'
   require 'spec/matchers'
   require 'spec/story'
   require 'spec/story/step'

   # This is the function that performs the step.

   def step_eval(stepname, stepgroupname)
     type = stepname.split(/\s+/).first.to_s.downcase.to_sym
     stepname = stepname.split(/\s+/)[1,100].join(" ")
     stepgroup = steps_for stepgroupname
     step = stepgroup.find(type, stepname)
     if step == nil
       raise Spec::Expectations::ExpectationNotMetError.new("Didn't find step: '#{stepname}'") 
     end
     args = step.parse_args(stepname)
     step.perform self, *args
   end



   # Here are the step implementations

   steps_for :model do

      Given "$count $models" do |count, model|
        klass = eval "#{model.singularize.camelize}"
        count.to_i.times { klass.create }
      end

    end



   # Here is the spec

   describe "Given $count $models" do
     it "should create 1 article" do
       class Article
       end
       Article.should_receive(:create).once
       step_eval "Given 1 article", :model
     end

     it "should create 17 lists" do
       class List
       end
       List.should_receive(:create).exactly(17).times
       step_eval "Given 17 lists", :model
     end
   end
   

Run:

Green again!

-->Part 2

 

Sorry, comments are closed for this article.

Blog