2008-11-01 8 views
26

Ich kämpfe mit Test :: Unit. Wenn ich an Komponententests denke, denke ich an einen einfachen Test pro Datei. Aber in Rubys Rahmen, muß ich stattdessen schreiben:In Rubys Test :: Unit :: TestCase, wie überschreibe ich die Initialisierungsmethode?

class MyTest < Test::Unit::TestCase 
    def setup 
    end 

    def test_1 
    end 

    def test_1 
    end 
end 

Aber Setup und Teardown-Lauf für jeden Aufruf einer test_ * Methode. Genau das will ich nicht. Ich möchte vielmehr eine Setup-Methode, die nur einmal für die gesamte Klasse ausgeführt wird. Aber ich kann nicht scheinen, meine eigene initialize() zu schreiben, ohne TestCase zu initialisieren.

Ist das möglich? Oder mache ich das hoffnungslos kompliziert?

+0

Zwei Testverfahren mit dem gleichen Namen führt zu der ersten Methode nicht gestartet wird. Sie könnten einen Flunk in den ersten Test setzen, und Tests würden noch bestehen. Ein Nebeneffekt der Cut-and-Paste-Programmierung. –

+0

Ja, und es ist einfach. Dies wird abschließend in TestUnit implementiert. Siehe meine Post waaaay auf dieser Seite. – jpgeek

Antwort

9

So soll es funktionieren!

Jeder Test sollte vollständig vom Rest isoliert sein, daher werden die Methoden setup und tear_down einmal für jeden Testfall ausgeführt. Es gibt jedoch Fälle, in denen Sie mehr Kontrolle über den Ausführungsablauf haben möchten. Dann können Sie die Testfälle in Suites gruppieren.

In Ihrem Fall könnten Sie so etwas wie die schreiben folgende:

require 'test/unit' 
require 'test/unit/ui/console/testrunner' 

class TestDecorator < Test::Unit::TestSuite 

    def initialize(test_case_class) 
    super 
    self << test_case_class.suite 
    end 

    def run(result, &progress_block) 
    setup_suite 
    begin 
     super(result, &progress_block)  
    ensure 
     tear_down_suite 
    end 
    end 

end 

class MyTestCase < Test::Unit::TestCase 

    def test_1 
    puts "test_1" 
    assert_equal(1, 1) 
    end 

    def test_2 
    puts "test_2" 
    assert_equal(2, 2) 
    end 

end 

class MySuite < TestDecorator 

    def setup_suite 
    puts "setup_suite" 
    end 

    def tear_down_suite 
    puts "tear_down_suite" 
    end 

end 

Test::Unit::UI::Console::TestRunner.run(MySuite.new(MyTestCase)) 

Die TestDecorator definiert eine besondere Suite, die eine setup und tear_down Methode zur Verfügung, die nur einmal ausgeführt vor und nach dem Laufe des Satzes von test- Fälle, die es enthält.

Der Nachteil davon ist, dass Sie Test :: Einheit wie die Tests in der Einheit ausführen müssen. Für den Fall, enthält Ihr Gerät viele Testfälle und braucht einen Dekorateur für nur einen von ihnen Sie so etwas wie dies benötigen:

require 'test/unit' 
require 'test/unit/ui/console/testrunner' 

class TestDecorator < Test::Unit::TestSuite 

    def initialize(test_case_class) 
    super 
    self << test_case_class.suite 
    end 

    def run(result, &progress_block) 
    setup_suite 
    begin 
     super(result, &progress_block)  
    ensure 
     tear_down_suite 
    end 
    end 

end 

class MyTestCase < Test::Unit::TestCase 

    def test_1 
    puts "test_1" 
    assert_equal(1, 1) 
    end 

    def test_2 
    puts "test_2" 
    assert_equal(2, 2) 
    end 

end 

class MySuite < TestDecorator 

    def setup_suite 
    puts "setup_suite" 
    end 

    def tear_down_suite 
    puts "tear_down_suite" 
    end 

end 

class AnotherTestCase < Test::Unit::TestCase 

    def test_a 
    puts "test_a" 
    assert_equal("a", "a") 
    end 

end 

class Tests 

    def self.suite 
    suite = Test::Unit::TestSuite.new 
    suite << MySuite.new(MyTestCase) 
    suite << AnotherTestCase.suite 
    suite 
    end 

end 

Test::Unit::UI::Console::TestRunner.run(Tests.suite) 

Die Dokumentation liefert eine gute Erklärung, wie Suiten arbeitet.

+0

Warum erhalte ich einen Fehler "nicht initialisierte Konstante Test :: Unit :: TestSuite"? – Alexandre

1

Ich stieß auf dieses genaue Problem und erstellt eine Unterklasse von Test::Unit::TestCase für genau das, was Sie beschreiben.

Hier ist, was ich gefunden habe. Es stellt eigene setup und teardown Methoden bereit, die die Anzahl der Methoden in der Klasse zählen, die mit "test" beginnen. Auf dem ersten Aufruf von setup nennt es global_setup und auf dem letzten Aufruf von teardown nennt es

class ImprovedUnitTestCase < Test::Unit::TestCase 
    cattr_accessor :expected_test_count 

    def self.global_setup; end 
    def self.global_teardown; end  

    def teardown 
    if((self.class.expected_test_count-=1) == 0) 
     self.class.global_teardown 
    end 
    end 
    def setup 
    cls = self.class 

    if(not cls.expected_test_count) 
     cls.expected_test_count = (cls.instance_methods.reject{|method| method[0..3] != 'test'}).length 
     cls.global_setup 
    end 
    end 
end 

Ihre Testfälle wie folgt erstellen:

class TestSomething < ImprovedUnitTestCase 
    def self.global_setup 
    puts 'global_setup is only run once at the beginning' 
    end 

    def self.global_teardown 
    puts 'global_teardown is only run once at the end' 
    end 

    def test_1 
    end 

    def test_2 
    end 
end 

Der Fehler dabei ist, dass man nicht Stellen Sie Ihre eigenen Testmethoden setup und teardown, es sei denn, Sie verwenden die Klassenmethode setup :method_name (nur in Rails 2.X?) und wenn Sie eine Testsuite oder etwas, das nur eine der Testmethoden ausführt, dann gewann nicht aufgerufen werden, weil davon ausgegangen wird, dass alle Testmethoden irgendwann ausgeführt werden.

0

Verwenden Sie die TestSuite als @ romulo-a-ceccon für spezielle Vorbereitungen für jede Testsuite beschrieben.

Aber ich denke, es sollte hier erwähnt werden, dass Unit-Tests in völliger Isolation ausgeführt werden sollen. Somit ist der Ausführungsablauf ein Setup-Test-Teardown, der garantieren sollte, dass jeder Testlauf durch irgendetwas, was die anderen Tests taten, ungestört ist.

0

Ich habe ein Mixin namens SetupOnce erstellt. Hier ist ein Beispiel für die Verwendung.

require 'test/unit' 
require 'setuponce' 


class MyTest < Test::Unit::TestCase 
    include SetupOnce 

    def self.setup_once 
    puts "doing one-time setup" 
    end 

    def self.teardown_once 
    puts "doing one-time teardown" 
    end 

end 

Und hier ist der eigentliche Code; Beachten Sie, dass ein anderes Modul von der ersten Verknüpfung in den Fußnoten benötigt wird.

require 'mixin_class_methods' # see footnote 1 

module SetupOnce 
    mixin_class_methods 

    define_class_methods do 
    def setup_once; end 

    def teardown_once; end 

    def suite 
     mySuite = super 

     def mySuite.run(*args) 
     @name.to_class.setup_once 
     super(*args) 
     @name.to_class.teardown_once 
     end 

     return mySuite 
    end 
    end 
end 

# See footnote 2 
class String 
    def to_class 
    split('::').inject(Kernel) { 
     |scope, const_name| 
     scope.const_get(const_name) 
    } 
    end 
end 

Fußnoten:

  1. http://redcorundum.blogspot.com/2006/06/mixing-in-class-methods.html

  2. http://infovore.org/archives/2006/08/02/getting-a-class-object-in-ruby-from-a-string-containing-that-classes-name/

24

Wie in Hal Fulton Buch "The Ruby Way" erwähnt. Er überschreibt die self.suite-Methode von Test :: Unit, mit der die Testfälle in einer Klasse als Suite ausgeführt werden können.

def self.suite 
    mysuite = super 
    def mysuite.run(*args) 
     MyTest.startup() 
     super 
     MyTest.shutdown() 
    end 
    mysuite 
end 

Hier ist ein Beispiel:

class MyTest < Test::Unit::TestCase 
    class << self 
     def startup 
      puts 'runs only once at start' 
     end 
     def shutdown 
      puts 'runs only once at end' 
     end 
     def suite 
      mysuite = super 
      def mysuite.run(*args) 
       MyTest.startup() 
       super 
       MyTest.shutdown() 
      end 
      mysuite 
     end 
    end 

    def setup 
     puts 'runs before each test' 
    end 
    def teardown 
     puts 'runs after each test' 
    end 
    def test_stuff 
     assert(true) 
    end 
end 
+0

Danke für eine tolle Antwort! –

+0

Leider funktioniert die zweite Antwort nicht mit der aktuellen Version von Test :: Unit, zumindest läuft nicht in RubyMine unter Windows 7. Ausschneiden und Einfügen des Codes in RubyMine und Ausführen, Starten und Herunterfahren jedes Laufs _twice_, nicht _once_: ( Ich verwende es nur, um den Beginn und das Ende eines Testfalls zu protokollieren (mit Logger), damit ich sehen kann, welcher Testfall welches Bit des Protokolls erzeugt, damit ich widerwillig damit leben kann, aber andere Leute könnten – digitig

+0

Was ist, wenn ich eine ähnliche Sache in ActionController :: TestCase machen möchte? –

2

Nun, ich erreichte im Grunde die gleiche Art und Weise in einer wirklich hässlich und schrecklich Art und Weise, aber es war schneller. :) Sobald ich, dass die Tests realisiert werden in alphabetischer Reihenfolge ausgeführt werden:

class MyTests < Test::Unit::TestCase 
def test_AASetup # I have a few tests that start with "A", but I doubt any will start with "Aardvark" or "Aargh!" 
    #Run setup code 
end 

def MoreTests 
end 

def test_ZTeardown 
    #Run teardown code 
end 

Es ziemlich aint, aber es funktioniert :)

2

Um dieses Problem zu lösen ich das Setup-Konstrukt verwendet, mit nur einem Testverfahren gefolgt. Diese eine Testmethode ruft alle anderen Tests auf.

Zum Beispiel

class TC_001 << Test::Unit::TestCase 
    def setup 
    # do stuff once 
    end 

    def testSuite 
    falseArguments() 
    arguments() 
    end 

    def falseArguments 
    # do stuff 
    end 

    def arguments 
    # do stuff 
    end 
end 
0

+1 für die RSpec oben Antwort von @ ORion-edwards. Ich hätte seine Antwort kommentiert, aber ich habe noch nicht genug Reputation, um die Antworten zu kommentieren.

Ich benutze test/unit und RSpec viel und ich muß sagen ... der Code, dass jede Buchung ist ein sehr wichtiges Merkmal before(:all) fehlt das ist: @instance variable Unterstützung.

In RSpec können Sie tun:

describe 'Whatever' do 
    before :all do 
    @foo = 'foo' 
    end 

    # This will pass 
    it 'first' do 
    assert_equal 'foo', @foo 
    @foo = 'different' 
    assert_equal 'different', @foo 
    end 

    # This will pass, even though the previous test changed the 
    # value of @foo. This is because RSpec stores the values of 
    # all instance variables created by before(:all) and copies 
    # them into your test's scope before each test runs. 
    it 'second' do 
    assert_equal 'foo', @foo 
    @foo = 'different' 
    assert_equal 'different', @foo 
    end 
end 

Die Implementierungen von #startup und #shutdown vor allem Fokus auf, sicherzustellen, dass diese Verfahren nur einmal für die gesamte TestCase Klasse aufgerufen werden, aber alle Instanzvariablen in diesen verwendet Methoden wären verloren!

RSpec führt seine before(:all) in seiner eigenen Instanz von Object und alle lokalen Variablen werden kopiert, bevor jeder Test ausgeführt wird.

Um alle Variablen zugreifen, die während einer globalen #startup Methode erstellt werden, müssen Sie entweder:

  • Kopie alle der Instanzvariablen von #startup erstellt, wie RSpec tun
  • Ihre Variablen in #startup definieren in einen Bereich, auf den Sie von Ihren Testmethoden aus zugreifen können, z. @@class_variables oder Klasse-Ebene attr_accessors erstellen, die Zugriff auf die @instance_variables, die Sie innerhalb von 0,02 def self.startup

Just my $ erstellen!

7

ENDLICH, Testgerät hat dies implementiert! Woot! Wenn Sie v 2.5.2 oder höher verwenden, können Sie nur verwenden:

Test::Unit.at_start do 
    # initialization stuff here 
end 

Dies wird einmal ausgeführt, wenn Sie sich Ihre Tests starten. Es gibt auch Callbacks, die zu Beginn jedes Testfalls (Start) ausgeführt werden, zusätzlich zu denen, die vor jedem Test (Setup) ausgeführt werden.

http://test-unit.rubyforge.org/test-unit/en/Test/Unit.html#at_start-class_method

2

Ich weiß, das ist eine ganz alte Post, aber ich hatte das Problem (und hatte bereits Klassen geschrieben mit Tes/Einheit) und ave beantwortete andere Methode verwendet, also wenn es helfen kann ...

nur

Wenn Sie das Äquivalent der Start Funktion benötigen, können Sie die Klassenvariablen verwenden:

class MyTest < Test::Unit::TestCase 
    @@cmptr = nil 
    def setup 
    if @@cmptr.nil? 
     @@cmptr = 0 
     puts "runs at first test only" 
     @@var_shared_between_fcs = "value" 
    end 
    puts 'runs before each test' 
    end 
    def test_stuff 
    assert(true) 
    end 
end 
+0

Ich habe alle Antworten hier gelesen. Dies ist der einzige, der mir 100% klar macht - die anderen sind einfach so undurchsichtig und komplex und lassen mein teensy caffeeine-mangelndes Gehirn erbrechen. –