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]