Ausführungsreihenfolge von Python unittest.TestCase

77

Gibt es in Python eine Möglichkeit unittest, die Reihenfolge festzulegen , in der Testfälle ausgeführt werden?

In meiner aktuellen TestCaseKlasse haben einige Testfälle Nebenwirkungen, die Bedingungen für den ordnungsgemäßen Betrieb der anderen festlegen. Jetzt ist mir klar, dass der richtige Weg, dies setUp()zu tun, darin besteht, alle Setup-Aufgaben zu erledigen, aber ich möchte ein Design implementieren, bei dem jeder aufeinanderfolgende Test etwas mehr Status erzeugt, als der nächste verwenden kann. Ich finde das viel eleganter.

class MyTest(TestCase):
  def test_setup(self):
   #do something
  def test_thing(self)
   #do something that depends on test_setup()

Idealerweise möchte ich, dass die Tests in der Reihenfolge ausgeführt werden, in der sie in der Klasse erscheinen. Es scheint, dass sie in alphabetischer Reihenfolge ausgeführt werden.

Mike
quelle

Antworten:

71

Machen Sie keine unabhängigen Tests - wenn Sie einen monolithischen Test wünschen, schreiben Sie einen monolithischen Test.

class Monolithic(TestCase):
  def step1(self):
      ...

  def step2(self):
      ...

  def _steps(self):
    for name in dir(self): # dir() result is implicitly sorted
      if name.startswith("step"):
        yield name, getattr(self, name) 

  def test_steps(self):
    for name, step in self._steps():
      try:
        step()
      except Exception as e:
        self.fail("{} failed ({}: {})".format(step, type(e), e))

Wenn der Test später fehlschlägt und Sie Informationen zu allen fehlgeschlagenen Schritten wünschen, anstatt den Testfall beim ersten fehlgeschlagenen Schritt anzuhalten, können Sie die folgende subtestsFunktion verwenden: https://docs.python.org/3/library/unittest.html# Unterscheiden von Testiterationen unter Verwendung von Untertests

(Die Subtest-Funktion ist unittest2für Versionen vor Python 3.4 verfügbar : https://pypi.python.org/pypi/unittest2 )

ncoghlan
quelle
Ich bin ziemlich neu im Unit-Test und habe das Gefühl, dass der monolithische Test schlecht ist. Ist das wahr? Erstellen Sie einfach meine Testsuite, und ich bin wirklich auf monolithische Tests mit Ihrem Code angewiesen. Ist dies ein Zeichen dafür, dass ich mich dem Unit-Test schlecht nähere? Danke
swdev
8
Reine Unit-Tests bieten den Vorteil, dass sie Ihnen oft genau sagen , was falsch ist , wenn sie fehlschlagen . Sie können auch die fehlgeschlagenen Tests erneut ausführen, wenn Sie versuchen, sie zu beheben. Solche monolithischen Tests haben diese Vorteile nicht: Wenn sie fehlschlagen, ist es eine Debugging-Übung, um herauszufinden, was schief gelaufen ist. Auf der anderen Seite sind solche Tests oft viel einfacher und schneller zu schreiben, insbesondere wenn Tests an einer vorhandenen Anwendung nachgerüstet werden, die nicht für Unit-Tests entwickelt wurde.
Ncoghlan
5
@shakirthow Wenn die Reihenfolge der Ausführung wichtig ist, handelt es sich nicht mehr um Unit-Tests, sondern um Schritte in einem Szenariotest. Das ist immer noch eine lohnende Sache, aber es wird am besten entweder als größerer Testfall wie gezeigt behandelt oder mit einem übergeordneten Verhaltenstest-Framework wie pythonhosted.org/behave
ncoghlan
1
Beachten Sie, dass dies in Ihrem Code sorted()nicht unbedingt erforderlich ist, da dir()die Schrittmethoden alphabetisch nach Garantie sortiert zurückgegeben werden. Aus diesem Grund werden unittestdie Testklassen und Testmethoden standardmäßig auch in alphabetischer Reihenfolge behandelt (auch wenn dies nicht sortTestMethodsUsingder Fall ist). Dies kann aus praktischen Gründen genutzt werden, um beispielsweise die neuesten Arbeitstests zuerst ausführen zu lassen, um den Edit-Testrun-Zyklus zu beschleunigen.
kxr
1
@ncoghlan Nick, wollte mich nur für diese Kommentare zum Testen bedanken - öffnete mir wirklich die Augen für ein Problem, das ich hatte. Ich habe auch einige Ihrer anderen Antworten verfolgt, die ebenso hervorragend waren. Prost!
Brandon Bertelsen
40

Es ist eine gute Praxis, immer einen monolithischen Test für solche Erwartungen zu schreiben. Wenn Sie jedoch ein doofer Typ wie ich sind, können Sie einfach hässlich aussehende Methoden in alphabetischer Reihenfolge schreiben, damit sie von a nach b sortiert werden, wie in den Python-Dokumenten http erwähnt : //docs.python.org/library/unittest.html

Beachten Sie, dass die Reihenfolge, in der die verschiedenen Testfälle ausgeführt werden, durch Sortieren der Testfunktionsnamen in Bezug auf die integrierte Reihenfolge für Zeichenfolgen bestimmt wird

BEISPIEL:

  def test_a_first():
  print "1"
  def test_b_next(): 
  print "2" 
  def test_c_last(): 
  print "3"
varun
quelle
4
IMO ist dieser Ansatz besser als das Hinzufügen von mehr Code als Problemumgehung.
Raptor
Warum ist es Ihrer Meinung nach eine gute Praxis, monolithische Tests zu schreiben? Sehen Sie sich an, wie Java TestNG dies mit Testgruppen und Abhängigkeiten ausführt . Auf jeden Fall bin ich auch ein doofer Typ, und wenn ich meine Tests in Alpha-Reihenfolge schreibe, habe ich es als nützlich empfunden, den Status durch globale Variablen zu übergeben, da der Testläufer möglicherweise für jeden Test unterschiedliche Instanzen erstellt.
Joshua Richardson
1
@Joshua Wie bei allen anderen Dingen gibt es keine "einzige Lösung, um alle zu regieren". Die Monolithinc-Lösung ist oft eine gute Entwurfspraxis, die von einigen Programmierern in Betracht gezogen wird. Bestellte Tests oder szenariogesteuerte Tests verstoßen gegen eine der Entwurfsregeln für Tests, die ist "ein Test pro Erwartung", aber Sie müssen sich nicht daran halten. Ich bin kein großer Java-Fan, und nur weil ein Framework versucht, etwas zu tun, bedeutet dies nicht, dass es eine gute Praxis ist. Und das Wort Testgruppe selbst macht mir keinen Sinn, aber zögern Sie nicht, was auch immer bruh zu tun.
Varun
Es gibt einen Test, von dem ich denke, dass er nicht in der Mitte, sondern zuletzt ausgeführt werden sollte. Daher beginnt der Name mit einem "z".
Kardamom
26

http://docs.python.org/library/unittest.html

Beachten Sie, dass die Reihenfolge, in der die verschiedenen Testfälle ausgeführt werden, durch Sortieren der Testfunktionsnamen in Bezug auf die integrierte Reihenfolge für Zeichenfolgen bestimmt wird.

Stellen Sie also sicher, dass test_setupder Name den kleinsten Zeichenfolgenwert hat.

Beachten Sie, dass Sie sich nicht auf dieses Verhalten verlassen sollten - verschiedene Testfunktionen sollten unabhängig von der Ausführungsreihenfolge sein. In der obigen Antwort von ngcohlan finden Sie eine Lösung, wenn Sie ausdrücklich eine Bestellung benötigen.

kennytm
quelle
6
Unterschiedlicher Testrunner, unterschiedliches Verhalten. Ihr Rat ist nicht hilfreich, um stabilen Code und Tests zu schreiben.
Andreas Jung
20

Alte Frage, aber eine andere Art, die ich in verwandten Fragen nicht gesehen habe: Verwenden Sie aTestSuite .

Eine andere Möglichkeit, die Bestellung abzuschließen, besteht darin, die Tests zu a hinzuzufügen unitest.TestSuite. Dies scheint die Reihenfolge zu berücksichtigen, in der die Tests mithilfe von zur Suite hinzugefügt werden suite.addTest(...). Um dies zu tun:

  • Erstellen Sie eine oder mehrere TestCase-Unterklassen.

    class FooTestCase(unittest.TestCase):
        def test_ten():
            print('Testing ten (10)...')
        def test_eleven():
            print('Testing eleven (11)...')
    
    class BarTestCase(unittest.TestCase):
        def test_twelve():
            print('Testing twelve (12)...')
        def test_nine():
            print('Testing nine (09)...')
    
  • Erstellen Sie eine aufrufbare Testsuite-Generation, die in der gewünschten Reihenfolge hinzugefügt wurde und aus den Dokumenten und dieser Frage angepasst wurde :

    def suite():
        suite = unittest.TestSuite()
        suite.addTest(BarTestCase('test_nine'))
        suite.addTest(FooTestCase('test_ten'))
        suite.addTest(FooTestCase('test_eleven'))
        suite.addTest(BarTestCase('test_twelve'))
        return suite
    
  • Führen Sie die Testsuite aus, z.

    if __name__ == '__main__':
        runner = unittest.TextTestRunner(failfast=True)
        runner.run(suite())
    

Für den Kontext hatte ich dies gebraucht und war mit den anderen Optionen nicht zufrieden. Ich habe mich für die oben beschriebene Art der Testbestellung entschieden. Ich habe nicht gesehen, dass diese TestSuite-Methode eine der verschiedenen "Fragen zur Bestellung von Komponententests" auflistet (z. B. diese und andere Fragen, einschließlich Ausführungsreihenfolge oder Änderungsreihenfolge oder Testreihenfolge ).

hoc_age
quelle
Dies ist gut, außer dass für jeden Testfall eine neue Klasse erstellt wird. Gibt es eine Möglichkeit, Daten aus test_ten beizubehalten und in test_eleven zu verwenden?
Thang
@thang Wenn Sie Dinge machen @classmethod, können sie den Status über Instanzen hinweg beibehalten .
Nick Chapman
Wissen Sie dabei, ob setUpClassaufgerufen wird? Oder muss es manuell ausgeführt werden?
Nick Chapman
@NickChapman wie macht das Sinn? @ classmethod macht es so ziemlich zu einer statischen Funktion (mit Klasseninfo als Parameter)
thang
1
@ Thang @classmethod != @staticmethod!!! Sei vorsichtig, es sind ganz andere Dinge. @staticmethodMit dieser Option können Sie die Methode aufrufen, ohne über eine Instanz der Klasse zu verfügen. @classmethodgibt Ihnen Zugriff auf die Klasse und in der Klasse selbst können Sie Informationen speichern. Wenn Sie dies beispielsweise cls.somevar = 10innerhalb einer Klassenmethode tun, sehen alle Instanzen dieser Klasse und alle anderen Klassenmethoden dies, somevar = 10nachdem diese Funktion ausgeführt wurde. Klassen selbst sind Objekte, an die Sie Werte binden können.
Nick Chapman
4

Am Ende hatte ich eine einfache Lösung, die für mich funktionierte:

class SequentialTestLoader(unittest.TestLoader):
    def getTestCaseNames(self, testCaseClass):
        test_names = super().getTestCaseNames(testCaseClass)
        testcase_methods = list(testCaseClass.__dict__.keys())
        test_names.sort(key=testcase_methods.index)
        return test_names

Und dann

unittest.main(testLoader=utils.SequentialTestLoader())
caio
quelle
1

Tests, die wirklich voneinander abhängen, sollten explizit zu einem Test verkettet werden.

Bei Tests, für die unterschiedliche Setup-Ebenen erforderlich sind, kann auch das entsprechende setUp()Setup ausreichend ausgeführt werden - auf verschiedene Arten denkbar.

Andernfalls werden unittestdie Testklassen und Testmethoden innerhalb der Testklassen standardmäßig in alphabetischer Reihenfolge behandelt (auch wenn " loader.sortTestMethodsUsingKeine" ist). dir()wird intern verwendet, was nach Garantie sortiert.

Das letztere Verhalten kann aus Gründen der Praktikabilität ausgenutzt werden - z. B. um die neuesten Arbeitstests zuerst ausführen zu lassen, um den Edit-Testrun-Zyklus zu beschleunigen. Dieses Verhalten sollte jedoch nicht verwendet werden, um echte Abhängigkeiten herzustellen . Beachten Sie, dass Tests einzeln über Befehlszeilenoptionen usw. ausgeführt werden können.

kxr
quelle
0

@ ncoghlans Antwort war genau das, wonach ich gesucht habe, als ich zu diesem Thread kam. Am Ende habe ich es so geändert, dass jeder Stufentest ausgeführt werden kann, selbst wenn ein vorheriger Schritt bereits einen Fehler ausgelöst hat. Dies hilft mir (und vielleicht Ihnen!), die Ausbreitung von Fehlern in datenbankzentrierter Multithread-Software zu entdecken und zu planen.

class Monolithic(TestCase):
  def step1_testName1(self):
      ...

  def step2_testName2(self):
      ...

  def steps(self):
      '''
      Generates the step methods from their parent object
      '''
      for name in sorted(dir(self)):
          if name.startswith('step'):
              yield name, getattr(self, name)

  def test_steps(self):
      '''
      Run the individual steps associated with this test
      '''
      # Create a flag that determines whether to raise an error at
      # the end of the test
      failed = False

      # An empty string that the will accumulate error messages for 
      # each failing step
      fail_message = ''
      for name, step in self.steps():
          try:
              step()
          except Exception as e:
              # A step has failed, the test should continue through
              # the remaining steps, but eventually fail
              failed = True

              # get the name of the method -- so the fail message is
              # nicer to read :)
              name = name.split('_')[1]
              # append this step's exception to the fail message
              fail_message += "\n\nFAIL: {}\n {} failed ({}: {})".format(name,
                                                                       step,
                                                                       type(e),
                                                                       e)

      # check if any of the steps failed
      if failed is True:
          # fail the test with the accumulated exception message
          self.fail(fail_message)
dslosky
quelle
0

Eine einfache und flexible Möglichkeit besteht darin, eine Komparatorfunktion zuzuweisen unittest.TestLoader.sortTestMethodsUsing:

Funktion zum Vergleichen von Methodennamen beim Sortieren getTestCaseNames()und aller loadTestsFrom*()Methoden.

Minimale Nutzung:

import unittest

class Test(unittest.TestCase):
    def test_foo(self):
        """ test foo """
        self.assertEqual(1, 1)

    def test_bar(self):
        """ test bar """
        self.assertEqual(1, 1)

if __name__ == "__main__":
    test_order = ["test_foo", "test_bar"] # could be sys.argv
    loader = unittest.TestLoader()
    loader.sortTestMethodsUsing = lambda x, y: test_order.index(x) - test_order.index(y)
    unittest.main(testLoader=loader, verbosity=2)

Ausgabe:

test_foo (__main__.Test)
test foo ... ok
test_bar (__main__.Test)
test bar ... ok

Hier ist ein Proof-of-Concept zum Ausführen von Tests in der Quellcode-Reihenfolge anstelle der lexikalischen Standardreihenfolge (Ausgabe wie oben).

import inspect
import unittest

class Test(unittest.TestCase):
    def test_foo(self):
        """ test foo """
        self.assertEqual(1, 1)

    def test_bar(self):
        """ test bar """
        self.assertEqual(1, 1)

if __name__ == "__main__":
    test_src = inspect.getsource(Test)
    unittest.TestLoader.sortTestMethodsUsing = lambda _, x, y: (
        test_src.index(f"def {x}") - test_src.index(f"def {y}")
    )
    unittest.main(verbosity=2)

Ich habe Python 3.8.0 in diesem Beitrag verwendet.

ggorlen
quelle
0

Ein Ansatz kann darin bestehen, diese Untertests nicht als Tests nach unittestModulen zu behandeln, indem Sie sie anhängen _und dann einen Testfall erstellen, der auf der richtigen Reihenfolge dieser ausgeführten Unteroperationen aufbaut.

Dies ist besser, als sich auf die Sortierreihenfolge des unittestModuls zu verlassen, da sich dies morgen ändern könnte und auch das Erreichen einer topologischen Sortierung in der Reihenfolge nicht sehr einfach ist.

Ein Beispiel für diesen Ansatz, genommen von hier (Disclaimer: mein eigenes Modul) , ist als unten.

Hier führt der Testfall unabhängige Tests durch, z. B. die Überprüfung auf Tabellenparameter, die nicht gesetzt sind ( test_table_not_set) oder auf Primärschlüssel ( test_primary_key), die noch parallel sind. Der CRUD-Test ist jedoch nur dann sinnvoll, wenn er in der richtigen Reihenfolge und im richtigen Status durchgeführt wurde, die durch vorherige Operationen festgelegt wurden. Daher wurden diese Tests eher nur getrennt durchgeführt, unitaber nicht getestet. Ein anderer Test ( test_CRUD) erstellt dann eine richtige Reihenfolge dieser Operationen und testet sie.

import os
import sqlite3
import unittest

from sql30 import db

DB_NAME = 'review.db'


class Reviews(db.Model):
    TABLE = 'reviews'
    PKEY = 'rid'
    DB_SCHEMA = {
        'db_name': DB_NAME,
        'tables': [
            {
                'name': TABLE,
                'fields': {
                    'rid': 'uuid',
                    'header': 'text',
                    'rating': 'int',
                    'desc': 'text'
                    },
                'primary_key': PKEY
            }]
        }
    VALIDATE_BEFORE_WRITE = True

class ReviewTest(unittest.TestCase):

    def setUp(self):
        if os.path.exists(DB_NAME):
            os.remove(DB_NAME)

    def test_table_not_set(self):
        """
        Tests for raise of assertion when table is not set.
        """
        db = Reviews()
        try:
            db.read()
        except Exception as err:
            self.assertIn('No table set for operation', str(err))

    def test_primary_key(self):
        """
        Ensures , primary key is honored.
        """
        db = Reviews()
        db.table = 'reviews'
        db.write(rid=10, rating=5)
        try:
            db.write(rid=10, rating=4)
        except sqlite3.IntegrityError as err:
            self.assertIn('UNIQUE constraint failed', str(err))

    def _test_CREATE(self):
        db = Reviews()
        db.table = 'reviews'
        # backward compatibility for 'write' API
        db.write(tbl='reviews', rid=1, header='good thing', rating=5)

        # New API with 'create'
        db.create(tbl='reviews', rid=2, header='good thing', rating=5)

        # backward compatibility for 'write' API, without tbl,
        # explicitly passed
        db.write(tbl='reviews', rid=3, header='good thing', rating=5)

        # New API with 'create', without table name explicitly passed.
        db.create(tbl='reviews', rid=4, header='good thing', rating=5)

        db.commit()   # save the work.

    def _test_READ(self):
        db = Reviews()
        db.table = 'reviews'

        rec1 = db.read(tbl='reviews', rid=1, header='good thing', rating=5)
        rec2 = db.read(rid=1, header='good thing')
        rec3 = db.read(rid=1)

        self.assertEqual(rec1, rec2)
        self.assertEqual(rec2, rec3)

        recs = db.read()  # read all
        self.assertEqual(len(recs), 4)

    def _test_UPDATE(self):
        db = Reviews()
        db.table = 'reviews'

        where = {'rid': 2}
        db.update(condition=where, header='average item', rating=2)
        db.commit()

        rec = db.read(rid=2)[0]
        self.assertIn('average item', rec)

    def _test_DELETE(self):
        db = Reviews()
        db.table = 'reviews'

        db.delete(rid=2)
        db.commit()
        self.assertFalse(db.read(rid=2))

    def test_CRUD(self):
        self._test_CREATE()
        self._test_READ()
        self._test_UPDATE()
        self._test_DELETE()

    def tearDown(self):
        os.remove(DB_NAME)
ViFI
quelle