Ruby on Rails: Erste Schritte: Testen
Viele Entwickler, die noch nie mit automatischen Tests gearbeitet haben, werden vielleicht einwenden, dass es noch nichts zu prüfen gibt. Aber gerade die ach so einfachen aber doch fehlerhaften Codeteile verzögern die Fehlersuche bei komplizierten Komponenten unnötig. Wir werden ganz bewusst einfache Tests definieren.
Ruby on Rails bietet mehrere Möglichkeiten, um verschiedene Komponenten einer Web-Anwendung zu überprüfen. Dazu gehören:
- Unit-Tests - Funktionalität einzelner Modelle (ActiveRecords) prüfen
- Functional-Tests - Ablauf der Steuerung (Controller) prüfen
- Integration-Tests - Zusammenspiel von mehreren Steuerungen prüfen
Da wir bislang nur das activity-Modell entwickelt haben, können wir auch nur einen Unit-Test für dieses Modell schreiben.
Wie für die erste Webanwendung müssen wir ein paar Vorarbeiten erbringen
Testdatenbank einrichten
BearbeitenTesten sollte unabhängig vom Entwicklungs- und Produktionssystem ausgeführt werden und - viel wichtiger -, Tests dürfen das Produktionssystem nicht beeinflussen. Grundvoraussetzung ist die Trennung von Produktionsdaten und Testdaten.
In Rails können wir zu diesem Zweck eine unabhängige Testdatenbank einsetzen. In der Datenbankkonfiguration config/database.yml tragen wir eine zusätzliche sqlite-Datenbank ein.
# config/database.yml
development:
adapter: sqlite3
dbfile: db/development.sqlite3
test:
adapter: sqlite3
dbfile: db/test.sqlite3
Diese Einstellung gilt für alle Tests und alle Testdaten werden fortan in dieser Datenbank gespeichert. Ausnahmsweise müssen wir für diese Änderung das Rails-System neu starten.
Datenbankschema übertragen
BearbeitenDa die Testdatenbank unabhängig neben der Entwicklungsdatenbank betrieben wird, verwendet diese natürlich ihre eigenen Datenbankschemata.
Mit rake db:test:clone können wir das Schema der aktuellen Entwicklungsdatenbank in die Testdatenbank übertragen.
rake db:test:clone (in /Users/schuster/Entwicklungen/Sport/sport)
Im Verzeichnis db liegt jetzt die Testdatenbank mit dem Namen test.sqlite3 und wir überprüfen mit sqlite3, ob alles übertragen wurde.
schuster $ sqlite3 db/test.sqlite3 SQLite version 3.1.3 Enter ".help" for instructions sqlite> .tables activities schema_info sqlite> .schema activities CREATE TABLE activities ("id" INTEGER PRIMARY KEY NOT NULL, "description" varchar(255), "day" date); sqlite> .exit
Hiermit sind die Vorarbeiten abgeschlossen und wir kommen zum eigentlichen Testen.
Test anlegen
BearbeitenEin Unit-Test ist im allgemeinen mit genau einem Modell verknüpft. Als wir das activity-Modell mit script/generate modell activity erzeugt hatten, wurde gleichzeitig der dazugehörende Unit-Test angelegt. Die Ausgabe war:
... create app/models/activity.rb create test/unit/activity_test.rb create test/fixtures/activities.yml ...
Der Test besteht aus einer Datenbasis activities.yml, einer Test-Klasse activity_test.rb und dem schon bekannten activity.rb-Modell.
Datenbasis
BearbeitenDie Datenbasis activities.yml enthält beliebig viele Datensätze auf die während eines Tests zugegriffen werden kann. Vor einem Test werden automatisch alle Datensätze in die Testdatenbank eingefügt. Anschließend kann der Test sowohl auf die Daten in der Datenbank als auch auf die original Datensätze in der Datei zugreifen.
Die Datenbasis zu einem Test wird im yml-Format beschrieben und wird gemäß Konvention in der gleichnamigen YML-Datei gespeichert. Ein Test kann aber auch zusätzlich andersnamige Datenbasen verwenden. Dies werden wir in einem folgenden Kapitel nutzen.
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
first:
id: 1
another:
id: 2
Die YML-Syntax ist sehr einfach.
- Wir beginnen mit einem Datensatzbezeichner. Dieser unterscheidet die Datensätze innerhalb der Datenbasis; gehört aber nicht zum Datensatz.
- Es folgt der Datensatz mit seinen Attributen. Jedes Attribut wird in einer eigenen Zeile beschrieben. Zu beginn 2 Leerzeichen, der Attributname, ein Doppelpunkt und abschließend der Attributwert.
Als Datenbasis verwenden wir:
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
first:
id: 1
description: Eine Runde auf dem Trimmpfad
day: 13.1.2006
another:
id: 2
description: Zwei Runden getrabt
day: 20.2.2006
Test-Klasse
BearbeitenIm Unit-Test wird jede Methode, deren Namen mit test_ beginnt, als Test interpretiert.
Der Unit-Test activity_test.rb enthält zu beginn nur den truth-Test. Dies ist kein echter Test - Das Ergebnis ist immer positiv und der Test skizziert nur die Vorgehensweise.
require File.dirname(__FILE__) + '/../test_helper'
class ActivityTest < Test::Unit::TestCase
fixtures :activities
# Replace this with your real tests.
def test_truth
assert true
end
end
Die Test-Klasse zusammen mit allen test_ Methoden bildet einen Unit-Test, der sich auf ein Modell bezieht.
Test ausführen
BearbeitenWir starten diesen Test indem wir
- die Test-Klasse unmittelbar mit
ruby test/unit/activity_test.rb # funktioniert mit Rails 2.x nicht mehr
aufrufen oder
- den Test über das Test-System starten
rake test:units
Solang wir den Test entwickeln, werden wir die erste Variante wählen. Wenn wir später mehrere Unit-Tests erstellt haben, werden wir gelegentlich mit der zweiten Variante prüfen, ob alle Tests positiv verlaufen.
schuster $ ruby test/unit/activity_test.rb Loaded suite test/unit/activity_test Started . Finished in 0.398072 seconds. 1 tests, 1 assertions, 0 failures, 0 errors
In der letzten Zeile sehen wir eine kleine Statistik. Das System hat 1 Testmethode und 1 Zusicherung ausgeführt. Dabei sind 0 Störungen und 0 Fehler aufgetreten.
Test definieren
BearbeitenWie im Kommentar beschrieben, sollen wir zusätzliche Methoden für individuelle Tests anlegen - eine Arbeit, die uns keiner abnehmen kann.
Testbedingungen werden mit Zusicherungen (assertions) beschrieben. Hierbei wird zugesichert, dass alle folgenden Anweisungen nur dann ausgeführt werden, wenn die Zusicherungsbedingung erfüllt ist. Im anderen Fall wird die Test-Methode beendet.
Testen werden wir das Suchen, das Löschen und Einfügen von Datensätzen.
Suchen
BearbeitenDie Datenbasis enthält zwei Datensätze, die in der Testdatenbank enthalten sein müssten. Prüfen wir, ob dies der Fall ist.
require File.dirname(__FILE__) + '/../test_helper'
class ActivityTest < Test::Unit::TestCase
fixtures :activities
# Replace this with your real tests.
def test_truth
assert true
end
def test_find
a = Activity.find 1
assert_kind_of Activity, a
end
end
Die find-Methode von Activity erwartet den id-Wert vom Datensatz den wir suchen. Wenn alles funktioniert, so enthält die Variable a den ersten Datensatz aus der Datenbasis. Die Zusicherung assert_kind_of Activity, a prüft, ob die Variable a einen Datensatz enthält und ob dieser Datensatz vom Typ Activity ist.
Starten wir den Test mit
ruby test/unit/activity_test.rb
Die Ausgabe lautet
schuster $ ruby test/unit/activity_test.rb Loaded suite test/unit/activity_test Started .. Finished in 1.032084 seconds. 2 tests, 2 assertions, 0 failures, 0 errors
Simulieren wir einen Fehler und suchen nach einem nicht hinterlegten Datensatz.
require File.dirname(__FILE__) + '/../test_helper'
class ActivityTest < Test::Unit::TestCase
fixtures :activities
# Replace this with your real tests.
def test_truth
assert true
end
def test_find
a = Activity.find 4
assert_kind_of Activity, a
end
end
Wir erhalten eine Fehlermeldung
schuster $ ruby test/unit/activity_test.rb Loaded suite test/unit/activity_test Started E. Finished in 0.649107 seconds. 1) Error: test_find(ActivityTest): ActiveRecord::RecordNotFound: Couldn't find Activity with ID=4 /opt/pkg/ruby-1.8.4/lib/ruby/gems/1.8/gems/activerecord-1.14.0/lib/active_record/base.rb:955:in `find_one' /opt/pkg/ruby-1.8.4/lib/ruby/gems/1.8/gems/activerecord-1.14.0/lib/active_record/base.rb:941:in `find_from_ids' /opt/pkg/ruby-1.8.4/lib/ruby/gems/1.8/gems/activerecord-1.14.0/lib/active_record/base.rb:382:in `find' test/unit/activity_test.rb:12:in `test_find' 2 tests, 1 assertions, 0 failures, 1 errors
So sollte es auch sein. Setzen wir besser wieder den richtigen Test ein.
Löschen
BearbeitenDatensätze werden mit der destroy-Methode aus der Datenbank gelöscht. Wir suchen also einen Datensatz, löschen diesen und suchen diesen erneut. Anschließend darf der Datensatz nicht mehr in der Datenbank enthalten sein.
def test_destroy
a = Activity.find 2
a.destroy
assert_raise(ActiveRecord::RecordNotFound) do
a = Activity.find 2
end
end
Wir suchen den Datensatz wie zuvor mit find und einer id.
Einfügen
BearbeitenIm Abschnitt Die erste Anwendung hatten wir bereits gesehen, wie wir über die console einen Datensatz speichern. Im Einfüge-Test übernehmen wir diese Anweisung. Nachdem der Datensatz gespeichert wurde, fehlt noch eine Bedingung, welche den Erfolg oder den Misserfolg bestimmt. Auch das kennen wir bereits vom Suchtest.
def test_create
a = Activity.new :description => 'Leichtes warmlaufen', :day => Date.new(2006, 4, 17)
a.save!
a = Activity.find_by_day Date.new(2006, 4, 17)
assert_kind_of Activity, a
end
Auch dieser Test sollte erfolgreich verlaufen und wir gehen zum nächsten Abschnitt.