1af30891b7e22f41acf79a1e103b6a18?s=70x70

rails conf

Abstract

When our tests fail all we get is output in a terminal. Testing is core to the culture of Ruby, and the tools are sophisticated, but something big is missing from the workflow. Namely, a powerful debugger.

Integrating Pry creates an interactive, enjoyable TDD workflow. In this talk, I’ll recount the somewhat unique development experience that made me a believer. I’ll demonstrate with Pry a responsive and immersive TDD cycle, taking a test to green before it exits its first run.

Details

Best case scenario, the audience understands the value of making a debugger like Pry an integral part of their workflow rather than an afterthought. I want to show people how enjoyable an interactive TDD workflow can be. I want people to become fans and supporters of Pry.

I want to demonstrate with examples the pain of how most Rubyists TDD, which is mostly about being in the dark. I want to contrast that with a workflow that integrates Pry, which is about being immersed in the system while testing it and implementing it, and getting much faster feedback. I am considering demoing the TDD lifecycle in Smalltalk. Whether I do or not, I want to show how a debugger drives and informs the TDD process.

Pitch

I am a TDD believer and have been practicing it for many years in various languages.

I don’t have much involvement in Pry other than being a fan. I have been promoting it on the Gaslight blog over the years. I Like Pry But…, Pryme Time.

I saw Conrad Irwin’s talk at Rails Conf in Portland and it was vey well received. I consider this follow up. An intermediate and practical application of Pry as part of a regular TDD workflow.

The unique development experience that I allude to in my abstract is that I was a professional Smalltalk developer for some years. Like many Rubyists, I’m skeptical of IDEs and I’m most productive with a terminal and VIM. But I also loved working in the Smalltalk environment, which is arguably the worlds most unforgiving IDE. That enjoyment had a lot to much do with an integrated, interactive debugging tool that drove that process. I’ve never encountered anything quite like it in any other language.

Speakers

Joel Turnbull (joelbywan@gmail.com) Joel Turnbull is a Code Designer at Gaslight. He’s been a staple in the Cincinnati development community for over 10 years. This is his first presentation at a Rails Conf.

1af30891b7e22f41acf79a1e103b6a18?s=70x70

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.