Продолжаем в юнит-тесте Python, когда утверждение не выполняется - PullRequest
70 голосов
/ 19 января 2011

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

Я хотел бы написать модульные тесты на 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__ в Car правильно устанавливает свои поля. Я мог бы разбить его на четыре метода (и это часто отличная идея), но в этом случае я думаю, что удобнее читать его как единый метод, который тестирует единый концепт («объект инициализирован правильно»).

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

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

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

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


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

Ответы [ 10 ]

37 голосов
/ 17 февраля 2011

Еще один способ получить несмертельные утверждения - захватить исключение подтверждения и сохранить исключения в списке. Затем подтвердите, что этот список пуст как часть 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()
26 голосов
/ 10 января 2012

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

Например:

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

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

9 голосов
/ 19 января 2011

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

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

8 голосов
/ 20 января 2011

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

6 голосов
/ 19 января 2011

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

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)
2 голосов
/ 30 октября 2017

ожидаемо очень полезно в gtest.Это путь Python в gist и код:

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
2 голосов
/ 28 мая 2017

Мне понравился подход @ Anthony-Batchelor для захвата исключения 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
1 голос
/ 03 июля 2018

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

0 голосов
/ 03 февраля 2019

У меня проблема с @ Энтони Батчелором ответом, потому что это вынуждает меня использовать 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
 : 

Дополнительные альтернативные решения для правильного захвата трассировки стека могут быть размещены на Как правильно переопределить TestCase.assertEqual (), производя правильную трассировку стека?

0 голосов
/ 19 января 2011

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

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

.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)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...