Complex rspec specs can intermittently fail in continuous integration

We use an external service to perform our full suite of unit and integration rspec tests on every push of changes to our revision control repository.

We’ve found that tests that pass locally end up failing or, even worse, intermittently failing (flap) when run by the Continuous Integration service.

This is often due to dependencies that are available locally but are not part of our project or correctly prepared for in our tests.

Another common reason is complex test code that becomes fragile in the context of the third party CI environment.

So, here are some things I look to clean up when I see a ‘flapping’ spec:

  • Use factories. Rather than update models after creating them change the factory to accept a transient attribute and modify or add dependent models in an after build or create block. In other words try not to persist to the database in code in the spec itself particularly if it is a feature spec.
  • Assign variables using let blocks rather than within tests or before blocks.
  • code>mkdir_p before writing files to tmp directories to ensure the directory will exist when you need it.
  • Create straightforward assertions. Have one expectation per test unless the runtime savings of combining a group into a single test clearly outweighs the complexity and potential fragility you’re introducing.

Most of these are best practice for rspec tests. As with many things in software, following a recommended pattern avoids unpleasant surprises because smart people have solved problems before you can stumble into them.

Before cleanup

[ruby]require 'spec_helper'

describe 'ProductFeedGenerator' do

  it 'should generate an entry with no parent book, delete flag set to 0, and is in stock with US rights' do
    author = FactoryGirl.create(:author)
    cover_image = FactoryGirl.create(:tagged_asset, :cover_image, :image)
    sub_category1 = FactoryGirl.create(:sub_category, list_type: SubCategory::ListTypes::BISAC)
    sub_category2 = FactoryGirl.create(:sub_category, list_type: SubCategory::ListTypes::BISAC)
    sub_category3 = FactoryGirl.create(:sub_category, list_type: SubCategory::ListTypes::AUS)
    work = FactoryGirl.create(:work, sub_categories: [sub_category1, sub_category2, sub_category3])
    book = FactoryGirl.create(:book, :us, :available_for_sale, author: author, tagged_assets: [cover_image], work: work)

    description = BookShortDescription.create(book: book, country: 'us', short_description: 'A short description')

    product_number = book.isbn13
    product_name = 'Under the Dome'
    product_desc = description.short_description
    product_url = "http://foo.org/book/3",
    image_url = 'http://wtnadgfadhguy.cloudfront.net/assets/9366_1666839.jpg'
    parent_product_number = nil
    parent_product_name = nil
    kit_number = nil
    brand_id = author.key 
    brand_name = author.name
    msrp = 35.00
    list_price = 35.00
    sale_price = nil
    sale_condition = 'new'
    availability = 'in stock'
    available_qty = 0
    product_category_number = "#{sub_category1.code},#{sub_category2.code}"
    date_created = nil
    date_modified = nil
    delete_flag = 0

    product_attributes_fields = (1..10).map { nil }

    output_row = [product_number,
                  product_name,
                  product_desc,
                  product_url,
                  image_url,
                  parent_product_number,
                  parent_product_name,
                  kit_number,
                  brand_id,
                  brand_name,
                  msrp,
                  list_price,
                  sale_price,
                  sale_condition,
                  availability,
                  available_qty,
                  product_category_number,
                  date_created,
                  date_modified,
                  delete_flag]

    output_row += product_attributes_fields

    output_row = output_row.join("\t") + "\r\n"

    expect(Agilone::ProductFeedGenerator.generate_row(book.isbn13, 'US')).to eq output_row
  end

  it 'should generate an entry with a parent book, delete flag set to 1, and is osi not with US rights' do
    author = FactoryGirl.create(:author)
    cover_image = FactoryGirl.create(:tagged_asset, :cover_image, :image)
    sub_category1 = FactoryGirl.create(:sub_category, list_type: SubCategory::ListTypes::BISAC)
    sub_category2 = FactoryGirl.create(:sub_category, list_type: SubCategory::ListTypes::BISAC)
    work = FactoryGirl.create(:work, sub_categories: [sub_category1, sub_category2])
    book = FactoryGirl.create(:book, :us, :not_available_for_sale, author: author, tagged_assets: [cover_image], work: work, item_format: Book::Format::TRADE_PAPERBACK)
    other_book = FactoryGirl.create(:book, :us, :available_for_sale, author: author, tagged_assets: [cover_image], work: work, title: 'Another title')

    other_book.key_dates = [BookKeyDate.new(book: other_book, country: 'us', date: Date.yesterday, date_type: 'on_sale_date')]
    other_book.save!

    description = BookShortDescription.create(book: book, country: 'us', short_description: 'A short description')

    product_number = book.isbn13
    product_name = 'Under the Dome'
    product_desc = description.short_description
    product_url = "http://books.simonandschuster.com/Under-the-Dome/Stephen-King/#{book.isbn13}"
    image_url = 'http://d1jqlel0jyvr6e.cloudfront.net/tagged_assets/9366_1666839.jpg'
    parent_product_number = other_book.isbn13
    parent_product_name = other_book.title
    kit_number = nil
    brand_id = author.key 
    brand_name = author.name
    msrp = 35.00
    list_price = 35.00
    sale_price = nil
    sale_condition = 'new'
    availability = 'out of stock'
    available_qty = 0
    product_category_number = "#{sub_category1.code},#{sub_category2.code}"
    date_created = nil
    date_modified = nil
    delete_flag = 1

    product_attributes_fields = (1..10).map { nil }

    output_row = [product_number,
                  product_name,
                  product_desc,
                  product_url,
                  image_url,
                  parent_product_number,
                  parent_product_name,
                  kit_number,
                  brand_id,
                  brand_name,
                  msrp,
                  list_price,
                  sale_price,
                  sale_condition,
                  availability,
                  available_qty,
                  product_category_number,
                  date_created,
                  date_modified,
                  delete_flag]

    output_row += product_attributes_fields

    output_row = output_row.join("\t") + "\r\n"

    expect(Agilone::ProductFeedGenerator.generate_row(book.isbn13, 'US')).to eq output_row
  end
end[/ruby]

After cleanup

[ruby]require 'spec_helper'

describe 'ProductFeedGenerator' do

  let(:author) { FactoryGirl.create(:author) }
  let(:cover_image) { FactoryGirl.create(:tagged_asset, :cover_image, :image) }
  let(:sub_category1) { FactoryGirl.create(:sub_category, list_type: SubCategory::ListTypes::BISAC) }
  let(:sub_category2) { FactoryGirl.create(:sub_category, list_type: SubCategory::ListTypes::BISAC) }
  let(:sub_category3) { nil }
  let(:work) { FactoryGirl.create(:work, sub_categories: [sub_category1, sub_category2, sub_category3].compact) }
  let(:description) { BookShortDescription.create(book: book, country: 'us', short_description: 'A short description') }
  
  let!(:output_row) {
    ([book.isbn13,
      book.title,
      description.short_description,
      "http://foo.org/book/#{book.id}",
      "http://#{s3.cloudfront_host}/#{book.image}",
      parent_product_number,
      parent_product_name,
      nil,
      author.key,
      author.name,
      35.00,
      35.00,
      nil,
      'new',
      availability,
      0,
      "#{sub_category1.code},#{sub_category2.code}",
      nil,
      nil,
      delete_flag] + (1..10).map { nil }).join("\t") + "\r\n"
  }
  
  subject { Agilone::ProductFeedGenerator.generate_row(book.isbn13, 'US') }
  
  context 'should generate an entry with no parent book, delete flag set to 0, and is in stock with US rights' do
    let(:sub_category3) { FactoryGirl.create(:sub_category, list_type: SubCategory::ListTypes::AUS) }
    let(:book) { FactoryGirl.create(:book, :us, :available_for_sale, author: author, tagged_assets: [cover_image], work: work) }
    let(:parent_product_number) { nil }
    let(:parent_product_name) { nil }
    let(:availability) { 'in stock' }
    let(:delete_flag) { 0 }
    

    it { is_expected.to eq output_row }
  end

  context 'should generate an entry with a parent book, delete flag set to 1, and is osi not with US rights' do
    let(:book) { FactoryGirl.create(:book, :us, :not_available_for_sale, author: author, tagged_assets: [cover_image], work: work, item_format: Book::Format::TRADE_PAPERBACK) }
    let(:other_book) { FactoryGirl.create(:book, :us, :available_for_sale, author: author, tagged_assets: [cover_image], work: work, title: 'Another title', on_sale_date: Date.yesterday) }
    let(:parent_product_number) { other_book.isbn13 }
    let(:parent_product_name) { other_book.title }
    let(:availability) { 'out of stock' }
    let(:delete_flag) { 1 }

    it { is_expected.to include(other_book.isbn13) }
    it { is_expected.to include(other_book.title) }
    it { is_expected.to include('out of stock') }
    it { is_expected.to match(/1\t+\r\n$/)}
  end
end[/ruby]

Force bundler to rebuild your Ruby on Rails project gemset

I got myself into a bad place after migrating a Rails project to a newer version of Ruby where my gemset was built with the wrong native libraries (rbenv rehash?).

Resulting error when running rspec:

dyld: lazy symbol binding failed: Symbol not found: _rb_funcall2

I had to delete and rebuild my gems but bundler itself doesn’t offer a pristine option.

The easiest way I found was to temporarily remove all gems from my Gemfile so that my Gemfile looked like.

source 'https://rubygems.org'

ruby '2.1.2'

Then:

bundle clean –force

Then undo and re-save my complete Gemfile and:

bundle install

Good to go.

Ruby on Rails Reading List (kind of)

A student from RailsBridge NYC asked me for a reading list. Rather than focus on Ruby or Rails, I broadened the topic to software writing and writers I look to for inspiration. Here’s my reply:

The best book on Ruby language is the The Well-Grounded Rubyist by David Black. The Ruby documentation is pretty handy, especially the API pages http://ruby-doc.org/

For the Rails framework I dive into code and rely on google searches of Stack Overflow Q&A’s and the rails docs http://api.rubyonrails.org/ for answers to specific questions I run into.

For developer practice, I’ve been reading James Shore (The Art of Agile Development), Diana Larsen (Liftoff: Launching Agile Teams & Projects) and Jean Tabaka (Collaboration Explained: Facilitation Skills for Software Project Leaders)

For inspiration, I love The Existential Pleasures of Engineering by Samuel Florman and To Engineer Is Human: The Role of Failure in Successful Design by Henry Petroski.

Thought leaders who helped shape my values as a developer, product person and development manager are Jeff Sutherland, Ken Schwaber, Bob Martin, and Steve McConnell.

For online training resources, my team and I are using RailsCast, ThoughtBot, RubyTapas, Code School.

RailsBridge NYC

I am a coder and hiring manager for Rails developers. I am father of a daughter who aspires to a career in science and technology. For both reasons, I am grateful that there are programs like RailsBridge that introduce Ruby and Ruby on Rails to women interested in the technology and hopefully, a career in software development.

Bianca Rodrigues provides a nice, brief description of the RailsBridge NYC workshop June 6th.

It was great to meet other newbie Ruby developers who were going through some of the same beginner pains that I was. The workshop was a positive experience, and gave me the push I periodically need to keep myself immersed in technology. (read the entire post)

There is a large body of research and writing (including my own paper) on the fact that women are drastically under-represented in software development disproportionate to other careers in science and technology.

If you are an experienced developer, please consider volunteering. I participated as a TA and it really only took two evenings and one full day of my time to be of use to this great program. The experience of coaching is both a joy and a refreshing opportunity to see what you do through other people’s eyes.

Hiring a ruby on rails developer

One of our team is moving on. So we’re looking to hire one full-time, experienced developer.

At Simon & Schuster, we’ve created a small, collaborative team where people can do their best, learn in a collegial environment and get home at a reasonable hour. We work at a sustainable pace because after five years as a Ruby on Rails team we know the value of staying current, refactoring our codebase, and cleaning up tech debt.

We pair, we test drive, we retrospect, and we work as a single team with our product owners. If you want to work on a team that really does these things, contact us.

Scrum/XP/Agile team in midtown Manhattan looking for an experienced Rubyist or experienced Java/C# developer interested in learning Ruby. We are an established, six developer Scrum/XP team. We have a dedicated scrum master and we collaborate closely with our product team

Apply via StackOverflow

No recruiters, please.