6.4 Unit-Tests
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.
Erstellung des Country-Models
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.
Erstellung des Airport-Models
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.
Erstellung des Flight-Models
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.