Testumgebung vorbereiten

Ein Test besteht grundsätzlich aus 3 Schritten.

1.) Arrange 2.) Act 3.) Assert

Im ersten Schritt (Arrange) wird die notwendige Testumgebung hergestellt, um den Test ausführen zu können. Der Aufwand dafür ist je nach Test unterschiedlich groß.
Wenn es allerdings aufwendig ist, die Umgebung herzustellen, liegt das Problem aber wahrscheinlich eher in der Implementierung, als in dem Test. Dann lohnt es sich, darüber nachzudenken, warum es so aufwendig ist.
Grundsätzlich sollten nur Objekte erzeugt werden, die auch wirklich für den Test notwendig sind. Idealerweise ist das nur ein Objekt: das Test Objekt.

Factories für Test Objekte

Wenn das Erzeugen eines Test Objektes einen gewissen Aufwand erfordert, macht es Sinn Factories zu verwenden.
Das macht das Pflegen von Test Objekten einfach. Da ist nur eine Stelle: die Factory. Im Ruby Umfeld hat sich FactoryGirl etabliert für diese Aufgabe:

FactoryGirl.define do
  factory :person do
    name 'Alice'
    birthday { Date.new 1978, 3, 17 }
    association :address, strategy: :build
  end
end

Die Verwendung des Test Objektes aus der Factory:

# Test with RSpec
describe Person do
  subject { build(:person) }

  it { is_expected.to be_valid }
end

Test Objekte mit vorhersehbaren Daten

Test Objekte sollten aus Daten bestehen, die vorhersehbar sind. Nur dann hat die Aussage des Tests einen Wert und ist auch reproduzierbar.
Test Objekte mit zufälligen Daten sind eher ein Zeichen von Unsicherheit, ob die Testfälle komplett abgedeckt sind.
Deshalb sollten Gems wie Faker, die Zufallswerte generieren im Testumfeld nicht verwendet werden:

FactoryGirl.define do
  factory :person do
    # bad
    name Faker::Name.first_name
  end
end

Für das Verständnis eines Tests ist es gut, wenn Daten, die für die Aussage des Tests direkt von Bedeutung sind, auch in dem Test explizit gesetzt werden. Selbst wenn es in der Factory schon definiert wurde:

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

Test Objekte modularisieren

Je mehr Testfälle, desto wahrscheinlicher ist es, dass Test Objekte vom gleichen Typ unterschiedliche Daten haben müssen. Dafür jeweils eine eigene Factory zu erstellen, macht die Sache schnell unübersichtlich.
FactoryGirl hat dafür Traits. Ein Trait ist eine Möglichkeit optional andere Werte zu verwenden, aber alle anderen Werte zu übernehmen:

FactoryGirl.define do
  factory :person do
    name 'Alice'

    trait :with_jobs do
      jobs { build_list(:job, 1) }
    end
  end
end

und dann:

build(:person).jobs
# => #<ActiveRecord::Associations::CollectionProxy []>

oder:

build(:person, :with_jobs).jobs
# => #<ActiveRecord::Associations::CollectionProxy [#<Job id: nil, name: "Egineer">]>

Mit Traits können Factories also die unterschiedlichen Anforderungen abdecken und trotzdem übersichtlich bleiben.

Datenbankzugriffe vermeiden

Datenbankzugriffe in Tests sind manchmal notwendig, aber sie machen eine Testsuite langsam. Allerdings sind langsame Tests bei TDD (Test Driven Development) hinderlich. Also macht es Sinn auf unnötige Datenbankzugriffe zu verzichten. Hierbei zeigt ganz besonders: müssen mehrere Objekte persistiert werden, um einen Test ausführen zu können, dann ist vermutlich die Implementierung nicht optimal.
Sollte dennoch ein Test Objekt persistiert werden müssen, dann möglichst spät im Testzyklus.
Also weder durch die 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

und auch nicht in den Test Hooks:

# Schlecht
describe Person do
  subject { create(:person) }
end

# Gut
describe Person do
  subject { build(:person) }
end

sondern möglichst spät:

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

So wird die Testperformance optimiert. Außerdem werden so Seiteneffekte auf die Tests verhindert, weil sich keine unerwarteten Objekte in der Datenbank befinden.

Zugriffe auf externe System stubben

Im Test sollten Zugriffe auf externe Systeme aus mehreren Gründen möglichst nicht echt ausgeführt werden. In jedem Fall sind Zugriffe auf externe Systeme langsam. Problematisch ist es auch oft, wenn das externe System produktiv ist. Außerdem bietet der Zugriff aus Sicht des Testes meistens keinen Mehrwert, da nicht die Daten, sondern Verhalten getestet werden sollte.
Es macht Sinn für das externe System einen Mock zu haben oder aber die zugreifende Methode zu stubben:

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