Rails 5.1 Consulting und Schulung vom Autor:
www.wintermeyer-consulting.de/rails/

7.2. Beispiel für einen User in einem Webshop

Fangen wir mit einem User-Scaffold in einem imaginären Webshop an:
MacBook:~ xyz$ rails new webshop
[...]
MacBook:~ xyz$ cd webshop
MacBook:webshop xyz$ rails generate scaffold user login_name first_name last_name birthday:date
[...]
      invoke    test_unit
      create      test/unit/user_test.rb
      create      test/fixtures/users.yml
[...]
      invoke    test_unit
      create      test/functional/users_controller_test.rb
      invoke    helper
      create      app/helpers/users_helper.rb
      invoke      test_unit
      create        test/unit/helpers/users_helper_test.rb
[...]
MacBook:webshop xyz$ rake db:migrate
[...]
MacBook:webshop xyz$
Sie kennen sich ja mit Scaffold aus (falls nicht, bitte erst Kapitel 5, Scaffolding und REST lesen) und wissen, was die gerade erstellte Applikation macht. Sie haben auch gesehen, dass Scaffold einige Tests angelegt hat (die sind immer leicht am Wort test im Dateinamen zu erkennen).
Die komplette Test-Suite eines Rails-Projektes wird mit dem Befehl rake test abgearbeitet. Probieren wir mal aus, was ein Test an dieser Stelle der Entwicklung ausgibt:
MacBook:webshop xyz$ rake test
Run options: 

# Running tests:



Finished tests in 0.003920s, 0.0000 tests/s, 0.0000 assertions/s.

0 tests, 0 assertions, 0 failures, 0 errors, 0 skips
Run options: 

# Running tests:

.......

Finished tests in 0.223761s, 31.2834 tests/s, 44.6905 assertions/s.

7 tests, 10 assertions, 0 failures, 0 errors, 0 skips
MacBook:webshop xyz$
Die Ausgabe „7 tests, 10 assertions, 0 failures, 0 errors, 0 skips“ sieht gut aus. Per Default läuft ein Test in einem Standard-Scaffold korrekt durch.
Verändern wir jetzt die app/models/user.rb und fügen ein paar Validierungen (falls diese nicht ganz klar sind, bitte Abschnitt 4.15, „Validierung (Validation)“ lesen) ein:
class User < ActiveRecord::Base
  attr_accessible :birthday, :first_name, :last_name, :login_name

  validates :login_name,
            :presence => true,
            :format => { 
                         :with => /^.*(?=.*[\-_.]).*$/,
                         :message => "must include at least one of the special characters -_."
                       }

  validates :last_name, 
            :presence => true

end
Nachfolgend führen wir noch rake test aus:
MacBook:webshop xyz$ rake test
Run options: 

# Running tests:



Finished tests in 0.004409s, 0.0000 tests/s, 0.0000 assertions/s.

0 tests, 0 assertions, 0 failures, 0 errors, 0 skips
Run options: 

# Running tests:

F.....F

Finished tests in 0.277867s, 25.1919 tests/s, 32.3896 assertions/s.

  1) Failure:
test_should_create_user(UsersControllerTest) [/Users/xyz/webshop/test/functional/users_controller_test.rb:20]:
"User.count" didn't change by 1.
<3> expected but was
<2>.

  2) Failure:
test_should_update_user(UsersControllerTest) [/Users/xyz/webshop/test/functional/users_controller_test.rb:39]:
Expected response to be a <:redirect>, but was <200>

7 tests, 9 assertions, 2 failures, 0 errors, 0 skips
Errors running test:functionals! #<RuntimeError: Command failed with status (2): [/Users/xyz/.rvm/rubies/ruby-1.9.3-p194/bin...]>
MacBook:webshop xyz$ 
Dieses Mal haben wir „2 failures“. Der Fehler passiert beim „should create user“ und beim „should update user“. Die Erklärung hierfür liegt in unserer Validierung. Die vom Scaffold Generator angelegten Beispieldaten sind beim ersten rake test (ohne Validierung) durchgelaufen. Erst beim zweiten Durchlauf (mit Validierung) gab es die Fehler.
Diese Beispieldaten werden als sogenannte Fixtures im YAML-Format im Verzeichnis test/fixtures/ angelegt. Schauen wir uns die Beispieldaten für User in der Datei test/fixtures/users.yml an:
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html

one:
  login_name: MyString
  first_name: MyString
  last_name: MyString
  birthday: 2012-05-29

two:
  login_name: MyString
  first_name: MyString
  last_name: MyString
  birthday: 2012-05-29
Dort sind zwei Beispieldatensätze angelegt, die unserer Validierung nicht standhalten. Der login_name braucht mindestens ein "-_."-Sonderzeichen. Ändern wir login_name in test/fixtures/users.yml entsprechend ab:
# Read about fixtures at http://api.rubyonrails.org/classes/Fixtures.html

one:
  login_name: My-String
  firstname: MyString
  lastname: MyString
  birthday: 1970-01-01

two:
  login_name: My-String
  firstname: MyString
  lastname: MyString
  birthday: 1970-01-01
Ein rake test läuft jetzt wieder ohne Fehler durch:
MacBook:webshop xyz$ rake test
Run options: 

# Running tests:



Finished tests in 0.003977s, 0.0000 tests/s, 0.0000 assertions/s.

0 tests, 0 assertions, 0 failures, 0 errors, 0 skips
Run options: 

# Running tests:

.......

Finished tests in 0.204496s, 34.2305 tests/s, 48.9007 assertions/s.

7 tests, 10 assertions, 0 failures, 0 errors, 0 skips
MacBook:webshop xyz$ 
Wir wissen nun, dass in der test/fixtures/users.yml valide Daten stehen müssen, damit der mit Scaffold erstellte Standard-Test durchläuft. Aber auch nicht mehr. Nachfolgend ändern wir die test/fixtures/users.yml auf ein Minimum (wir brauchen z. B. keinen first_name) und mit für Menschen leichter lesbaren Daten ab:
one:
  login_name: horst.meier
  last_name: Meier

two:
  login_name: emil.stein
  last_name: Stein
Sicherheitshalber nach der Veränderung noch mal ein rake test (das kann man gar nicht oft genug machen):
MacBook:webshop xyz$ rake test
Run options: 

# Running tests:



Finished tests in 0.004234s, 0.0000 tests/s, 0.0000 assertions/s.

0 tests, 0 assertions, 0 failures, 0 errors, 0 skips
Run options: 

# Running tests:

.......

Finished tests in 0.202971s, 34.4877 tests/s, 49.2681 assertions/s.

7 tests, 10 assertions, 0 failures, 0 errors, 0 skips
MacBook:webshop xyz$ 

Wichtig

Alle Fixtures werden beim Starten eines Tests in die Datenbank geladen. Besonders wenn man uniqueness in der Validierung benutzt, muss man dies beim Test im Hinterkopf behalten.

Functional Tests

Schauen wir uns mal genau an, an welcher Stelle die ursprünglichen Fehler aufgetreten sind:
  1) Failure:
test_should_create_user(UsersControllerTest) [/Users/xyz/webshop/test/functional/users_controller_test.rb:20]:
"User.count" didn't change by 1.
<3> expected but was
<2>.

7 tests, 9 assertions, 1 failures, 0 errors, 0 skips
Errors running test:functionals! #<RuntimeError: Command failed with status (1): [/Users/xyz/.rvm/rubies/ruby-1.9.3-p194/bin...]>
Im UsersControllerTest konnte der User nicht angelegt und nicht verändert werden. Die Controller-Tests befinden sich im Verzeichnis test/functional/. Schauen wir uns jetzt die Datei test/functional/users_controller_test.rb genau an:
require 'test_helper'

class UsersControllerTest < ActionController::TestCase
  setup do
    @user = users(:one)
  end

  test "should get index" do
    get :index
    assert_response :success
    assert_not_nil assigns(:users)
  end

  test "should get new" do
    get :new
    assert_response :success
  end

  test "should create user" do
    assert_difference('User.count') do
      post :create, user: { birthday: @user.birthday, first_name: @user.first_name, last_name: @user.last_name, login_name: @user.login_name }
    end

    assert_redirected_to user_path(assigns(:user))
  end

  test "should show user" do
    get :show, id: @user
    assert_response :success
  end

  test "should get edit" do
    get :edit, id: @user
    assert_response :success
  end

  test "should update user" do
    put :update, id: @user, user: { birthday: @user.birthday, first_name: @user.first_name, last_name: @user.last_name, login_name: @user.login_name }
    assert_redirected_to user_path(assigns(:user))
  end

  test "should destroy user" do
    assert_difference('User.count', -1) do
      delete :destroy, id: @user
    end

    assert_redirected_to users_path
  end
end
Am Anfang finden wir eine setup-Anweisung:
  setup do
    @user = users(:one)
  end
Diese drei Zeilen Code bedeuten, dass zum Start eines jeden einzelnen Tests eine Instanz @user mit den Daten des Eintrages one aus der Datei test/fixtures/users.yml angelegt wird. setup ist ein vordefinierter Callback, der – falls vorhanden – von Rails vor jedem Test gestartet wird. Das Gegenstück zu setup ist teardown. Ein teardown wird – falls vorhanden – nach jedem Test automatisch aufgerufen.

Wichtig

Bei jedem Test (also bei jedem Durchlauf von rake test) wird automatisch eine frische und damit leere Test-Datenbank angelegt. Das ist eine andere Datenbank als die, auf die Sie per Default mit rails console zugreifen (das ist die Development-Datenbank). Die Datenbanken werden in der Konfigurationsdatei config/database.yml definiert. Auf die Test-Datenbank können Sie zu Debug-Zwecken mit rails console test zugreifen.
Danach werden in diesem Functional Test verschiedene Webseiten-Funktionen getestet. Als Erstes der Zugriff auf die Index-Seite:
  test "should get index" do
    get :index
    assert_response :success
    assert_not_nil assigns(:users)
  end
Der Befehl get :index ruft die Seite /users auf. assert_response :success bedeutet, dass die Seite ausgeliefert wurde. Die Zeile assert_not_nil assigns(:users) stellt sicher, dass die Instanz-Variable @users vom Controller nicht mit dem Wert nil zum View gegeben wird (durch setup wurde ja sichergestellt, dass bereits ein Eintrag in der Datenbank ist).[27]
Schauen wir uns jetzt die beiden Probleme von vorhin genauer an. Als Erstes should create user:
  test "should create user" do
    assert_difference('User.count') do
      post :create, user: { birthday: @user.birthday, first_name: @user.first_name, last_name: @user.last_name, login_name: @user.login_name }
    end

    assert_redirected_to user_path(assigns(:user))
  end
Der Block assert_difference('User.count') do ... end erwartet eine Veränderung durch den darin enthaltenen Code. User.count müsste am Anfang 1 ergeben und am Ende 2. Da wir aber in der ersten test/fixtures/users.yml-Variante einen ungültigen Datensatz hatten, ergab User.count vorher und hinterher 0. 0 und nicht 1 am Anfang, weil auch das setup do ... end nicht funktioniert haben kann.
Die letzte Zeile assert_redirected_to user_path(assigns(:user)) überprüft, ob nach einem neu angelegten Datensatz auch auf den entsprechenden View show geleitet wird.
Den zweiten Fehler gab es bei should update user:
  test "should update user" do
    put :update, id: @user, user: { birthday: @user.birthday, first_name: @user.first_name, last_name: @user.last_name, login_name: @user.login_name }
    assert_redirected_to user_path(assigns(:user))
  end
Hier sollte der Datensatz mit der id des @user-Datensatzes mit den Attributen des @user-Datensatzes geupdatet werden. Danach soll auch wieder der show-View zu diesem Datensatz angezeigt werden. Logischerweise ging dieser Test auch nicht, da a) der @user-Datensatz gar nicht in der Datenbank existierte und b) er auch nicht geupdatet werden konnte, da er nicht valide war.
Ohne jetzt auf jeden einzelnen Functional Test Zeile für Zeile einzugehen, wird klar, was diese Tests machen: Sie führen echte Anfragen an das Web-Interface aus (bzw. eigentlich an die Controller) und können somit dazu benutzt werden, die Controller zu testen.

Tipp

Mit rake test:functionals können Sie auch nur die Functional Tests im Verzeichnis test/functional/ durchlaufen lassen.
MacBook:webshop xyz$ rake test:functionals
Run options: 

# Running tests:

.......

Finished tests in 0.205717s, 34.0273 tests/s, 48.6105 assertions/s.

7 tests, 10 assertions, 0 failures, 0 errors, 0 skips
MacBook:webshop xyz$ 

Unit Tests

Zum Testen der Validierungen, die wir in app/models/user.rb eingetragen haben, sind Unit Tests besser geeignet. Diese testen nicht wie die Functional Tests die Arbeit des Controllers, sondern nur das Model.

Tipp

Mit rake test werden alle im Rails-Projekt vorhandenen Tests ausgeführt. Mit rake test:units werden nur die Unit Tests im Verzeichnis test/unit/ ausgeführt.
Die Unit Tests befinden sich im Verzeichnis test/unit/. Ein Blick in die Datei test/unit/user_test.rb ist aber ernüchternd:
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  # test "the truth" do
  #   assert true
  # end
end
Per Default schreibt Scaffold nur einen auskommentierten Dummy-Test rein. Deshalb läuft rake test:units auch so inhaltslos durch:
MacBook:webshop xyz$ rake test:units
Run options: 

# Running tests:



Finished tests in 0.004657s, 0.0000 tests/s, 0.0000 assertions/s.

0 tests, 0 assertions, 0 failures, 0 errors, 0 skips
MacBook:webshop xyz$
Ein Unit-Test besteht immer aus folgender Struktur:
  test "eine Behauptung" do
    assert etwas_ist_true_oder_false
  end
Das Wort assert bedeutet in diesem Kontext behaupten oder feststellen. Es wird also eine Behauptung aufgestellt. Wenn diese Behauptung true (wahr) ist, dann läuft der Test durch und alles ist okay. Wenn diese Behauptung false (falsch) ist, schlägt der Test fehl und wir haben einen Fehler im Programm (die Ausgabe des Fehlers können Sie am Ende der assert-Zeile als String angeben).

Anmerkung

Wenn Sie sich einmal auf http://guides.rubyonrails.org/testing.html umschauen, dann werden Sie sehen, dass es noch ein paar andere assert-Varianten gibt. Hier einige Beispiele:
  • assert( boolean, [msg] )
  • assert_equal( obj1, obj2, [msg] )
  • assert_not_equal( obj1, obj2, [msg] )
  • assert_same( obj1, obj2, [msg] )
  • assert_not_same( obj1, obj2, [msg] )
  • assert_nil( obj, [msg] )
  • assert_not_nil( obj, [msg] )
  • assert_match( regexp, string, [msg] )
  • assert_no_match( regexp, string, [msg] )
Füllen wir den ersten Test in der test/unit/user_test.rb mit Leben:
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  test 'an empty user is not valid' do
    assert !User.new.valid?, 'Saved an empty user.'
  end
  
end
Dieser Test überprüft, ob ein neu angelegter User, der keine Daten enthält, valide ist. Da assert nur auf true reagiert, habe ich vor User.new.valid? ein „!“ gesetzt, um aus dem false ein true zu machen, denn ein leerer User kann ja nicht valide sein.
Ein rake test:units läuft dann auch direkt durch:
MacBook:webshop xyz$ rake test:units
Run options: 

# Running tests:

.

Finished tests in 0.081926s, 12.2061 tests/s, 12.2061 assertions/s.

1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
MacBook:webshop xyz$
Jetzt bauen wir zwei Asserts in einem Test ein, die überprüfen, ob die beiden Fixture-Einträge in der test/fixtures/users.yml auch valide sind:
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  test 'an empty user is not valid' do
    assert !User.new.valid?, 'Saved an empty user.'
  end

  test "the two fixture users are valid" do
    assert User.new(last_name: users(:one).last_name, login_name: users(:one).login_name ).valid?, 'First fixture is not valid.'
    assert User.new(last_name: users(:two).last_name, login_name: users(:two).login_name ).valid?, 'Second fixture is not valid.'
  end
  
end
Danach wieder ein rake test:units:
MacBook:webshop xyz$ rake test:units
Run options: 

# Running tests:

..

Finished tests in 0.090001s, 22.2220 tests/s, 33.3330 assertions/s.

2 tests, 3 assertions, 0 failures, 0 errors, 0 skips
MacBook:webshop xyz$
Fügen wir jetzt noch Tests für einige andere login_name-Varianten ein:
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  test 'an empty user is not valid' do
    assert !User.new.valid?, 'Saved an empty user.'
  end

  test "the two fixture users are valid" do
    assert User.new(last_name: users(:one).last_name, login_name: users(:one).login_name ).valid?, 'First fixture is not valid.'
    assert User.new(last_name: users(:two).last_name, login_name: users(:two).login_name ).valid?, 'Second fixture is not valid.'
  end
  
  [
    'hans.meier',
    'hans-meier',
    'h-meier',
    'h_meier',
    'h.meier2',
  ].each do |valid_login_name|
    test "the login_name '#{valid_login_name}' is valid" do
      assert User.new(last_name: users(:one).last_name, login_name: valid_login_name ).valid?, "login_name '#{valid_login_name}' is not valid."
    end
  end
  
end
Der Durchlauf der Test-Suite zeigt die Ergebnisse:
MacBook:webshop xyz$ rake test:units
Run options: 

# Running tests:

.......

Finished tests in 0.090630s, 77.2371 tests/s, 88.2710 assertions/s.

7 tests, 8 assertions, 0 failures, 0 errors, 0 skips
MacBook:webshop xyz$  
Mit rake test könnten Sie jetzt wieder alle Tests durchlaufen lassen.


[27] Dabei wird hier das Symbol :users genommen, um sicherzustellen, dass @users in der zu testenden Controller-Klasse und nicht @users in der Test-Klasse selbst genommen wird.

Autor

Stefan Wintermeyer