Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Geleitwort des Fachgutachters
Einleitung
1 Einführung
2 Installation
3 Erste Schritte
4 Einführung in Ruby
5 Eine einfache Bookmarkverwaltung
6 Test-Driven Development
7 Rails-Projekte erstellen
8 Templatesystem mit ActionView
9 Steuerzentrale mit ActionController
10 Datenbankzugriff mit ActiveRecord
11 E-Mails verwalten mit ActionMailer
12 Nützliche Helfer mit ActiveSupport
13 Ajax on Rails
14 RESTful Rails und Webservices
15 Rails mit Plug-ins erweitern
16 Performancesteigerung
17 Sicherheit
18 Veröffentlichen einer Rails-Applikation auf einem Server
Ihre Meinung?

Spacer
 <<   zurück
Ruby on Rails 2 von Hussein Morsy, Tanja Otto
Das Entwickler-Handbuch
Buch: Ruby on Rails 2

Ruby on Rails 2
geb., mit DVD
699 S., 39,90 Euro
Rheinwerk Computing
ISBN 978-3-89842-779-1
Online bestellenPrint-Version Jetzt Buch bestellen
* versandkostenfrei in (D) und (A)
Pfeil 6 Test-Driven Development
  Pfeil 6.1 Was ist TDD?
  Pfeil 6.2 Vorstellung des Projekts
  Pfeil 6.3 Projekt erstellen und konfigurieren
  Pfeil 6.4 Unit-Tests
  Pfeil 6.5 Functional-Tests erstellen
  Pfeil 6.6 Autotest
  Pfeil 6.7 Referenz


Rheinwerk Computing - Zum Seitenanfang

6.4 Unit-Tests  Zur nächsten ÜberschriftZur vorigen Überschrift

Schritt für Schritt werden wir im Folgenden die drei Models mit der TDD-Technik erstellen. Das heißt, wir werden wenn möglich vor jedem Code, den wir schreiben, jeweils einen Test entwickeln und dann sukzessive die Funktionalität des Models erweitern.


Rheinwerk Computing - Zum Seitenanfang

Erstellung des Country-Models  Zur nächsten ÜberschriftZur vorigen Überschrift

Generierung

Bevor wir einen ersten Test erstellen können, erzeugen wir zunächst das Model Country mit den Feldern code und name, beide vom Typ string, mit Hilfe des Generators, den uns Rails zur Erzeugung eines Models zur Verfügung stellt. Dazu wechseln Sie bitte in das Projektverzeichnis und führen folgenden Befehl aus:

Listing  Generierung des Models country

ruby script/generate model country code:string name:string
...
create  app/models/country.rb
create  test/unit/country_test.rb
create  test/fixtures/countries.yml
create  db/migrate/001_create_countries.rb

Folgende Dateien wurden erstellt:

  • app/models/country.rb
    Die Model-Datei
  • test/unit/country_test.rb
    Die Test-Datei
  • test/fixtures/countries.yml
    Datei mit Testdaten
  • db/migrate/001_create_countries.rb
    Migration-Datei zur Erstellung der Tabelle

Zuerst der Test?
Einige Leser mögen vielleicht anmerken, dass wir hier nicht ganz so vorgehen, wie in der Einleitung beschrieben. Eines der Hauptmerkmale von TDD ist es nämlich, zuerst Test-Code zu entwickeln, bevor wir den eigentlichen Code schreiben. In Rails ist es jedoch sinnvoll, zuerst das Model (oder den Controller) per Generator zu erstellen, da dann automatisch auch die Test-Dateien generiert werden.

Model-Klasse

Wenn Sie die Klasse country.rb im Ordner app/models öffnen, sehen Sie, dass die Klasse von ActiveRecord::Base erbt, es sich also um ein Model handelt, das auf einer Datenbanktabelle basiert:

class Country < ActiveRecord::Base
end

Migration

Der Model-Generator hat im Verzeichnis db/migrate die Migration-Datei 001_create_countries,

in der wir die Felder für die Tabelle countries definieren können, angelegt. Der Eintrag zum Anlegen der Tabelle wurde schon automatisch von Rails vorgenommen. Aber zu der Migration-Datei kommen wir später in diesem Kapitel. Wie im Detail eine Migration funktioniert, erfahren Sie im Abschnitt 10.7

Der erste Unit-Test für das Country-Model

Rails hat uns auch automatisch die Test-Datei country_test.rb im Verzeichnis test/unit angelegt. In dieser Datei können wir unseren Testcode zum Testen des Models Country hinterlegen:

require File.dirname(__FILE__) + '/../test_helper'

class CountryTest < ActiveSupport::TestCase
  # Replace this with your real tests.
  def test_truth
    assert true
  end
end

Zunächst entfernen wir das Testbeispiel (Methode test_truth), das von Rails automatisch erzeugt wurde, um uns zu zeigen, wie eine Testmethode definiert wird und an welcher Stelle wir unseren Code einfügen können.

Unser nächster Schritt wäre jetzt, dass wir die Felder für die Tabelle countries in der Migration-Datei definieren und die Migration ausführen, um die Felder anzulegen.

Da wir aber testgetrieben entwickeln, werden wir zuerst einen Test schreiben, der prüft, was wir erwarten. Wir erwarten, dass die Tabelle countries mit den Feldern name und code existiert, wir also ein Objekt der Klasse Country mit den Attributen name und code erstellen und speichern können. Konkret bedeutet das, dass wir die Methoden new und save auf einem Country-Objekt anwenden können. Den entsprechenden Test dazu formulieren wir wie folgt:

require File.dirname(__FILE__) + '/../test_helper'

class CountryTest < Test::Unit::TestCase

  def test_should_create_a_country
    country = Country.new(:code => "DE", :name => "Germany")
    assert country.save
  end
end
Benennung von Testmethoden
Der Name der Testmethode wird üblicherweise auf Englisch gewählt und beginnt immer mit test_ . Aus dem Namen soll hervorgehen, was diese Methode testet. Kommentare können Sie natürlich auf Deutsch formulieren, wenn Sie das möchten. Es ist üblich, das Wort »should« im Namen der Test-Methode zu verwenden.

assert

In unserem Test erzeugen wir ein neues Country-Objekt mit den Attributen code und name und prüfen, ob dieses Objekt gespeichert werden kann. Diese Prüfung erfolgt mit der Methode assert, die einfach nur prüft, ob der nachfolgende Ausdruck wahr ist. Der Name assert kann mit zusichern übersetzt werden. Das heißt, assert country.save kann wie folgt gelesen werden:

Es soll sichergestellt sein, dass der Country-Datensatz erfolgreich gespeichert werden kann.

Sie können den Test in der Konsole aus dem Projektverzeichnis heraus mit dem Befehl

rake test

ausführen. Sie erhalten folgendes Ergebnis:

You have 1 pending migrations:
     1 CreateCountries
Run 'rake db:migrate' to update your database then try again.

Migration

Wir werden darauf hingewiesen, dass wir das sogenannte Migration-Skript zur Erstellung der Tabelle countries noch nicht ausgeführt haben.

Dazu öffnen Sie bitte die Datei db/migrate/001_create_countries.rb:

class CreateContries < ActiveRecord::Migration
  def self.up
    create_table :countries do |t|
      t.string :code
      t.string :name
      t.timestamps
    end
  end

  def self.down
    drop_table :contries
  end
end
Tests im TextMate-Editor ausführen
Sie können eine Testdatei im TextMate mit Hilfe der Tastenkombination + ausführen. Eine einzelne Testmethode innerhalb einer Datei führen Sie mit Hilfe der Tastenkombination + + aus.

Wie bereits erwähnt, hat Rails das Anlegen der Tabelle countries vorbereitet.

Wir ändern nur die Zeile für das Feld code, da wir nur zwei Zeichen speichern möchten.

Unsere Migration-Datei sollte dann wie folgt aussehen:

class CreateCountries < ActiveRecord::Migration
  def self.up
    create_table :countries do |t|
      t.column :code, :string , :limit => 2
      t.column :name, :string
    end
  end

  def self.down
    drop_table :countries
  end
end

Führen Sie die Migration-Datei mit dem Befehl rake db:migrate aus, um die Datenbanktabelle zu erstellen.

== CreateCountries: migrating ================================
-- create_table(:countries)
   -> 0.0029s
== CreateCountries: migrated (0.0031s) =======================

Wenn Sie jetzt die Tests wieder mit dem Befehl rake test ausführen, erhalten Sie folgende Ausgabe:

Started
.
Finished in 0.073833 seconds.

1 tests, 1 assertions, 0 failures, 0 errors

Die Ausgabe besagt, dass ein Test ausgeführt wurde mit keinem Fehler. Der Test ist also erfolgreich.

Tests zuerst

Das ist die Vorgehensweise bei der testgetriebenen Entwicklung: Zuerst den Test zum nächsten Schritt schreiben, dann den Code schreiben, damit der Test läuft, und erst dann den nächsten Test zum nächsten Schritt schreiben.

Testdaten mit Fixtures anlegen

Unser nächster Schritt ist, dass wir uns um Testdaten für unsere Applikation kümmern. Wir hätten gerne, dass drei Testdatensätze in der Testumgebung hinterlegt werden. Den Test dazu formulieren wir in der Methode test_should_have_3_fixtures . Fixture ist der Fachbegriff für Testdaten. Beim Erzeugen des Models hat Rails automatisch die Datei test/fixtures/countries.yml mit zwei Testdatensätzen angelegt.

assert oder assert_equal

Sie haben zwei Möglichkeiten zu überprüfen, ob drei Testdatensätze vorhanden sind. Entweder Sie prüfen mit assert, ob Country.count == 3 ist, oder Sie setzen die Methode assert_equal ein, die als Parameter den Soll- und den Ist-Wert erwartet, die sie dann auf Gleichheit überprüft. Wir entscheiden uns für die Prüfung mit assert_equal:

def test_should_have_3_fixtures
  assert_equal 3, Country.count
end

Wenn Sie die Tests mit rake test ausführen, erhalten Sie folgende Fehlermeldung:

1) Failure:
test_should_have_3_fixtures(CountryTest)
<3> expected but was
<2>.

Wie bereits erwähnt, wurden von Rails zwei Testdatensätze angelegt. Diese Daten werden über den Aufruf fixtures :countries in der Testdatei geladen. Da wir aber drei erwarten, kommt es zu dieser Fehlermeldung.

Das heißt, wir müssen einen weiteren Testdatensatz anlegen. Dazu öffnen wir das Fixture countries.yml im Verzeichnis test/fixtures:

one:
  code: MyString
  name: MyString

two:
  code: MyString
  name: MyString

YAML

Die Testdaten sind im YAML-Format geschrieben, einem Textformat, das über Einrückungen formatiert wird. Da zum Zeitpunkt der Generierung des Models von Rails nur das Feld id vorgesehen war und Rails nicht wissen konnte, welche Felder wir noch hinzufügen werden, sind die Testdaten unvollständig, weil die Werte für code und name fehlen. Außerdem benennt Rails die Datensätze standardmäßig nach ihrer Position. Das alles werden wir ändern bzw. anpassen und einen dritten Datensatz hinzufügen:

germany:
  code: DE
  name: Germany
japan:
  code: JP
  name: Japan
usa:
  code: US
  name: United States of America

Wenn wir jetzt die Tests wieder ausführen, laufen sie fehlerfrei.

def test_should_destroy
  assert_equal 3, Country.count
  Country.find(:first).destroy
  assert_equal 2, Country.count
end

def test_should_have_3_fixtures
  assert_equal 3, Country.count
end

Der Test schlägt nicht fehl, was beweist, dass die Fixtures vor dem Ausführen jeder Testmethode neu geladen werden.

Nicht testen, was schon getestet wurde
Es ist nicht notwendig, alle Methoden des Country-Objekts zu testen. Zum Beispiel wurde die Methode destroy von den Railsentwicklern entwickelt und getestet. Das heißt, wir testen solche Methoden nicht erneut, sondern nur noch die Methoden, die für unsere Applikation spezifisch sind. Daher hätten wir die Testmethode test_should_destroy auch weglassen können.

Testen von Pflichtfeldern

Pflichtfelder

Was erwarten wir noch von unserem Country-Objekt? Unser Country-Objekt soll Pflichtfelder haben, die gesetzt sein müssen, bevor es gespeichert werden kann. Zur Zeit können wir nämlich auch Country-Objekte mit leeren Werten speichern. Unser Country-Objekt soll jedoch sowohl einen Wert im code -Feld als auch einen Wert im name -Feld haben, bevor wir es speichern können. Im Test können wir das so formulieren, dass wir ein Country-Objekt ohne den code zu setzen erzeugen und dann erwarten, dass dieses Objekt nicht gespeichert werden kann:

def test_should_require_code
  country = Country.new(:name => "Germany")
  assert !country.save
end

Wenn wir diesen Test ausführen, erhalten wir folgende Fehlermeldung:

1) Failure:
test_should_require_code(CountryTest)
<false> is not true.

Um unsere Erwartung im Test erfüllen zu können, müssen wir im Model das Feld code als Pflichtfeld angeben. Dazu steht die Methode validates_presence_of zur Verfügung, der wir den entsprechenden Feldnamen übergeben:

class Country < ActiveRecord::Base
  validates_presence_of :code
end

Jetzt laufen unsere Tests wieder fehlerfrei. Da wir auch möchten, dass das Feld name ein Pflichtfeld ist, müssen wir eine Testmethode definieren, die darauf prüft, dass ein Country-Objekt ohne name nicht gespeichert wird:

def test_should_require_name
  country = Country.new(:code => "DE")
  assert !country.save
end

Damit der Test fehlerfrei läuft, müssen wir das Feld name als Pflichtfeld im Model Country definieren:

class Country < ActiveRecord::Base
  validates_presence_of :code
  validates_presence_of :name
end

Pflichtfelder testen

Eine weitere Möglichkeit, auf Pflichtfelder zu prüfen, wäre zu testen, ob ein Fehler vorliegt. Dazu gibt es mehrere Techniken. Die einfachste Möglichkeit ist aber die, zu prüfen, ob das Speichern fehlschlägt oder nicht.

Uns ist es noch zu wenig, dass code ein Pflichtfeld ist. Wir möchten auch prüfen, ob genau zwei Zeichen verwendet wurden und nicht etwa eins oder drei. Dazu fügen wir eine weitere Testmethode hinzu, in der wir die beiden nicht zulässigen Fälle definieren:

def test_should_require_2_characters_in_code
  country = Country.new(:code => "D", :name => "Germany")
  assert !country.save
  country2 = Country.new(:code => "DES", :name => "Germany")
  assert !country2.save
end

Man hätte den Test alternativ auch in zwei Testmethoden aufteilen können.

validates_ length_of

Der Test schlägt erwartungsgemäß fehl, da wir ja im Model noch nicht definiert haben, dass die Länge des Feldes code zwei Zeichen betragen soll. Wenn wir das mit Hilfe der Methode validates_length_of nachholen, laufen unsere Tests fehlerfrei:

class Country < ActiveRecord::Base
  validates_presence_of :code
  validates_presence_of :name
  validates_length_of :code, :is => 2
end

Die Frage, die sich jetzt stellt, ist, ob wir noch den Eintrag validates_presence_of :code benötigen. Da wir testgetrieben entwickeln, können wir das einfach ausprobieren. Wenn wir die Tests danach starten, werden sie uns sagen, wenn wir etwas falsch gemacht haben.

In diesem Fall laufen unsere Tests auch dann noch fehlerfrei, wenn wir den Eintrag validates_presence_of :code aus dem Model Country entfernen.

Eine weitere Anforderung an unser Country-Objekt ist, dass der code eindeutig sein soll. Das heißt, es soll keine zwei Länder geben, die mit dem gleichen code gespeichert werden können. Dazu legen wir folgende Testmethode an:

def test_code_should_be_unique
  country2 = Country.new(:code => "DE",
			 :name => "Deutschland")
  assert !country2.save
end

Da wir bereits ein Land mit dem Code »DE« in den Fixtures definiert haben und diese vor jeder Ausführung einer Test-Methode ausgeführt werden, sollte das Anlegen eines weiteren Datensatzes mit dem Code »DE« fehlschlagen.

validates_ uniqueness_ of

Wenn wir diesen Test ausführen, schlägt er erwartungsgemäß fehl, da es zur Zeit möglich ist, zwei Datensätze mit gleichem code zu speichern. Damit das nicht mehr möglich ist, müssen wir im Model den Eintrag vornehmen, dass das Feld code eindeutig sein muss. Dazu verwenden wir die Methode validates_uniqueness_of :code in der Country-Methode:

class Country < ActiveRecord::Base
  validates_presence_of :name
  validates_length_of :code, :is => 2
  validates_uniqueness_of :code
end

Wenn wir diesen Eintrag vornehmen und dann die Tests ausführen, stellen wir fest, dass der Test immer noch fehlschlägt. Diesmal jedoch nicht wegen der neuen Testmethode, sondern wegen der Testmethode test_should_create_a_country.

def test_should_create_a_country
  country = Country.new(:code => "DE", :name => "Germany")
  assert country.save
end

Das liegt daran, dass vor dem Ausführen jeder Testmethode die Fixtures neu geladen werden und es in den Fixtures bereits einen Eintrag gibt mit dem code »DE«. Deshalb müssen wir entweder die Fixtures ändern oder innerhalb der Testmethoden einen anderen code wählen. Wir möchten innerhalb der Testmethoden ein anderes Land wählen. Da wir aber an vielen Stellen ein neues Country-Objekt erzeugen, ist es besser, die Beispielwerte bzw. die Testdaten, die dem Objekt übergeben werden, an eine zentrale Stelle zu legen.

Hilfsmethode für Tests erstellen

Am Anfang unserer Testdatei wird eine Helper-Datei geladen:

require File.dirname(__FILE__) + '/../test_helper'

valid_country_ attributes

Diese helper-Datei können wir nutzen, um dort eine Hilfsmethode valid_country_attributes zu definieren, die gültige Werte zum Erzeugen eines Country-Objekts zurückliefert. Die helper-Datei befindet sich im Ordner test und sieht am Ende der Datei einen Bereich vor, in dem man eigene Methoden definieren kann:

def valid_country_attributes
  {
    :code => "EG",
    :name => "Egypt"
  }
end

Damit wir, wenn nötig, die Werte in dieser Methode ganz leicht ändern können, legen wir einen optionalen Hash add_attributes als Parameter an, über den die Werte in der Methode überschrieben werden und/oder neue Werte hinzugefügt werden können:

def valid_country_attributes(add_attributes={})
  {
    :code => "EG",
    :name => "Egypt"
  }.merge(add_attributes)
end

Zum Beispiel würde der Aufruf der Methode

valid_country_attributes(:name => "Aegypten")

die Werte :code => "EG" und :name => "Aegypten" zurückliefern.

Unsere Helper-Methode valid_country_attributes können wir in den Testmethoden wie folgt einsetzen:

require File.dirname(__FILE__) + '/../test_helper'

class CountryTest < Test::Unit::TestCase

  def test_should_create_a_country
    country = Country.new(valid_country_attributes)
    assert country.save , "country could not be saved"
  end

  def test_should_have_3_fixtures
    assert_equal 3, Country.count
  end

  def test_should_require_code
    country = Country.new(
	      valid_country_attributes(:code => nil))
    assert !country.save
  end

  def test_should_require_name
    country = Country.new(
    	      valid_country_attributes(:name => nil))
    assert !country.save
  end

  def test_should_require_2_characters_in_code
    country = Country.new(
              valid_country_attributes(:code => "E"))
    assert !country.save
    country2 = Country.new(
               valid_country_attributes(:code => "EGT"))
    assert !country2.save
  end

  def test_code_should_be_unique
    country = Country.new(valid_country_attributes)
    assert country.save
    country2 = Country.new(valid_country_attributes)
    assert !country2.save
  end

end

Der Trick mit der Hilfsmethode valid_country_attributes erhöht auch die Lesbarkeit des Testcodes. In der Methode test_should_require_name z. B. steht :name=> nil, um anzugeben, dass der Name nicht mit einem Wert belegt werden soll.

Damit haben wir das Model Country ausreichend getestet. Wir haben intensiv gezeigt, dass man im Test-Driven Development die Anforderungen Schritt für Schritt formuliert, ausführt und den Code der Applikation anpasst, bis die Tests fehlerfrei laufen, und dann eventuell wieder eine neue Anforderung formuliert.


Rheinwerk Computing - Zum Seitenanfang

Erstellung des Airport-Models  Zur nächsten ÜberschriftZur vorigen Überschrift

code 3-stellig

Da das Model Airport auch die Felder namen und code hat, sehen die Tests fast genauso aus wie für das Model Country, bis auf den kleinen Unterschied, dass der code 3-stellig ist. Das zusätzliche Feld country_id für die 1:n-Relation ignorieren wir zunächst.

Wir generieren das Model Airport mit dem folgenden Befehl:

ruby script/generate  model airport code:string name:string

Da der Airportcode dreistellig ist ändern wir die automatisch erzeugte Migration-Datei db/migrate/002_create_airports.rb wie folgt ab:

class CreateAirports < ActiveRecord::Migration
  def self.up
    create_table :airports do |t|
      t.string :code
      , :limit => 3
      t.string :name

      t.timestamps
    end
  end

  def self.down
    drop_table :airports
  end
end

Anschließend führen wir die Migration-Datei mit rake db:migrate aus:

rake db:migrate
== 2 CreateAirports: migrating ==============
-- create_table(:airports)
   -> 0.0705s
== 2 CreateAirports: migrated (0.0711s) =====

Die Tests erzeugen wir auch wieder, indem wir zuerst den Test schreiben und dann den Code anpassen. Das Ergebnis sieht dann wie folgt aus:

require File.dirname(__FILE__) + '/../test_helper'

class AirportTest < Test::Unit::TestCase

  def test_should_create_a_airport
    airport = Airport.new(valid_airport_attributes)
    assert airport.save , "airport could not be saved"
  end

  def test_should_have_4_fixtures
    assert_equal 4, Airport.count
  end

  def test_should_require_code
    airport = Airport.new(
	      valid_airport_attributes(:code => nil))
    assert !airport.save
  end

  def test_should_require_name
    airport = Airport.new(
	      valid_airport_attributes(:name => nil))
    assert !airport.save
  end

  def test_should_require_3_characters_in_code
    airport = Airport.new(
	      valid_airport_attributes(:code => "CA"))
    assert !airport.save
    airport2 = Airport.new(
	       valid_airport_attributes(:code => "CAIR"))
    assert !airport2.save
  end

  def test_code_should_be_uniq
    airport = Airport.new(valid_airport_attributes)
    assert airport.save
    airport2 = Airport.new(valid_airport_attributes)
    assert !airport2.save
  end
end

In der Test-Helper-Datei test/test_helper fügen wir noch folgende Hilfsmethode hinzu:

Listing  test/test_helper

...
class Test::Unit::TestCase
  ...
  def valid_airport_attributes(add_attributes={})
    {
      :code => "CAI",
      :name => "Cairo International Airport"
    }.merge(add_attributes)
  end
end

Fixtures

Wir haben folgende Fixtures in der Datei airports.yml angelegt:

Listing  test/fixtures/airports.yml

dus:
  code: DUS
  name: Düsseldorf International Airport
muc:
  code: MUC
  name: Munich International Airport
hnd:
  code: HND
  name: Tokyo International Airport
sfo:
  code: SFO
  name: San Francisco International Airport

Wenn wir die Tests ausführen, erhalten wir 4 Fehler. Durch Hinzufügen der folgenden validates -Methoden werden die Fehler behoben.

Listing  app/models/airport.rb

class Airport < ActiveRecord::Base
  validates_presence_of :name
  validates_length_of :code, :is => 3
  validates_uniqueness_of :code
end

Relationen zwischen Models

Wir möchten nun eine Relation zwischen unseren beiden Models Airport und Country erstellen. Ein Airport gehört zu genau einem Country und ein Country, kann beliebig viele Airports besitzen.

Schön wäre doch, wenn wir Folgendes schreiben könnten, um den Namen des zugehörigen Landes zu einem Airport auszugeben:

airport.country       # Country Objekt zum Airport
airport.country.name  # Name des Country zum gegebenen Airport

assert_respond_to

Mit der Methode test_should_respond_to_airport möchten wir zunächst nur testen, ob wir die Methode country auf das Airport-Objekt anwenden können. Dazu kann man die Assert-Methode assert_respond_to verwenden:

def test_should_respond_to_country
  airport = airports(:dus)
  assert_respond_to airport, :country
end

Der Befehl airports(:dus) lädt das Objekt mit dem Fixture-Namen :dus, das wir in der Fixutre-Datei airports.yml definiert haben. Dieser Befehl steht nur in der Testumgebung zur Verfügung.

Der Test schlägt erwartungsgemäß fehl.

Damit der Test erfolgreich ist, müssen wir Folgendes durchführen:

  • belongs_to im Model Airport angeben:
    class Airport < ActiveRecord::Base
      validates_presence_of :name
      validates_length_of :code, :is => 3
      validates_uniqueness_of :code
      belongs_to :country
    end
  • Neues Feld country_id zur Tabelle airports hinzufügen:

    Dazu legen wir eine Migration-Datei mit dem migration-Generator an, die das Feld country_id vom Typ integer hinzufügt.

    script/generate migration AddCountryIdToAirports \
    country_id:integer

    Die generierte Migration-Datei können wir dann mit rake db:migrate ausführen.

  • Test-Helper-Datei ergänzen

    Ergänzen Sie unsere Test-Helper-Datei test/test_helper wie folgt:

    ... 
    def valid_airport_attributes(add_attributes={})
      {
        :code => "CAI",
        :name => "Cairo International Airport",
        :country_id => 1
      }.merge(add_attributes)
    end

Tests erfolgreich

Wenn wir den Test mit rake test ausführen, sollten alle Tests erfolgreich laufen.

In einem weiteren Test möchten wir überprüfen, ob zu einem Airport auch der Name des zugehörigen Landes erfolgreich ausgelesen werden kann. Wir setzen dazu die Assert-Methode assert_equal ein, die überprüft, ob ein Soll-Wert (in unserem Fall »Germany«) auch tatsächlich ermittelt wurde.

def test_should_get_country_name
  airport = airports(:dus)
  assert_equal "Germany", airport.country.name
end

Die Ausführung des Tests ergibt einen Fehler:

1) Error:
test_should_get_country_name(AirportTest):
NoMethodError: You have a nil object when you didn't expect
it! The error occurred while evaluating nil.name
./test/unit/airport_test.rb:45:in `test_should_get_country_name'

14 tests, 16 assertions, 0 failures, 1 errors

Fixtures

Die Ursache für den Fehler liegt in den Fixtures. Es fehlt nämlich die Information für die Verknüpfung zwischen den Airport-Objekten und den Country-Objekten.

Rails 2 vereinfacht die Sache erheblich, indem man jetzt einfach den Namen des Fixtures aus dem anderen Fixture angibt. Wir ergänzen dazu das Fixture airports.yml wie folgt:

dus:
  code: DUS
  name: Düsseldorf International Airport
  country: germany
muc:
  code: MUC
  name: Munich International Airport
  country: germany
hnd:
  code: HND
  name: Tokyo International Airport
  country: japan
sfo:
  code: SFO
  name: San Francisco International Airport
  country: usa 

Zu beachten ist, dass die Bezeichnungen germany, japan und usa den Namen der Fixtures aus countries.yml entsprechen.

Umgekehrt wäre es auch interessant, wenn man von Country auf alle Airports zugreifen könnte. Dafür müssen wir folgenden Test für das Model Country implementieren:

def test_should_respond_to_airports
  country = countries(:germany)
  assert_respond_to country, :airports
end

Dieser Test scheitert, bis wir dem Model Country die Information geben, dass es zu mehreren Airports gehört:

Foxy Fixtures

Vor Rails 2 hätten wir jeweils die ID des zugehörigen Country-Objekts angeben müssen, wie folgendes Beispiel zeigt.

# Fixture countries.yml
germany:
  id: 1
  code: DE
  name: Germany
...
# Fixture airports.yml
dus:
  id: 1
  code: DUS
  name: Düsseldorf International Airport
  country_id: 1
...

Bei komplexen Relationen kommt man da schnell durcheinander. Zum Glück müssen wir uns in Rails 2 nicht mehr um die IDs kümmern.

# Fixture countries.yml
germany:
  code: DE
  name: Germany
...
# Fixture airports.yml
dus:
  code: DUS
  name: Düsseldorf International Airport
  country: germany
...

Dieses neue Feature wird als »Foxy Fixtures« bezeichnet. Ein weiteres Feature ist, dass die Datumsfelder created_* und updated_* automatisch auf die aktuelle Uhrzeit gesetzt werden.

class Country < ActiveRecord::Base
  validates_presence_of :name
  validates_length_of :code, :is => 2
  validates_uniqueness_of :code

  has_many :airports
end

Dabei ist zu beachten, dass man bei einem has_many Plural bildet und beim belongs_to Singular.

Relationen testen

Wir könnten sogar basierend auf den Fixtures überprüfen, zu wie vielen Airports ein bestimmtes Land gehört. Dazu müssten wir unseren Test wie folgt anpassen:

def test_should_respond_to_airports
  country = countries(:germany)
  assert_respond_to country, :airports
  assert_equal 2, country.airports.count
end

Das heißt, Relationen sind in Rails sehr einfach über die Formulierung der Verknüpfung zu realisieren. Durch die Tests stellen wir sicher, dass wir diese Formulierungen (has_many und belongs_to) auch vornehmen.

Wir müssen allerdings bedenken, dass wir uns in unseren Tests von den Fixtures abhängig gemacht haben. Das heißt, immer dann, wenn wir etwas an den Fixtures ändern, besteht die hohe Wahrscheinlichkeit, dass danach unsere Tests nicht mehr fehlerfrei laufen. Wir müssen also wachsam mit unseren Fixtures umgehen.

Es wäre doch sinnvoll, wenn die country_id in der Tabelle airports auch ein Pflichtfeld wäre, das bei der Generierung eines neuen Airport-Objektes gesetzt sein muss. Den dazugehörigen Test kennen wir schon von den anderen Pflichfeldern, die wir haben. Allerdings ist zu beachten, dass wir dieses Feld auch in unserer Hilfsmethode valid_airport_attributes setzen:

def test_should_require_country_id
  airport = Airport.new(
	    valid_airport_attributes(:country_id => nil))
  assert !airport.save
end

Fehlerfreie Tests

Der Test scheitert. Wenn wir den Eintrag validates_presence_of :country_id im Model Airport vornehmen, laufen unsere Tests fehlerfrei.


Rheinwerk Computing - Zum Seitenanfang

Erstellung des Flight-Models  topZur vorigen Überschrift

Für die Erstellung des dritten Models Flight werden wir jetzt nicht den model -Generator verwenden, sondern den scaffold -Generator. Der scaffold -Generator generiert nämlich nicht nur den Model mit der passenden Migration-Datei, sondern auch einen Controller mit Views, um die Datensätze per Webbrowser zu verwalten, und die Testdateien. Im Prinzip enthält der scaffold -Generator den model -Generator, nur dass auch noch der passende Controller mit Views und die Tests generiert werden.

ruby script/generate scaffold flight \
  nr:string departure_datetime:datetime \
  arrival_datetime:datetime \
  departure_airport_id:integer \
  arrival_airport_id:integer

Der Backslash am Ende der Zeilen dient nur zum Umbrechen des einzeiligen Befehls in mehrere Zeilen. Sie können den Befehl auch ohne den Backslash in einer Zeile durchschreiben.

Folgende Dateien werden erstellt:

create  app/views/flights
create  test/functional/
create  app/views/flights/index.html.erb
create  app/views/flights/show.html.erb
create  app/views/flights/new.html.erb
create  app/views/flights/edit.html.erb
create  app/views/layouts/flights.html.erb
create  public/stylesheets/scaffold.css
dependency  model
create  app/models/flight.rb
create  test/unit/flight_test.rb
create  test/fixtures/flights.yml
create  db/migrate/004_create_flights.rb
create  app/controllers/flights_controller.rb
create  test/functional/flights_controller_test.rb
create  app/helpers/flights_helper.rb
route  map.resources :flights

Den generierten Controller mit Views und den passenden Functional-Test werden wir im nächsten Abschnitt behandeln.

Migration

Mit dem Befehl rake db:migrate werden wir zunächst die generierte Migration-Datei 004_create_flight ausführen. Eine Bearbeitung der Migration-Datei ist nicht notwendig, da wir beim Aufruf des Generators bereits alle Felder angegeben haben.

rake db:migrate
== 4 CreateFlights: migrating ============
-- create_table(:flights)
   -> 0.3016s
== 4 CreateFlights: migrated (0.3018s) ===

Wenn wir den Befehl rake test ausführen, werden sowohl die Unit-Tests als auch die Functional-Tests ausgeführt. Da der scaffold -Generator auch Functional-Tests erstellt hat und wir uns zunächst noch ausschließlich auf Unit-Tests konzentrieren möchten, verwenden wir den folgenden Befehl: rake test:units . Der Test sollte erfolgreich ausgeführt werden.

Test-Helper

In der Testhelper-Datei werden wir wie bei den anderen Models eine Hilfsmethode erstellen:

...
class Test::Unit::TestCase
  ...
  def valid_flight_attributes(add_attributes={})
    {
      :nr => "RA123",
      :departure_datetime => Time.parse("2008-08-30 12:50"),
      :arrival_datetime => Time.parse("2008-08-30 13:50"),
      :departure_airport_id => airports(:dus).id,
      :arrival_airport_id => airports(:muc).id
    }.merge(add_attributes)
  end
end

Das Model soll folgende Funktionalität erfüllen:

  • Ein Flug soll korrekt gespeichert werden können
    Man sollte ein Flug mit sämtlichen Feldern erstellen können.
  • Die Flugnummer (Feld nr ) soll ein Pflichtfeld sein
    Ein Flug ohne nr sollte nicht gespeichert werden können.
  • Der Abflughafen sollte vom Typ Airport sein
    Es soll möglich sein, über den folgenden Befehl auf ein Airport-Objekt zugreifen zu können: flight.departure_airport. Wir prüfen, ob das Ergebnis vom Typ Airport ist, bzw. ob das Ergebnis eine Instanz der Airport-Klasse ist.
  • Der Ankunftsflughafen sollte vom Typ Airport sein
    Der Aufruf flight.departure_airport sollte auch vom Typ Airport sein.
  • Der Code des Abflughafen soll ausgegeben werden können
    Wir prüfen, ob der Code des Abflughafens DUS ist.
  • Der Code des Ankunftsflughafen soll ausgegeben werden können
    Wir prüfen, ob derCode des Ankunftsflughafens MUC ist.

Selbstverständlich wäre es auch sinnvoll, die weiteren Felder ebenfalls als Pflichtfelder zu fordern. Wir belassen es jedoch bei dem Feld nr, da die anderen Felder analog als Pflichtfelder definiert werden können.

Alle Test- methoden

Anstatt nun zunächst nur den Test für die erste Funktionalität zu erstellen und anschließend mit der Implementierung der Funktionalität fortzufahren, präsentieren wir hier bereits alle Testmethoden, um schneller in diesem Kapitel vorgehen zu können. In der Praxis sollten Sie jedoch Schritt für Schritt vorgehen.

Listing  test/unit/flight_test.rb

require File.dirname(__FILE__) + '/../test_helper'

class FlightTest < ActiveSupport::TestCase
  def setup
    @flight = Flight.new(valid_flight_attributes)
  end

  def test_should_create_a_flight
    assert @flight.save
  end

  def test_should_require_nr
    flight = Flight.new(valid_flight_attributes(:nr => nil))
    assert !flight.save
  end

  def test_departure_airport_should_be_an_airport
    assert_instance_of Airport, @flight.departure_airport
  end

  def test_arrival_airport_should_be_an_airport
    assert_instance_of Airport, @flight.arrival_airport
  end

  def test_should_get_the_name_of_the_departure_airport
    assert_equal "DUS", @flight.departure_airport.code
  end

  def test_should_get_the_name_of_the_departure_airport
    assert_equal "MUC", @flight.arrival_airport.code
  end
end

setup

Das Besondere an dieser Testklasse ist die Methode setup . Diese Methode wird vor Ausführung jeder Testmethode ausgeführt. In unserem Beispiel wird ein neues flight -Objekt in eine Instanzvariable gespeichert.

teardown

Wenn in der Testklasse eine Methode mit dem Namen teardown vorkommt, so wird diese Methode nach Abarbeitung jeder Testmethode ausgeführt. Diese Methode wird verwendet, um Aufräumarbeiten, wie z. B. das Schließen von Netzwerkverbindungen oder das Löschen von temporären Verbindungen, zu löschen.

Fixtures

Selbstverständlich schlagen die Tests fehl. Um den Test erfolgreich zu machen, erstellen wir Fixtures und passen das Model an:

Listing  test/fixtures/flights.yml

dus_muc:
  nr: RA447
  departure_datetime: 2008-06-10 16:10:00
  arrival_datetime: 2008-06-10 17:10:00
  departure_airport: dus
  arrival_airport: muc

muc_dus:
  nr: RA448
  departure_datetime: 2008-06-11 9:50:00
  arrival_datetime: 2008-06-11 10:50:00
  departure_airport: muc
  arrival_airport: dus

Das Model flight.rb implementieren wir wie folgt:

Listing  app/models/flight.rb

class Flight < ActiveRecord::Base
  validates_presence_of :nr
  belongs_to :departure_airport, :class_name => "Airport"
  belongs_to :arrival_airport, :class_name => "Airport"
end

Keine Fehler mehr

Die Ausführung der Unit-Tests mit rake test:units sollte nun keinen Fehler mehr liefern

rake test:units
...

19 tests, 26 assertions, 0 failures, 0 errors

Wir haben damit die Erstellung der Models abgeschlossen und kommen nun zu den Controllern und Views, die wir mit den sogenannten Functional-Tests testen werden.



Ihre Meinung

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.

 <<   zurück
  Zum Rheinwerk-Shop
Zum Rheinwerk-Shop: Ruby on Rails 2
Ruby on Rails 2
Jetzt Buch bestellen
 Ihre Meinung?
Wie hat Ihnen das Openbook gefallen?
Ihre Meinung

 Buchtipps
Zum Rheinwerk-Shop: Ruby on Rails 3.1






 Ruby on Rails 3.1


Zum Rheinwerk-Shop: Responsive Webdesign






 Responsive Webdesign


Zum Rheinwerk-Shop: Suchmaschinen-Optimierung






 Suchmaschinen-
 Optimierung


Zum Rheinwerk-Shop: JavaScript






 JavaScript


Zum Rheinwerk-Shop: Schrödinger lernt HTML5, CSS3 und JavaScript






 Schrödinger lernt
 HTML5, CSS3
 und JavaScript


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
InfoInfo




Copyright © Rheinwerk Verlag GmbH 2008
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern