A test basically consists of the the 3 steps:
1.) Arrange 2.) Act 3.) Assert
In the first step (Arrange) the necessary testing environment is made up for running the test. The effort depends on the test.
If test complexity is unreasonably high and it takes much effort to make up the objects, the problem is more the implementation than the test itself. Then it is rewarding to think about, why the complexity is that high. A test, that is hard to write, is a code smell for a bad implementation.
In general only objects, which are significant for running the test, should be created. Ideally this is just one object: the object under test.
Factories for test object
If it takes some effort to generate test objects, it makes sense to use factories.
This makes maintaining test objects easy. There is just one location: the factory. In Ruby land the Gem FactoryGirl is quite established for that task.
FactoryGirl.define do
factory :person do
name 'Alice'
birthday { Date.new 1978, 3, 17 }
association :address, strategy: :build
end
end
The factory in action:
# Test with RSpec
describe Person do
subject { build(:person) }
it { is_expected.to be_valid }
end
Test objects with predictable data
Test objects should have predictable data. Then and only then the test evidence has a value and is reproducible.
Test objects with random data are more a sign of uncertainty, if all test cases are completely covered.
That is why gems like Faker, generating random data should not be used in the test environment:
FactoryGirl.define do
factory :person do
# bad
name Faker::Name.first_name
end
end
If setting the data explicitly in the test itself improves the test comprehension, then it should be done. Even if it was already defined in the factory:
describe Person do
subject { build(:person) }
describe '#adult?' do
context 'when older than 19 years' do
it 'returns true' do
subject.birthday = 19.years.ago
expect(subject.adult?).to be true
end
end
end
end
Modularize test objects
The more test cases, the more likely test objects of the same type have to have different data. Creating a factory for each test case is awkward.
FactoryGirl hast traits for that. A trait is the possibility to set different data, but adopt all other:
FactoryGirl.define do
factory :person do
name 'Alice'
trait :with_jobs do
jobs { build_list(:job, 1) }
end
end
end
and then:
build(:person).jobs
# => #<ActiveRecord::Associations::CollectionProxy []>
or:
build(:person, :with_jobs).jobs
# => #<ActiveRecord::Associations::CollectionProxy [#<Job id: nil, name: "Egineer">]>
Avoid touching the database
In some tests database accesses are necessary, but they slow down the test suite. Doing TDD (Test Driven Development), slow tests are cumbersome. Needless database accesses should be avoided. Once again, if it is costly to test a case, then the implementation probably is not optimal.
If there really is the need for persistence, then as late as possible in the test cycle.
So neither in the factory:
# Schlecht
factory :person do
association :address
end
build(:person).address.new_record? # => false
# Gut
factory :person do
association :address, strategy: :build
end
build(:person).address.new_record? # => true
and also not in the test hooks:
# Schlecht
describe Person do
subject { create(:person) }
end
# Gut
describe Person do
subject { build(:person) }
end
but as late as possible:
describe Person do
subject { build(:person) }
describe '.adult' do
it 'returns adult people' do
subject.birthday = 18.years.ago
subject.save
create(:person, birthday: 17.years.ago)
expect(Person.adult).to eq([subject])
end
end
end
That is how the test performance can be improved. Besides side effects to other tests can be avoided, because the persisted test objects are reasonable.
Stubbing access to external systems
In the tests, accesses to external systems should not be executed for several reasons. They are slow in any case. And they can be problematic, because the external system is productive. Besides the access does not add any value to the test in most cases. The goal is not to test data, but behaviour.
It makes sense to build mocks as external systems or stub the accessing methods out:
describe ExternalArticleCollector do
subject { described_class.new }
describe '#fetch' do
before do
WebMock.stub_request(:get, 'http://www.chrisrolle.com/blog')
.to_return(body: '{ "articles": [] }')
end
it 'returns JSON' do
expect(subject.fetch).to be_kind_of(JSON)
end
end