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

Bearbeiten

Testen 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

Bearbeiten

Da 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

Bearbeiten

Ein 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

Bearbeiten

Die 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

Bearbeiten

Im 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

Bearbeiten

Wir 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

Bearbeiten

Wie 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.

Die 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

Bearbeiten

Datensä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

Bearbeiten

Im 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.