Продолжение в питоне, когда утверждение не удается

StackOverflow https://stackoverflow.com/questions/4732827

  •  12-10-2019
  •  | 
  •  

Вопрос

РЕДАКТИРОВАТЬ: Переключится на лучший пример и пояснил, почему это реальная проблема.

Я хотел бы написать модульные тесты на Python, которые продолжают выполнять, когда утверждение не удается, чтобы я мог видеть несколько сбоев в одном тесте. Например:

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!

Здесь цель теста состоит в том, чтобы гарантировать, что автомобиль __init__ Устанавливает свои поля правильно. Я мог бы разбить его на четыре метода (и это часто отличная идея), но в этом случае я думаю, что это более читабельно, чтобы сохранить его как единственный метод, который проверяет одну концепцию («объект инициализируется правильно»).

Если мы предположим, что здесь лучше не разрушать метод, то у меня есть новая проблема: я не вижу всех ошибок одновременно. Когда я исправляю model ошибка и повторно запустить тест, затем wheel_count появляется ошибка. Это сэкономило бы мне время, чтобы увидеть обе ошибки, когда я впервые запускаю тест.

Для сравнения, платформу модульного тестирования Google C ++ различает между Между некалистым EXPECT_* Утверждения и смертельные ASSERT_* Утверждения:

Утверждения приходят в парах, которые проверяют одно и то же, но имеют разные эффекты на текущую функцию. Версии assert_* генерируют фатальные сбои, когда они терпят неудачу, и прерывают текущую функцию. Версии ожидают, что генерируют нефатальные сбои, которые не прерывают текущую функцию. Обычно ожидают_* предпочтительнее, так как они позволяют сообщать о нескольких неудачах в тесте. Тем не менее, вы должны использовать Assert_*, если не имеет смысла продолжать, когда рассматриваемое утверждение не удается.

Есть ли способ получить EXPECT_*-Поподобное поведение в Python unittest? Если не в unittest, Тогда есть еще одна структура теста на блок Python, которая поддерживает такое поведение?


Между прочим, мне было любопытно, сколько реальных тестов может выиграть от нерадостных утверждений, поэтому я посмотрел на некоторые примеры кода (Отредактировано 2014-08-19 для использования SearchCode вместо поиска кода Google, RIP). Из 10 случайно выбранных результатов с первой страницы все содержали тесты, которые сделали несколько независимых утверждений в одном и том же методе испытаний. Все выиграют от нерадостных утверждений.

Это было полезно?

Решение

То, что вы, вероятно, захотите сделать, это вывести unittest.TestCase Так как это класс, который бросает, когда утверждение терпит неудачу. Вам придется повторно архитектировать свой TestCase не бросить (возможно, вместо этого хранить список неудач). Повторное архитекция вещей может вызвать другие проблемы, которые вам придется решить. Например, вам может понадобиться получить TestSuite Чтобы внести изменения в поддержку изменений, внесенных в ваш TestCase.

Другие советы

Другим способом иметь не лечебные утверждения является захват исключения утверждения и сохранение исключений в списке. Затем утверждайте, что этот список пуст как часть разрыва.

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()

Одним из вариантов является утверждение на всех значениях одновременно как кортеж.

Например:

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))

Вывод из этих тестов будет:

======================================================================
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)
?           ^  ++++         ^

Это показывает, что и модель, и количество колес неверны.

Считается анти-паттерном, чтобы иметь несколько утверждений в одном модульном тесте. Ожидается, что один модульный тест проверит только одну вещь. Возможно, вы слишком много проверяете. Рассмотрите возможность разделения этого теста на несколько тестов. Таким образом, вы можете правильно назвать каждый тест.

Иногда, однако, нормально проверять несколько вещей одновременно. Например, когда вы утверждаете свойства одного и того же объекта. В этом случае вы на самом деле утверждаете, является ли этот объект правильным. Способ сделать это - написать пользовательский вспомогательный метод, который знает, как утверждать на этом объекте. Вы можете написать этот метод таким образом, что он показывает все сбои свойств или, например, показывает полное состояние ожидаемого объекта и полное состояние фактического объекта, когда утверждение сбои.

Сделайте каждое утверждение отдельным методом.

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)

Мне понравился подход @Энтони-Бэтчелор, чтобы запечатлеть исключение AssertionError. Но небольшой вариант этого подхода с использованием декораторов, а также способ сообщить о случаях тестов с проходом/неудачей.

#!/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()

Вывод из консоли:

(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

Ожидайте очень полезно в GTEST. Это Python Way в суть, и код:

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

В PYPI есть мягкий пакет утверждений softest Это будет справляться с вашими требованиями. Он работает, собирая сбои, объединяя данные исключения и стека и отчитывая все это как часть обычного unittest выход.

Например, этот код:

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()

... производит эту консольную вывод:

======================================================================
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)

ПРИМЕЧАНИЕ: Я создал и поддерживаю softest.

Я не думаю, что есть способ сделать это с Pyunit, и не хотел бы видеть, как Pyunit расширяется таким образом.

Я предпочитаю придерживаться одного утверждения на тестовую функцию (или, более конкретно, утверждая одну концепцию за тест) и переписал бы test_addition() как четыре отдельных тестовых функции. Это дало бы более полезную информацию о неудаче, а именно:

.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)

Если вы решите, что этот подход не для вас, вы можете найти этот ответ полезный.

Обновлять

Похоже, вы тестируете две концепции с вашим обновленным вопросом, и я разделил бы их на два модульных теста. Во -первых, параметры хранятся при создании нового объекта. У этого будет два утверждения, одно для make и один для model. Анкет Если первое не удалось, это явно должно быть исправлено, независимо от того, проходит ли второй или терпит неудачу, не имеет значения на этот момент.

Вторая концепция более сомнительна ... вы проверяете, инициализированы ли некоторые значения по умолчанию. Почему? Было бы более полезно проверить эти значения в тот момент, когда они фактически используются (и если они не используются, то почему они там?).

Оба эти теста терпят неудачу, и оба должны. Когда я испытываю на единицу тестирования, я гораздо больше заинтересован в неудаче, чем на успех, так как именно мне нужно сосредоточиться.

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)

У меня проблема с @Anthony Batchelor ответить, потому что он заставляет меня использовать try...catch Внутри моих модульных тестов. Затем я заключил try...catch логика в переопределении TestCase.assertEqual метод Следующий взлом удалил try...catch Блоки от кода модульных тестов:

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()

Результат вывод:

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
 : 

Можно опубликовать больше альтернативных решений для правильного захвата Stacktrace. Как правильно переопределить testcase.assertequal (), создавая правильную Stacktrace?

Я понимаю, что этот вопрос был задан буквально много лет назад, но теперь есть (по крайней мере) два пакета Python, которые позволяют вам сделать это.

Один мягкий: https://pypi.org/project/softest/

Другой-Python Delayed-Assert: https://github.com/pr4bh4sh/python-delayed-assert

Я тоже не использовал, но они очень похожи на меня.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top