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


EDIT: переключился на лучший пример и выяснил, почему это реальная проблема.

Я хотел бы написать модульные тесты на 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 ошибка и повторите тест, затем ошибка. Это сэкономит мне время, чтобы увидеть обе ошибки при первом запуске теста.

для сравнения, Google c++ unit testing framework отличает между между несмертельных EXPECT_* утверждения и смертельных ASSERT_* утверждения:

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

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


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

9 68

9 ответов:

что вы, вероятно, хотите сделать, это вывести 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)

мне понравился подход @Anthony-Batchelor, чтобы захватить исключение AssertionError. Но небольшое изменение этого подхода с использованием декораторов, а также способ сообщить о тестовых случаях с 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()

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

(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 в суть, и код:

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 (или более конкретно утверждение одной концепции на тест) и переписать test_addition() как 4 отдельных функции теста. Это даст больше полезной информации о сбое,, а именно:

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