ZenTest ist eine Suite aus 4 Tools und einer Library: zentest, unit_diff, autotest, multiruby und Test::Rails. Alle helfen dem geneigten Programmierer Code zu schreiben der komplett durch Unit-Tests abgedeckt ist.
Wie üblich über Rubygems:
gem install ZenTest
Wichtig ist dabei die richtige Gross/Kleinschreibung des Namens, da "zentest" eine veraltete Version des Gems installiert.
Windows-User brauchen ausserdem für unit_diff eine diff.exe im PATH, die README empfiehlt dazu das Diff-Tool aus der GNU Win32 Toolsammlung.
ZenTest besteht aus 4 Programmen: multiruby, zentest, unit_diff und autotest. Auf Test::Rails gehe ich hier nicht weiter ein und verweise einfach auf die dazugehörige Dokumentation.
multirubymultiruby führt Unit-Tests mit verschiedenen Versionen von Ruby aus. Dies ist vor allem interessant wenn man ein Programm entwickeln will das sowohl unter Ruby 1.8 als auch unter 1.9 laufen soll. Diese beide Ruby-Versionen verwendet multiruby auch von Haus aus (beim ersten mal werden beide heruntergeladen und in ein extra Verzeichnis kompiliert), wenn man andere Ruby-Versionen ebenfalls testen will, so muss man einfach deren tarball (im Format "ruby*.tar.gz") in das Verzeichnis ~/.multiruby/versions legen, woraufhin sie automatisch kompiliert und verwendet werden.
Leider funktioniert das noch nicht mit jRuby, da multiruby von dem Unix-üblichen configure-make-make install-Zyklus ausgeht, ich bin mir sicher das Ryan Davis Patches diesbezüglich akzeptiert.
zentestzentest sieht sich ein oder mehrere Rubyfiles an, und erzeugt Test::Unit-Stubs für Methoden ohne Testfälle. Das geht sowohl bei der Entwicklung von neuem Code, aber auch wenn man für bereits bestehenden Code Testfälle machen will.
Nehmen wir mal folgende einfache Klasse her:
class SomeClass attr_accessor :foo, :bar def initialize @foo = "foo" @bar = 42 end def a_method nil end def another_method(a, b, c = 3) [a, b, c] end def yet_another_method "foo" end end
zentest erzeugt uns automatisch unsere Testfälle:
$ zentest sample_program.rb > test/test_sample_program.rb
$ cat test/test_sample_program.rb
# Code Generated by ZenTest v. 3.9.1
# classname: asrt / meth = ratio%
# SomeClass: 0 / 3 = 0.00%
require 'test/unit' unless defined? $ZENTEST and $ZENTEST
class TestSomeClass < Test::Unit::TestCase
def test_a_method
raise NotImplementedError, 'Need to write test_a_method'
end
def test_another_method
raise NotImplementedError, 'Need to write test_another_method'
end
def test_bar
raise NotImplementedError, 'Need to write test_bar'
end
def test_bar_equals
raise NotImplementedError, 'Need to write test_bar_equals'
end
def test_foo
raise NotImplementedError, 'Need to write test_foo'
end
def test_foo_equals
raise NotImplementedError, 'Need to write test_foo_equals'
end
end
# Number of errors detected: 7
Der Witz dabei ist aber das zentest nur Testfälle für Methoden erstellt, für die wir noch keine Test haben. Dazu müssen wir zentest aber auch sagen wo unsere Testfälle liegen. Wenn wir also eine neue Methode yet_another_method zu unserer Klasse hinzufügen, müssen wir zentest so aufrufen:
$ zentest sample_program.rb test/test_sample_program.rb
# Code Generated by ZenTest v. 3.9.1
# classname: asrt / meth = ratio%
# SomeClass: 0 / 4 = 0.00%
require 'test/unit' unless defined? $ZENTEST and $ZENTEST
class TestSomeClass < Test::Unit::TestCase
def test_yet_another_method
raise NotImplementedError, 'Need to write test_yet_another_method'
end
end
# Number of errors detected: 1
Statt nochmals Stubs für alle Methoden erzeugen, kriegen wir nur für unsere neue Methode den Stub. Diesen können wir bequem in unsere bereits bestehende Test-Datei kopieren, und munter weiter testen.
zentest ist dabei aber nicht hellseherisch, und sieht logischerweise Methoden, die durch Meta-Programmierung erzeugt werden, nicht.
unit_diffunit_diff erleichtert das lesen des Outputs von Test::Unit. Statt dem normalen Output:
1) Failure:
test_to_gpoints(RouteTest) [test/unit/route_test.rb:29]:
<"new GPolyline([\n new GPoint( 47.00000, -122.00000),\n new GPoint( 46.5000
0, -122.50000),\n new GPoint( 46.75000, -122.75000),\n new GPoint( 46.00000,
-123.00000)])" expected but was
<"new Gpolyline([\n new GPoint( 47.00000, -122.00000),\n new GPoint( 46.5000
0, -122.50000),\n new GPoint( 46.75000, -122.75000),\n new GPoint( 46.00000,
-123.00000)])">.
erzeugt unit_diff folgenden Output:
1) Failure:
test_to_gpoints(RouteTest) [test/unit/route_test.rb:29]:
1c1
< new GPolyline([
---
> new Gpolyline([
(Beispiele wurden aus der unit_diff-Dokumentation entnommen) Der Unterschied ist winzig, und wäre uns im normalen Output nur sehr schwer aufgefallen - unit_diff zeigt uns dagegen sofort wo das Problem liegt, und lässt den Rest weg.
Das verwenden von unit_diff ist dabei denkbar einfach: Wenn test.rb die Tests ausführt, muss man nur dessen Ausgabe an unit_diff weiterleiten:
$ test.rb | unit_diff
So einfach ist das.
autotestautotest ist das interessanteste Programm aus der ZenTest-Suite: Es wartet bis wir Änderungen an unserem Unit-Tests abspeichern, und lässt dann automatisch unsere Unit-Tests (sowohl Test::Unit als auch RSpec) laufen. Damit erhalten wir sofort Feedback über die Auswirkung unserer Änderungen.
Nehmen wir unsere Test-Klasse mitsamt den von zentest generierten Unit-Tests von vorhin noch mal her (und nehmen an dass wir für alle Methoden bereits Tests implementiert haben). autotest geht davon aus dass die Unittests im Verzeichnis test (bzw. spec für RSpec) liegen, und das unser Code in 'lib' liegt. Wir starten einfach autotest:
$ ls -l1R
lib
test
./lib:
sample_program.rb
./test:
test_sample_program.rb
$ autotest
# Waiting since 2008-03-04 18:35:10
/usr/bin/ruby -I.:lib:test -rtest/unit -e "%w[test/test_sample_program.rb].each { |f| require f }" | unit_diff -u
Loaded suite -e
Started
......F
Finished in 0.008107 seconds.
1) Failure:
test_yet_another_method(TestSomeClass) [./test/test_sample_program.rb:43]:
--- /var/folders/8Z/8ZNHjeOOHTOEQ15lS-+vH++++TI/-Tmp-/expect.46957.0 2008-03-05 00:35:45.000000000 +0100
+++ /var/folders/8Z/8ZNHjeOOHTOEQ15lS-+vH++++TI/-Tmp-/butwas.46957.0 2008-03-05 00:35:45.000000000 +0100
@@ -1 +1 @@
-something
+<nil>
7 tests, 7 assertions, 1 failures, 0 errors
================================================================================
# Waiting since 2008-03-04 18:36:15
Dieser Output sagt uns: Unsere neue Methode ist noch nicht korrekt implementiert. Sobald das geschehen ist und wir im Editor auf "Speichern" drücken, kriegen wir sofort folgenden Output:
/usr/bin/ruby -I.:lib:test -rtest/unit -e "%w[test/test_sample_program.rb].each { |f| require f }" | unit_diff -u
Loaded suite -e
Started
.......
Finished in 0.000579 seconds.
7 tests, 7 assertions, 0 failures, 0 errors
================================================================================
# Waiting since 2008-03-04 18:41:12
Jetzt sind alle Tests grün, und wir können neue Tests für neue Methoden implementieren, immer in dem Wissen das wir sofort sehen ob unsere Methoden auch wirklich das tun was wir wollen.
autotest kann auch feststellen welche Datei genau geändert wurde und nur die dazugehörigen Unittests laufen lassen, sofern man sich an das normale Namen-Schema hält (z.b. test_sample_program.rb enthält die Unit Tests für sample_program.rb)
Zur Klarstellung: autotest erwartet das gewisse Regeln eingehalten werden:
test_.*implementation.rb bzw. implementation.*_spec.rb.Mac-User finden hier eine Möglichkeit autotest mit Growl zu verknüpfen - und dabei zeigt uns der Doom-Marine an wie es um unsere Tests steht.
Nächstes Mal: HTML-Parsen mit hpricot.
Kommentar schreiben
Kommentare
Ui, das liest sich sehr praktisch. Könntest du bitte mal einen generischen Aufbau des Testverzeichnisses geben? Ich bin mir da noch nicht ganz klar darüber. Was steht z.B. in test/unit und wo wird unsere someclass.rb includiert?
Ich hab den Post erweitert. Falls es noch immer nicht ganz klar ist, einfach weiter fragen.
Hm, das meinte ich nicht ganz. In test_someclass.rb müsst doch noch ein "require 'lib/somclass.rb' hinein. Oder kennt der Test automagisch die Implementation der zu testenden Methoden? Wie sieht eine "korrekter" rake-task aus, der die Test laufen lässt? So desc "Runs the tests" task :test do chdir('test') do ruby 'test_someclass.rb' end end Geht's ja auch nicht, da es kein Verzeichnis "lib" im Verzeichnis "test" gibt.
Hmpf.. und jetzt verrate mir doch mal bitte, wie ich meine Kommentare besser formatieren kann!
Ah ok - hab gerade gesehen, dass es bei Rake dieses TestTask gibt der die Tests startet.
Ahso.
@Kommentare: Some HTML is allowed. (Ja, mir wäre auch Markdown oder sowas lieber, aber so ist es halt im Moment...)
In der "test_someclass.rb" musst du nur ein
require 'someclass'machen. autotest gibt das lib-Verzeichnis ja per-I-Parameter mit, wodurch Ruby automatisch auch in diesem Verzeichnis nach .rb-Dateien sucht.@Raketask: Dem Ruby-Aufruf musst du irgendwie den
-I-Parameter mitgeben, so wie es autotest macht.Ok danke.