Domanda

EDIT:. Commutata ad un esempio migliore, e chiarito il motivo per cui questo è un vero problema

Mi piacerebbe scrivere unit test in Python che continuare l'esecuzione quando un'asserzione fallisce, in modo che posso vedere più errori in un singolo test. Ad esempio:

class Car(object):
  def __init__(self, make, model):
    self.make = make
    self.model = make  # Copy and paste error: should be model.
    self.has_seats = True
    self.wheel_count = 3  # Typo: should be 4.

class CarTest(unittest.TestCase):
  def test_init(self):
    make = "Ford"
    model = "Model T"
    car = Car(make=make, model=model)
    self.assertEqual(car.make, make)
    self.assertEqual(car.model, model)  # Failure!
    self.assertTrue(car.has_seats)
    self.assertEqual(car.wheel_count, 4)  # Failure!

Qui, lo scopo del test è quello di garantire che __init__ di auto imposta correttamente i suoi campi. Potrei suddividerlo in quattro metodi (e questo è spesso una grande idea), ma in questo caso penso che sia più leggibile per tenerlo come un unico metodo che mette alla prova un singolo concetto ( "l'oggetto è inizializzato correttamente").

Se si assume che è meglio qui per non rompere il metodo, poi ho un nuovo problema: non riesco a vedere tutti gli errori in una sola volta. Quando posso correggere l'errore model ed eseguire nuovamente il test, poi il compare di errore wheel_count. Mi farebbe risparmiare tempo di vedere entrambi gli errori quando ho eseguire il test.

Per fare un confronto, di Google C ++ quadro unit testing distingue tra tra asserzioni non fatali EXPECT_* e asserzioni ASSERT_* fatali:

Le affermazioni venire a coppie che prova la stessa cosa, ma hanno effetti diversi sulla funzione corrente. ASSERT_ * versioni generano errori fatali quando falliscono, e interrompere la funzione corrente. EXPECT_ * versioni generano fallimenti non fatali, che non interrompere la funzione corrente. Solitamente EXPECT_ * sono preferibili, in quanto consentono più di uno guasti da segnalare in una prova. Tuttavia, è necessario utilizzare ASSERT_ * se non ha senso continuare a quando l'affermazione in questione non riesce.

C'è un modo per ottenere EXPECT_* simile comportamento in unittest di Python? Se non in unittest, poi c'è un altro framework di unit test Python che supporta questo comportamento?


Per inciso, ero curioso di sapere quanti test di vita reale possono trarre vantaggio da affermazioni non fatali, così ho guardato un po ' esempi di codice (a cura 2014/08/19 per uso searchcode al posto di Google code Search, RIP). Di 10 risultati selezionati casualmente dalla prima pagina, tutti i test che hanno reso più asserzioni indipendenti nello stesso metodo di prova contenuta. Tutto sarebbe beneficiare di asserzioni non fatali.

È stato utile?

Soluzione

Quello che probabilmente si vorrà fare è unittest.TestCase derive dato che è la classe che lancia quando un'asserzione fallisce. Si dovrà ri-architetto vostra TestCase di non buttare (forse tenere un elenco di fallimenti, invece). Re-architecting roba può causare altri problemi che si devono risolvere. Per esempio si può finire per dover TestSuite Derive per apportare modifiche a sostegno delle modifiche apportate al TestCase.

Altri suggerimenti

Un altro modo per avere asserzioni non fatali è quello di catturare l'eccezione asserzione e memorizzare le eccezioni in un elenco. Quindi affermare che tale elenco è vuota come parte del tearDown.

import unittest

class Car(object):
  def __init__(self, make, model):
    self.make = make
    self.model = make  # Copy and paste error: should be model.
    self.has_seats = True
    self.wheel_count = 3  # Typo: should be 4.

class CarTest(unittest.TestCase):
  def setUp(self):
    self.verificationErrors = []

  def tearDown(self):
    self.assertEqual([], self.verificationErrors)

  def test_init(self):
    make = "Ford"
    model = "Model T"
    car = Car(make=make, model=model)
    try: self.assertEqual(car.make, make)
    except AssertionError, e: self.verificationErrors.append(str(e))
    try: self.assertEqual(car.model, model)  # Failure!
    except AssertionError, e: self.verificationErrors.append(str(e))
    try: self.assertTrue(car.has_seats)
    except AssertionError, e: self.verificationErrors.append(str(e))
    try: self.assertEqual(car.wheel_count, 4)  # Failure!
    except AssertionError, e: self.verificationErrors.append(str(e))

if __name__ == "__main__":
    unittest.main()

Una possibilità è assert su tutti i valori in una volta come una tupla.

Ad esempio:

class CarTest(unittest.TestCase):
  def test_init(self):
    make = "Ford"
    model = "Model T"
    car = Car(make=make, model=model)
    self.assertEqual(
            (car.make, car.model, car.has_seats, car.wheel_count),
            (make, model, True, 4))

L'output di questo test potrebbe essere:

======================================================================
FAIL: test_init (test.CarTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\temp\py_mult_assert\test.py", line 17, in test_init
    (make, model, True, 4))
AssertionError: Tuples differ: ('Ford', 'Ford', True, 3) != ('Ford', 'Model T', True, 4)

First differing element 1:
Ford
Model T

- ('Ford', 'Ford', True, 3)
?           ^ -          ^

+ ('Ford', 'Model T', True, 4)
?           ^  ++++         ^

Questo dimostra che sia il modello e il conteggio ruota sono corretti.

E 'considerato un anti-pattern per avere più asserisce in un singolo test di unità. Una singola unit test è previsto per testare una sola cosa. Forse si sta testando troppo. Pensare di dividere questo test fino in prove multiple. In questo modo è possibile assegnare un nome ogni test in modo corretto.

A volte però, è bene controllare più cose allo stesso tempo. Per esempio quando si stanno affermando le proprietà dello stesso oggetto. In tal caso, si sono infatti affermando se tale oggetto è corretta. Un modo per farlo è quello di scrivere un metodo di supporto personalizzato che sa far valere su tale oggetto. È possibile scrivere tale metodo in tal modo un che mostra tutte le proprietà mancanza o per esempio mostra lo stato completa dell'oggetto previsto e lo stato completa dell'oggetto effettivo quando un'asserzione fallisce.

fare ogni assert in un metodo separato.

class MathTest(unittest.TestCase):
  def test_addition1(self):
    self.assertEqual(1 + 0, 1)

  def test_addition2(self):
    self.assertEqual(1 + 1, 3)

  def test_addition3(self):
    self.assertEqual(1 + (-1), 0)

  def test_addition4(self):
    self.assertEqaul(-1 + (-1), -1)

Mi è piaciuto il metodo da @ Anthony-Batchelor, per catturare l'eccezione AssertionError. Ma una lieve variazione a questo approccio utilizzando decoratori e anche un modo per segnalare i casi di test con pass / fail.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import unittest

class UTReporter(object):
    '''
    The UT Report class keeps track of tests cases
    that have been executed.
    '''
    def __init__(self):
        self.testcases = []
        print "init called"

    def add_testcase(self, testcase):
        self.testcases.append(testcase)

    def display_report(self):
        for tc in self.testcases:
            msg = "=============================" + "\n" + \
                "Name: " + tc['name'] + "\n" + \
                "Description: " + str(tc['description']) + "\n" + \
                "Status: " + tc['status'] + "\n"
            print msg

reporter = UTReporter()

def assert_capture(*args, **kwargs):
    '''
    The Decorator defines the override behavior.
    unit test functions decorated with this decorator, will ignore
    the Unittest AssertionError. Instead they will log the test case
    to the UTReporter.
    '''
    def assert_decorator(func):
        def inner(*args, **kwargs):
            tc = {}
            tc['name'] = func.__name__
            tc['description'] = func.__doc__
            try:
                func(*args, **kwargs)
                tc['status'] = 'pass'
            except AssertionError:
                tc['status'] = 'fail'
            reporter.add_testcase(tc)
        return inner
    return assert_decorator



class DecorateUt(unittest.TestCase):

    @assert_capture()
    def test_basic(self):
        x = 5
        self.assertEqual(x, 4)

    @assert_capture()
    def test_basic_2(self):
        x = 4
        self.assertEqual(x, 4)

def main():
    #unittest.main()
    suite = unittest.TestLoader().loadTestsFromTestCase(DecorateUt)
    unittest.TextTestRunner(verbosity=2).run(suite)

    reporter.display_report()


if __name__ == '__main__':
    main()

Output da console:

(awsenv)$ ./decorators.py 
init called
test_basic (__main__.DecorateUt) ... ok
test_basic_2 (__main__.DecorateUt) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK
=============================
Name: test_basic
Description: None
Status: fail

=============================
Name: test_basic_2
Description: None
Status: pass

aspettare è molto utile in GTEST. Questo è il modo in pitone Gist , e il codice:

import sys
import unittest


class TestCase(unittest.TestCase):
    def run(self, result=None):
        if result is None:
            self.result = self.defaultTestResult()
        else:
            self.result = result

        return unittest.TestCase.run(self, result)

    def expect(self, val, msg=None):
        '''
        Like TestCase.assert_, but doesn't halt the test.
        '''
        try:
            self.assert_(val, msg)
        except:
            self.result.addFailure(self, sys.exc_info())

    def expectEqual(self, first, second, msg=None):
        try:
            self.failUnlessEqual(first, second, msg)
        except:
            self.result.addFailure(self, sys.exc_info())

    expect_equal = expectEqual

    assert_equal = unittest.TestCase.assertEqual
    assert_raises = unittest.TestCase.assertRaises


test_main = unittest.main

Esiste un pacchetto asserzione morbido in PyPI chiamato softest che consente di gestire le vostre esigenze. Funziona attraverso la raccolta di fallimenti, combinando i dati di eccezione e traccia dello stack, e reporting tutto come parte della solita uscita unittest.

Per esempio, questo codice:

import softest

class ExampleTest(softest.TestCase):
    def test_example(self):
        # be sure to pass the assert method object, not a call to it
        self.soft_assert(self.assertEqual, 'Worf', 'wharf', 'Klingon is not ship receptacle')
        # self.soft_assert(self.assertEqual('Worf', 'wharf', 'Klingon is not ship receptacle')) # will not work as desired
        self.soft_assert(self.assertTrue, True)
        self.soft_assert(self.assertTrue, False)

        self.assert_all()

if __name__ == '__main__':
    softest.main()

... produce questo output della console:

======================================================================
FAIL: "test_example" (ExampleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\...\softest_test.py", line 14, in test_example
    self.assert_all()
  File "C:\...\softest\case.py", line 138, in assert_all
    self.fail(''.join(failure_output))
AssertionError: ++++ soft assert failure details follow below ++++

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
The following 2 failures were found in "test_example" (ExampleTest):
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Failure 1 ("test_example" method)
+--------------------------------------------------------------------+
Traceback (most recent call last):
  File "C:\...\softest_test.py", line 10, in test_example
    self.soft_assert(self.assertEqual, 'Worf', 'wharf', 'Klingon is not ship receptacle')
  File "C:\...\softest\case.py", line 84, in soft_assert
    assert_method(*arguments, **keywords)
  File "C:\...\Python\Python36-32\lib\unittest\case.py", line 829, in assertEqual
    assertion_func(first, second, msg=msg)
  File "C:\...\Python\Python36-32\lib\unittest\case.py", line 1203, in assertMultiLineEqual
    self.fail(self._formatMessage(msg, standardMsg))
  File "C:\...\Python\Python36-32\lib\unittest\case.py", line 670, in fail
    raise self.failureException(msg)
AssertionError: 'Worf' != 'wharf'
- Worf
+ wharf
 : Klingon is not ship receptacle

+--------------------------------------------------------------------+
Failure 2 ("test_example" method)
+--------------------------------------------------------------------+
Traceback (most recent call last):
  File "C:\...\softest_test.py", line 12, in test_example
    self.soft_assert(self.assertTrue, False)
  File "C:\...\softest\case.py", line 84, in soft_assert
    assert_method(*arguments, **keywords)
  File "C:\...\Python\Python36-32\lib\unittest\case.py", line 682, in assertTrue
    raise self.failureException(msg)
AssertionError: False is not true


----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

Nota : ho creato e mantengo softest

Non credo ci sia un modo per fare questo con PyUnit e non vorrebbe vedere PyUnit esteso in questo modo.

preferisco attenersi a un'affermazione per ogni funzione di test ( o più specificamente affermando un concetto per test ) e sarebbe riscrivere test_addition() come quattro funzioni di test separati. Questo darebbe informazioni più utili in caso di fallimento, e cioè :

.FF.
======================================================================
FAIL: test_addition_with_two_negatives (__main__.MathTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_addition.py", line 10, in test_addition_with_two_negatives
    self.assertEqual(-1 + (-1), -1)
AssertionError: -2 != -1

======================================================================
FAIL: test_addition_with_two_positives (__main__.MathTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_addition.py", line 6, in test_addition_with_two_positives
    self.assertEqual(1 + 1, 3)  # Failure!
AssertionError: 2 != 3

----------------------------------------------------------------------
Ran 4 tests in 0.000s

FAILED (failures=2)

Se si decide che questo approccio non è per voi, si possono trovare questa risposta utile.

Aggiorna

Sembra che si sta testando due concetti con la tua domanda aggiornato e vorrei dividere questi in due test di unità. Il primo è che i parametri vengono memorizzati sulla creazione di un nuovo oggetto. Ciò avrebbe due asserzioni, una per make e uno per model. Se la prima fallisce, il che deve chiaramente da fissare, se il secondo passaggio o non è irrilevante in questo frangente.

Il secondo concetto è più discutibile ... si sta testando se alcuni valori di default vengono inizializzati. Perché ? Sarebbe più utile per testare questi valori al punto che essi sono effettivamente utilizzati (e se non vengono utilizzati, allora perché sono lì?).

Entrambi questi test falliscono, ed entrambi dovrebbero. Quando sono unit test, io sono molto più interessato a fallimento di me nel successo come quello è dove ho bisogno di concentrarmi.

FF
======================================================================
FAIL: test_creation_defaults (__main__.CarTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_car.py", line 25, in test_creation_defaults
    self.assertEqual(self.car.wheel_count, 4)  # Failure!
AssertionError: 3 != 4

======================================================================
FAIL: test_creation_parameters (__main__.CarTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_car.py", line 20, in test_creation_parameters
    self.assertEqual(self.car.model, self.model)  # Failure!
AssertionError: 'Ford' != 'Model T'

----------------------------------------------------------------------
Ran 2 tests in 0.000s

FAILED (failures=2)

Ho un problema con la @Anthony Batchelor risposta, perché costringe-me di utilizzare try...catch all'interno mio test di unità. Poi, ho incapsulato la logica try...catch in un override del metodo TestCase.assertEqual. Il seguente trucco rimuovere i blocchi try...catch dalla unit test del codice:

import unittest
import traceback

class AssertionErrorData(object):

    def __init__(self, stacktrace, message):
        super(AssertionErrorData, self).__init__()
        self.stacktrace = stacktrace
        self.message = message

class MultipleAssertionFailures(unittest.TestCase):

    def __init__(self, *args, **kwargs):
        self.verificationErrors = []
        super(MultipleAssertionFailures, self).__init__( *args, **kwargs )

    def tearDown(self):
        super(MultipleAssertionFailures, self).tearDown()

        if self.verificationErrors:
            index = 0
            errors = []

            for error in self.verificationErrors:
                index += 1
                errors.append( "%s\nAssertionError %s: %s" % ( 
                        error.stacktrace, index, error.message ) )

            self.fail( '\n\n' + "\n".join( errors ) )
            self.verificationErrors.clear()

    def assertEqual(self, goal, results, msg=None):

        try:
            super( MultipleAssertionFailures, self ).assertEqual( goal, results, msg )

        except unittest.TestCase.failureException as error:
            goodtraces = self._goodStackTraces()
            self.verificationErrors.append( 
                    AssertionErrorData( "\n".join( goodtraces[:-2] ), error ) )

    def _goodStackTraces(self):
        """
            Get only the relevant part of stacktrace.
        """
        stop = False
        found = False
        goodtraces = []

        # stacktrace = traceback.format_exc()
        # stacktrace = traceback.format_stack()
        stacktrace = traceback.extract_stack()

        # https://stackoverflow.com/questions/54499367/how-to-correctly-override-testcase
        for stack in stacktrace:
            filename = stack.filename

            if found and not stop and \
                    not filename.find( 'lib' ) < filename.find( 'unittest' ):
                stop = True

            if not found and filename.find( 'lib' ) < filename.find( 'unittest' ):
                found = True

            if stop and found:
                stackline = '  File "%s", line %s, in %s\n    %s' % ( 
                        stack.filename, stack.lineno, stack.name, stack.line )
                goodtraces.append( stackline )

        return goodtraces

# class DummyTestCase(unittest.TestCase):
class DummyTestCase(MultipleAssertionFailures):

    def setUp(self):
        self.maxDiff = None
        super(DummyTestCase, self).setUp()

    def tearDown(self):
        super(DummyTestCase, self).tearDown()

    def test_function_name(self):
        self.assertEqual( "var", "bar" )
        self.assertEqual( "1937", "511" )

if __name__ == '__main__':
    unittest.main()

uscita Risultato:

F
======================================================================
FAIL: test_function_name (__main__.DummyTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\User\Downloads\test.py", line 77, in tearDown
    super(DummyTestCase, self).tearDown()
  File "D:\User\Downloads\test.py", line 29, in tearDown
    self.fail( '\n\n' + "\n\n".join( errors ) )
AssertionError: 

  File "D:\User\Downloads\test.py", line 80, in test_function_name
    self.assertEqual( "var", "bar" )
AssertionError 1: 'var' != 'bar'
- var
? ^
+ bar
? ^
 : 

  File "D:\User\Downloads\test.py", line 81, in test_function_name
    self.assertEqual( "1937", "511" )
AssertionError 2: '1937' != '511'
- 1937
+ 511
 : 

soluzioni più alternative per la cattura stacktrace corretta potrebbe essere pubblicato su Come ignorare correttamente TestCase.assertEqual (), che produce lo stacktrace giusto?

Mi rendo conto che questa domanda è stato chiesto letteralmente anni fa, ma ora ci sono (almeno) due pacchetti di Python che ti permettono di fare questo.

Una è più morbido: https://pypi.org/project/softest/

L'altro è Python-Delayed-Assert: https://github.com/pr4bh4sh/ python-ritardo-assert

Non ho usato sia, ma guardare piuttosto simile a me.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top