17 April 2014

Accessing Models with Confidence in Cucumber Specs with factory_girl

We’ve mentioned our love of page objects in feature testing. Setting up the environment page objects, our pattern is to implement method missing to handle methods of type _page. When a step requests login_page, method missing picks up the request, lazy loads a LoginPage into a instance variable on demand. That @login_page instance is then available to any subsequent step in the spec through the login_page method.

module ProjectWorld
  def method_missing(method, *args, &block)
    if method =~ /_page$/
      variable_name = "@#{method}"
      ivar = instance_variable_get(variable_name)

      unless ivar
        page = method.to_s.classify.constantize.new
        ivar = instance_variable_set(variable_name, page)
      end
      ivar
    else
      super
    end
  end
end
World(ProjectWorld)

This is possible because any instance variable that is set in the World of a Cucumber feature spec is globally available to any other step of that spec. This applies not only to page objects, but to any data models that are created or factory’d up into instance variables. We found ourselves doing things like this a lot in our steps.

Given(/^there is an active promotion with title "(.*?)"$/) do |title|
  @active_promotion = FactoryGirl.create(:active_promotion, title: title)
end

Given(/^the active promotion has a message "(.*?)"$/) do |text|
  FactoryGirl.create(:message, promotion: @active_promotion, text: text)
end

The assumption in the second step makes makes me uncomfortable, i.e. some previous step has set up @active_promotion. Keep in mind these two steps could have been defined in completely different files, and still work. Global accessibility of instance variables in Cucumber seems wrong, but it can be rationalized I suppose. You can be relatively assured that each spec is a self contained little program with a relatively small scope.

We can avoid this in our data objects though by catching calls to them and lazy loading, similar to what we do with our page objects. Since we’ve given in to the idea of global state in our specs, why not apply this same pattern to our short-lived data models? Chris Nelson and I decided to give it a try on our last project.

It turns out that factory_girl makes this super simple. factory_girl makes it easy to represent a model in it’s various states by just setting it up and referring to it by name. For example it’s easy and preferable in factory_girl to define representations for a promotion like this:

FactoryGirl.create(:active_promotion)
FactoryGirl.create(:pending_promotion)

rather than:

FactoryGirl.create(:promotion, status: "active")
FactoryGirl.create(:promotion, status: "pending")

This makes it easy define simple accessors in our Project world that return very specific, consistent data objects. Our module iterates through each defined factory, defining a lazy-loading accessor for each:

module ProjectWorld
  def method_missing(method, *args, &block)
    ...
  end

  FactoryGirl.factories.map(&:name).each do |factory|
    define_method(factory) do
      variable_name = "@#{factory}"
      ivar = instance_variable_get(variable_name)

      unless ivar
        object = FactoryGirl.create(factory)
        ivar = instance_variable_set(variable_name, object)
      end
      ivar
    end
  end
end

We can now effectively change the above steps to this:

Given(/^there is an active promotion with title "(.*?)"$/) do |title|	
end

Given(/^the active promotion has a message "(.*?)"$/) do |text|
  FactoryGirl.create(:message, promotion: active_promotion, text: text
end

The active_promotion method in the second step has been defined in our module. It will look for an instance_variable @active_promotion, and return it if it is defined. Otherwise it will set that @active_promoiton to a newly instantiated active_promotion factory. This effectively makes the first step, which was responsible for setting up the @active_promotion, a NOOP.

This pattern allows us to access models in the same way in every step, having confidence that it will never be nil.

Heads up! This article may make reference to the Gaslight team—that's still us! We go by Launch Scout now, this article was just written before we re-introduced ourselves. Find out more here.

Related Posts

Want to learn more about the work we do?

Explore our work

Ready to start your software journey with us?

Contact Us