6.5 Functional-Tests erstellen
Functional Tests testen im Prinzip die Controller und die Views. Jedoch ist zu beachten, dass, wenn ein Model (Unit-getestet) einen Bug hat, der Functional Test fehlschlägt. Daher ist zu empfehlen, vorher sicherzustellen, dass die Unit-Tests einwandfrei laufen.
Mit Functional-Tests können wir z. B. Folgendes konkret testen:
- Testen, ob eine bestimmte Zeichenkette angezeigt wird.
- Testen, ob der Controller ein bestimmtes Template anzeigt.
- Testen, ob der Controller richtig weiterleitet.
- Testen, ob die Seite korrekt geladen worden ist.
- Testen, ob die richtigen Parameter übergeben worden sind.
- Testen, ob das Routing zu dem Controller korrekt funktioniert.
- Testen, ob die richtige Flash-Message angezeigt wird.
- u. v. m.
Die Erstellung von Functional-Tests zeigen wir anhand des Flights-Controllers, den wir bereits im letzten Abschnitt mit dem scaffold -Generator erstellt haben. Der scaffold -Generator hat neben dem Flight-Model auch den passenden Controller und die Views erstellt, um die Flüge (»Flight«) zu verwalten. Folgende Template- und Controller-Dateien wurden u. a. generiert:
- app/views/flights/index.html.erb
Template für die Anzeige aller Flüge
- app/views/flights/show.html.erb
Template zum Anzeigen eines bestimmten Fluges (Detailansicht)
- app/views/flights/edit.html.erb
Formular zum Editieren der Flüge
- app/views/flights/new.html.erb
Formular, um eine neuen Flug anzulegen
- app/views/layouts/flights.html.erb
Legt das Layout für sämtliche Flight-Templates fest. Ist anfangs leer. In der Datei app/views/layouts/application.html.erb kann das Layout für sämtliche Templates festgelegt werden.
- app/controllers/flights_controller.rb
Controller für das Flight-Model. Die Klasse ist die Schaltzentrale zwischen dem Benutzer und dem Model. Der Controller nimmt die Benutzereingaben entgegen und führt die entsprechenden Model-Befehle aus. Der Controller nimmt beispielsweise die Daten für einen neuen Flug an, speichert sie mit Hilfe des Flight-Models und leitet den Benutzer auf die Detailseite des neu erstellten Fluges weiter.
- test/functional/flights_controller_test.rb
Der Functional-Test ist größtenteils schon fertig generiert und leicht erweiterbar.
Der Functional-Test des Flights-Controllers
Wenn Sie sich den Functional-Test flights_controller_test.rb im Verzeichnis test/functional anschauen, werden Sie feststellen, das er bereits vollständig implementiert ist. Hier werden die Grundfunktionen wie Anzeigen, Hinzufügen, Ändern und Löschen, die der Controller zur Verfügung stellt, getestet. Den Functional-Test müssen wir erst dann anpassen, wenn wir besondere Anforderungen implementieren möchten.
Bevor wir den Functional-Test anpassen und ergänzen, werden wir uns zunächst die generierten Testmethoden im Detail anschauen. Die Methoden des Controllers werden im Folgenden auch als Action bezeichnet.
- Testen der Index-Action
def test_should_get_index get :index assert_response :success assert_not_nil assigns(:flights) end
get :index
Die Methode get :index ruft die Index-Action des Flights-Controller auf. Mit assert_response :success wird überprüft, ob die vorherige Aktion erfolgreich ausgeführt werden konnte. Dies ist genau dann der Fall, wenn der HTTP-Rückgabecode 200 (O.K.) ist. Anschließend wird mit assert_not_nil sichergestellt, dass die Instanzvariable @flight mit einem Wert gesetzt ist (nicht nil). Die Instanzvariable wird vom Template index.html.erb zur Anzeige sämtlicher Flight-Objekte verwendet.
- Testen der New-Action
Diese Testmethode ruft die New-Action auf und überprüft, ob der Aufruf erfolgreich war.def test_should_get_new get :new assert_response :success end
- Testen der Create-Action
Diese Testmethode besteht aus zwei Assert-Methoden.Die Erste ist etwas komplexer, da sie einen Block verwendet. Der Befehl post :create, :flight => { } ruft mit der HTTP-Methode POST die Action :create auf, mit dem leeren Parameter flight . Die Assert-Methode assert_difference('Flight.count') überprüft, ob die Anzahl der Flight-Objekte Flight.count sich nach Ausführung des Blocks ( post ) um eins erhöht hat. Im Prinzip wird einfach überprüft, ob die Action create die Anzahl der Flight-Objekte verändert hat. Die zweite Assert-Methode assert_redirected_to überprüft, ob der Controller zur Detailseite ( flight_path ) des Flight-Objektes, das in der Instanzvariable assigns(:flight) gespeichert wurde, weiterleitet.def test_should_create_flight assert_difference('Flight.count') do post :create, :flight => { } end assert_redirected_to flight_path(assigns(:flight)) end
- Testen der show-Action
Die Show-Action wird mit dem Parameter id=flights(:one).id aufgerufen, wobei flights(:one).id die ID des Fixtures mit dem Namen one aus der Fixture-Datei test/fixtures/flights.yml zurückgibt. Da wir jedoch die Fixture-Datei verändert haben, gibt es kein Fixture mehr mit dem Namen one . Wir werden das später korrigieren. Anschließend wird überprüft, ob die Seite erfolgreich, ohne einen Fehler ausgeführt werden konnte.def test_should_show_flight get :show, :id => flights(:one).id assert_response :success end
- Testen der Update-Action
Zunächst wird die Update-Action mit der HTTP-Methode put und mit den Parametern id und flight aufgerufen. Anschließend wird überprüft, ob zur Detailseite des geänderten flight-Objekts weitergeleitet wurde.def test_should_update_flight put :update, :id => flights(:one).id, :flight => { } assert_redirected_to flight_path(assigns(:flight)) end
- Testen der Destroy-Action
Dieser Test funktioniert im Prinzip wie der Test für die Create-Action, nur dass hier die Action destroy des Contollers mit der HTTP-Methode delete aufgerufen und überprüft wird, ob sich die Anzahl der Flight-Objekte nach der Ausführung um eins verringert hat.def test_should_destroy_flight assert_difference('Flight.count', -1) do delete :destroy, :id => flights(:one).id end assert_redirected_to flights_path end
Test korrigieren
Um den Functional-Test auszuführen, verwenden wir entweder den allgemeinen Befehl rake test, um alle Tests auszuführen, oder wir rufen
rake test:functionals
auf, um nur die Functional-Tests auszuführen.
Fünf Fehler
Die Ausgabe liefert fünf verschiedene Fehler:
1) Failure: test_should_create_flight(FlightsControllerTest) ... <3> expected but was <2>. ... 2)Error: test_should_destroy_flight(FlightsControllerTest): StandardError: No fixture with name 'one' found ... 3)Error: test_should_get_edit(FlightsControllerTest): StandardError: No fixture with name 'one' found ... 4)Error: test_should_show_flight(FlightsControllerTest): StandardError: No fixture with name 'one' found ... 5)Error: test_should_update_flight(FlightsControllerTest): StandardError: No fixture with name 'one' found ... ... 7 tests, 4 assertions, 1 failures, 4 errors
Fehlertypen
In der Fehlerausgabe kommen zwei Typen von Fehlern vor, Failure und Error . Diese haben allgemein folgende Bedeutung:
- Failure:
Ein Failure liegt vor, wenn die Ausführung einer Assert-Methode nicht das gewünschte Ergebnis liefert. In unserem Beispiel ist die Assert-Methode assert_difference derMethode test_should_create_flight gescheitert, da diese 3 Datensätze statt 2 Datensätze erwartet hat. Da wir in den Fixtures flights.yml zwei Flüge definiert haben und nach Ausführung der Create-Action immer noch zwei Datensätze vorliegen, bedeutet dies, dass kein neuer flight-Datensatz erstellt werden konnte. Der Grund dafür liegt darin, dass wir die Flugnummer nr im Model als Pflichtfeld festgelegt haben. Wir beheben das Problem, indem wir den Aufruf die Create-Action nicht mit einem leeren flight-Parameter aufrufen, sondern mit Beispielwerten. Wir können dazu die Testhelper- Methode valid_flight_attributes nutzen: Wir ersetzen dazupost :create, :flight => { }
durch
post :create, :flight => valid_flight_attributes
- Error:
Errors liegen vor, wenn ein Ruby bzw. Rails-Befehl fehlgeschlagen ist. In unserem Beispiel erhalten wir vier Errors, die alle dieselbe Ursache haben. In den Fixutures flights.yml haben wir die Bezeichnungen geändert. Um den Fehler zu korrigieren, ersetzen wir in den Testmethoden sämtliche :one durch :dus_muc, wie z. B.:
get :show, :id => flights(:dus_muc).id
Fehlerfrei
Der Befehl rake test:functionals sollte nach den Korrekturen nun keine Errors und Failures mehr liefern.
Fixtures in die Entwicklungsdatenbank laden
Bevor wir weitere Ergänzungen durchführen, starten wir den lokalen Server mit ruby script/server und rufen die URL
http://localhost:3000/flights
auf.
Abbildung http://localhost:3000/flights ohne Fixtures
Beispieldatensätze hinzufügen
Anfangs liegen jedoch noch keine Datensätze vor. Der einfachste Weg Beispieldatensätze hinzuzufügen ist, die Fixtures, die wir für die Tests angelegt haben, in die Entwicklungsdatenbank (development) zu laden.
rake db:fixtures:load
Fixtures einzeln laden |
Es ist zu beachten, dass rake db:fixtures:load alle Tabellen leert, zu denen es Fixtures im Verzeichnis test/fixtures gibt. Anschließend werden die Daten aus den Fixtures in die Datenbanktabellen importiert. In unserem Beispiel werden die Datenbanktabellen countries, airports und flights geleert und mit den Daten aus den entsprechenden Fixtures geladen. Möchte man hingegen nicht alle Fixtures, sondern nur bestimmte Fixtures laden (z. B. nur countries und airports), so kann man den folgenden Befehl aufrufen: rake db:fixtures:load FIXTURES=countries,airports |
Ein erneuter Aufruf der Seite http://localhost:3000/flights zeigt, dass die Fixtures korrekt geladen worden sind.
Abbildung http://localhost:3000/flights mit Fixtures
Fixtures eignen sich daher also nicht nur für die Testklassen, sondern auch, um auf einfache Weise Testdaten zur Verfügung zu stellen.
Anzeigen der Flughafen-Codes
Als Erstes möchten wir gerne die Liste der Flights verbessern. Neben den unformatierten Datumsangaben fällt auf, dass die IDs der Abflughäfen (»Departure Airport«) und die IDs der Ankunftsflughäfen (»Arrival Airport«) statt der dreistelligen Codes der Flughäfen, wie z. B. DUS und MUC, angezeigt werden.
Bevor wir uns direkt an die Lösung des Problems machen, formulieren wir zunächst unseren Wunsch in Form einer Testmethode in der Datei flights_controller_test.rb:
def test_should_show_airport_names_in_index get :index assert_select 'td', 'DUS' assert_select 'td', 'MUC' end
assert_select
In der Testmethode wird zunächst die Index-Action mit der GET-HTTP-Methode aufgerufen und dann mit den beiden Assert-Methoden assert_select überprüft, ob sich die Codes DUS und MUC jeweils innerhalb eines <td> -Tags befinden. Die Codes sollen nämlich in der Tabelle angezeigt werden.
Der Test rake test:functionals schlägt fehl. Um ihn erfolgreich zu machen, ändern wir die Templatedatei index.html.erb wie folgt ab:
Ersetze die Zeilen
Listing *%app/views/flights/index.html.erb
<td><%=h flight.departure_airport_id %></td> <td><%=h flight.arrival_airport_id %></td>
durch
Listing *%app/views/flights/index.html.erb
<td><%=h flight.departure_airport.code %></td> <td><%=h flight.arrival_airport.code %></td>
Die Tests werden nun erfolgreich ausgeführt.
Abbildung http://localhost:3000/flights mit den Flughafen-Codes
Datumswerte formatieren
Als Nächstes kümmern wir uns um die Datumsformatierung. Anstatt Wed Jun 11 09:50:00 +0200 2008 soll 11.06.2008 auf der Index-Seite angezeigt werden. Der Fixture-Datei flights.yml entnehmen wir die Datumswerte, deren gewünschte Formatierung wir in einer Testmethode formulieren:
def test_should_format_dates_in_index get :index assert_select 'td', '10.06.2008 16:10' assert_select 'td', '10.06.2008 16:10' assert_select 'td', '11.06.2008 09:50' assert_select 'td', '11.06.2008 10:50' end
Damit die Testmethode nicht mehr fehlschlägt, ändern wir die Templatedatei index.html.erb wie folgt ab:
Ersetze die Zeilen
Listing *%/app/views/flights/index.html.erb
<td><%=h flight.departure_datetime %></td> <td><%=h flight.arrival_datetime %></td>
durch
Listing *%/app/views/flights/index.html.erb
<td> <%=h flight.departure_datetime.strftime('%d.%m.%Y %H:%M') %> </td> <td> <%=h flight.arrival_datetime.strftime('%d.%m.%Y %H:%M') %> </td>
Die Tests sollten nun erfolgreich laufen. Wir könnten jetzt den Code refaktorisieren, indem wir die Formatierungsbefehle in einen Helper auslagern. Laufe die Tests nach dieser Änderung immer noch, ist die Refaktorisierung erfolgreich durchgeführt worden.
Die Datumsformatierung für die Detailseite (Show-Action) erfolgt analog.
Select-Felder zur Flughafenauswahl
Als nächstes widmen wir uns den Formularen zum Editieren und Erstellen von Flügen.
Wenn wir die Editieren-Seite eines Datensatzes durch Klick auf den Edit-Link öffnen, fällt auf, dass die Felder für die Flughäfen lediglich Textfelder sind.
Abbildung http://localhost:3000/flights/219740562/edit ohne Select-Felder
Select-Felder
Anstelle des Textfeldes zur Eingabe der Flughafen-IDs hätten wir gerne Select-Felder für die Auswahl der Flughäfen. Der HTML-Code für solch ein Select-Feld könnte beispielsweise so aussehen:
<select id="flight_departure_airport_id"> <option value="859882751">DUS</option> <option value="893487900">HND</option> <option value="936559827">MUC</option> <option value="986663167">SFO</option> </select>
Es ist zu beachten, dass wir die IDs nicht kennen, da sie beim Importieren der Fixtures in die Datenbank automatisch gesetzt werden. In der folgenden Test-Methode wird überprüft, ob ein <select> -Tag mit der ID flight_departure_airport_id ein <option> -Tag enthält mit dem Wert »DUS« und dem value-Attribut mit der entsprechenden ID.
def test_should_have_select_fields_in_edit get :edit, :id => flights(:dus_muc).id assert_select 'select#flight_departure_airport_id' do assert_select "option",'DUS' assert_select 'option[value=?]', airports(:dus).id end end
Bei dem Test ist es ausreichend, nur auf das Vorkommen eines option -Tags (für »DUS«) zu prüfen.
Fehlerfreie Tests
Die Testmethode läuft fehlerfrei, wenn wir folgende Zeilen in der Template-Datei edit.html.erb
<p><b>Departure airport</b><br /> <%= f.text_field :departure_airport_id %></p> <p><b>Arrival airport</b><br /> <%= f.text_field :arrival_airport_id %></p>
durch
<p><b>Departure airport</b><br /> <%= f.collection_select :departure_airport_id, Airport.find(:all), :id, :code %></p> <p><b>Arrival airport</b><br /> <%= f.collection_select :arrival_airport_id, Airport.find(:all), :id, :code %> </p>
ersetzen.
Abbildung http://localhost:3000/flights/219740562/edit mit Select-Feldern
Weitere Funktionen zur Übung
Folgende offene Punkte seien Ihnen zur Übung überlassen:
- Anzeige der Flughafen-Codes auf der Detailseite (Show-Action)
- Datumsformatierung auf der Detailseite (Show-Action)
- Select-Felder für die Auswahl der Flughäfen beim Erstellen einen neuen Flughafendatensatzes (New-Action)
- Zusammenfassen der Formulare aus den Templates edit.html.erb und new.html.erb in ein Partial _form.html.erb
Gehen Sie auch hier Schritt für Schritt vor. Formulieren Sie für jeden Punkt zunächst die Testmethode, und implementieren Sie die Anforderung, bis der Test erfolgreich läuft. Erst dann widmen Sie sich dem nächsten Punkt.
Beispielprojekt auf CD |
Auf der beiliegenden CD finden Sie das vollständige Beispielprojekt »railsair« mit sämtlichen Tests. |
Integration-/Acceptance-Tests erstellen
Der Hauptanwendungszweck für Integration-Tests ist das Testen der Applikation aus der Sicht eines Benutzers, indem der Integration-Test mehrere Seiten aufruft und jeweils überprüft, ob die geladene Seite korrekt aufgerufen wird. Dies wird auch als »Szenario« oder »Story« bezeichnet.
Dabei ist es möglich, dass ein Integration-Test mehrere Controller involviert. Der Integration-Test kümmert sich dabei im Gegensatz zu den Functional- und den Unit-Tests nicht um Details, wie z. B. ob der Controller bestimmte Instanzvariablen gesetzt hat.
Ein Integration-Test könnte beispielsweise so aussehen:
- Öffne die Seite /login.
- Logge dich mit dem Usernamen »kara« und dem Passwort »geheim« ein.
- folge der Weiterleitung.
- Überprüfe, ob zur Home-Seite weitergeleitet wurde.
- Öffne die Seite /flights.
- Überprüfe, ob ein Delete-Link vorhanden ist.
- Betätige den ersten Delete-Link.
- Überprüfe, ob zur Übersichtsseite aller Flüge gewechselt wurde.
Story
Es handelt sich hierbei um eine »Story«, die beschreibt, wie ein Benutzer sich zunächst anmeldet und anschließend auf eine andere Seite wechselt, um einen Datensatz zu löschen.
Anhand von mehreren »Stories« kann die Funktionalität einer Webapplikation beschrieben werden. Dies wird dann auch als Acceptance-Test bezeichnet.
Es gibt verschiedene Technologien, mit denen man Integration-Tests durchführen kann.
- Integration-Test in Rails
Ohne eine Erweiterung zu installieren, kann man mit Rails Integration-Tests ausführen.
- Selenium
Selenium funktioniert auf Basis des Firefox-Browsers. Der Integration-Test läuft dabei direkt auf diesem Browser. Eine Programmierung des Integration-Tests in Ruby ist nicht notwendig (aber möglich). Selenium eignet sich hervorragend, um größere Applikationen zu testen, insbesondere wenn Ajax verwendet wird. Für weitere Informationen siehe http://www.openqa.org/selenium. Das Rails-Plug-in wird auf der Seite http://www.openqa.org/selenium-on-rails beschrieben.
Integration-Test mit Rails
Mit dem in Rails integrierten Generator script/generate integration_test wird eine Datei für Integration-Tests generiert. Als Parameter wird der Name der Integration-Test-Klasse angegeben.
ruby script/generate integration_test GeneralStories create test/integration/general_stories_test.rb
Die Integration-Test-Datei general_stories_test.rb sieht nach der Generierung wie folgt aus:
Listing test/integration/general_stories_test.rb
require "#{File.dirname(__FILE__)}/../test_helper" class GeneralStoriesTest < ActionController::IntegrationTest # fixtures :your, :models # Replace this with your real tests. def test_truth assert true end end
fixtures
Im Gegensatz zu Unit- und Functional-Tests werden die Fixtures nicht automatisch geladen. Mit dem Befehl fixtures kann man explizit angeben, welche Fixtures geladen werden sollen.
Da wir in unserer Beispielapplikation keine Login-Controller haben, werden wir uns bei unserem Integration-Test nur um den Flight-Controller kümmern.
- Seite »flights/new« aufrufen.
- Überprüfen, ob die Seite fehlerfrei aufgerufen werden konnte (HTTP-Status = 200).
- Überprüfen, ob das Template »flights/new« geladen wurde.
- Flight-Formulardaten an die Seite »/flights« schicken mit der HTML-Methode POST.
- Der Weiterleitung folgen.
- Überprüfen, ob die Seite ohne Fehler aufgerufen werden konnte (HTTP-Status = 200).
- Überprüfen, ob das Template »flights/show« geladen wurde.
Die Implementierung des Tests sieht wie folgt aus:
Listing test/integration/general_stories_test.rb
require "#{File.dirname(__FILE__)}/../test_helper" class GeneralStoriesTest < ActionController::IntegrationTest fixtures :countries, :airports # Replace this with your real tests. def test_new_flight get '/flights/new' assert_equal 200, status assert_template 'flights/new' post '/flights', :flight => valid_flight_attributes follow_redirect! assert_equal 200, status assert_template 'flights/show' end end
URL aufrufen
Im Gegensatz zu den Functional-Tests fällt auf, dass wir nicht direkt eine Action-Methode des Controllers im Test aufrufen, sondern die URL, wie z. B. get '/flights/new' statt get :new.
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.