Python unittest - навпроти assrtRaises?


374

Я хочу написати тест, щоб встановити, що Виняток не порушений за певних обставин.

Це просто для тестування , якщо виключення буде піднято ...

sInvalidPath=AlwaysSuppliesAnInvalidPath()
self.assertRaises(PathIsNotAValidOne, MyObject, sInvalidPath) 

... але як можна зробити навпаки .

Щось подібне до цього, про що я пішов ...

sValidPath=AlwaysSuppliesAValidPath()
self.assertNotRaises(PathIsNotAValidOne, MyObject, sValidPath) 

6
Ви завжди могли просто зробити все, що вам слід працювати в тесті. Якщо вона призведе до помилки, вона з’явиться (вважається помилкою, а не помилкою). Звичайно, це передбачає, що це не викликає помилок, а не лише певний тип помилок. Крім цього, я думаю, вам доведеться написати своє.
Томас К


Виявляється, ви насправді можете реалізувати assertNotRaisesметод, який розділяє 90% свого коду / поведінки assertRaisesприблизно в ~ 30-іш рядках коду. Детальну інформацію див. У моїй відповіді нижче .
тел

Я хочу цього, щоб я міг порівняти дві функції, hypothesisщоб переконатися, що вони виробляють однаковий вихід для всіх видів вводу, ігноруючи випадки, коли оригінал викликає виняток. assume(func(a))не працює, оскільки вихід може бути масивом з неоднозначним значенням істини. Тому я просто хочу зателефонувати до функції і отримати, Trueякщо вона не виходить з ладу. assume(func(a) is not None)роботи я думаю
ендоліти

Відповіді:


394
def run_test(self):
    try:
        myFunc()
    except ExceptionType:
        self.fail("myFunc() raised ExceptionType unexpectedly!")

32
@hiwaylon - Ні, це справді правильне рішення. Рішення, запропоноване користувачем9876, є концептуально помилковим: якщо ви перевіряєте на неповторність казки ValueError, але ValueErrorзамість цього підняли, ваш тест повинен вийти із умови відмови, а не помилки. З іншого боку, якщо при запуску одного і того ж коду ви піднімете a KeyError, це була б помилка, а не збій. У python - інакше, ніж у деяких інших мовах - винятки звичайно використовуються для управління потоком, тому ми except <ExceptionName>справді маємо синтаксис. З цього приводу рішення user9876 просто неправильне.
мак

@mac - Це теж правильне рішення? stackoverflow.com/a/4711722/6648326
MasterJoe2

Цей невдалий ефект показує <100% охоплення (за винятком випадків, коли це не відбудеться) для тестів.
Шай

3
@Shay, IMO, ви завжди повинні виключати самі тестові файли із звітів про покриття (оскільки вони майже завжди працюють 100% за визначенням, ви штучно надуваєте звіти)
Оригінальний соус для барбекю

@ оригінальний-bbq-соус, чи не залишив би мене відкритим для ненавмисних пропущених тестів. Наприклад, помилка в тестовому імені (ttst_function), неправильна конфігурація запуску в pycharm тощо?
Шай

67

Привіт - я хочу написати тест, щоб встановити, що Виняток не порушений за певних обставин.

Це припущення за замовчуванням - винятки не збільшуються.

Якщо ви нічого не говорите, це передбачається в кожному тесті.

Не потрібно насправді писати жодне твердження для цього.


7
@IndradhanushGupta Добре прийнята відповідь робить тест більш пітонічним, ніж цей. Явне краще, ніж неявне.
0xc0de

17
Жоден інший коментатор не вказав, чому ця відповідь неправильна, хоча це та сама причина, що відповідь user9876 неправильна: відмови та помилки - це різні речі тестового коду. Якщо ваша функція полягала в тому, щоб викинути виняток під час тесту, який не стверджує, тестова рамка трактуватиме це як помилку , а не як невідповідність.
coredumperror

@CoreDumpError Я розумію різницю між помилкою та помилкою, але чи не змусило б вас оточити кожен тест конструкцією спроб / виключення? Або ви б рекомендували робити це лише для тестів, які явно підвищують виняток за певної умови (що в основному означає, що виняток очікується ).
federicojasson

4
@federicojasson Ви досить добре відповіли на своє запитання у другому реченні. Помилки та збої в тестах можна коротко охарактеризувати як "несподівані збої" проти "ненавмисну ​​поведінку" відповідно. Ви хочете, щоб ваші тести показали помилку, коли ваша функція припиняється, але не тоді, коли виняток, який ви знаєте, буде кинутий, якщо певні входи підкидаються, коли даються різні входи.
coredumperror

52

Просто викличте функцію. Якщо він викликає виняток, блок тестування модуля позначить це як помилку. Ви можете додати коментар, наприклад:

sValidPath=AlwaysSuppliesAValidPath()
# Check PathIsNotAValidOne not thrown
MyObject(sValidPath)

35
Відмови та помилки концептуально різні. Крім того, оскільки в python винятки звичайно використовуються для управління потоком, це буде дуже важко зрозуміти з першого погляду (= не вивчаючи тестовий код), якщо ви порушили свою логіку чи свій код ...
mac

1
Або ваш тест проходить, або його немає. Якщо це не пройде, вам доведеться виправити це. Незалежно від того, чи не повідомляється про "провал" чи "помилку", це неактуально. Є одна відмінність: з моєї відповіді ви побачите слід стека, щоб ви могли бачити, куди було кинуто PathIsNotAValidOne; з прийнятою відповіддю у вас не буде цієї інформації, тому налагодження буде складніше. (Припустимо, Py2; не впевнений, чи краще Py3 в цьому).
user9876

19
@ user9876 - Ні. Умови випробувального виходу 3 (пропуск / nopass / помилка), а не 2, як ви, здається, помилково вважаєте. Різниця між помилками та відмовами істотна, і трактувати їх так, ніби вони однакові, - це лише погане програмування. Якщо ви не вірите мені, просто погляньте навколо, як працюють тестові бігуни та які дерева рішень вони реалізують для відмов і помилок. Хороша відправна точка для пітона декоратор в pytest. xfail
мак

4
Я думаю, це залежить від того, як ви використовуєте одиничні тести. Те, як моя команда використовує одиничні тести, повинні пройти всі. (Agile програмування, з машиною безперервної інтеграції, яка виконує всі одиничні тести). Я знаю, що тестовий випадок може повідомити про "пропуск", "збій" або "помилку". Але на високому рівні, що насправді має значення для моєї команди, - "чи проходять усі одиничні тести?" (тобто "зелений Дженкінс?"). Тож для моєї команди практично немає різниці між "провалом" та "помилкою". У вас можуть бути різні вимоги, якщо ви використовуєте тести своїх пристроїв по-іншому.
user9876

1
@ user9876 різниця між "fail" та "error" - це різниця між "моїм твердженням не вдалося" та "моїм тестом навіть не дійти до затвердження". Це, на мій погляд, корисна відмінність під час фіксації тестів, але я гадаю, як ви кажете, не для всіх.
CS

14

Я оригінальний плакат, і я прийняв вищезгадану відповідь DGH, не попередньо використовуючи його в коді.

Як тільки я застосував, я зрозумів, що для цього потрібно трохи підправити, щоб зробити те, що я потребував (щоб бути справедливим до DGH, він сказав, що "чи щось подібне"!).

Я подумав, що варто опублікувати налаштування тут на благо інших:

    try:
        a = Application("abcdef", "")
    except pySourceAidExceptions.PathIsNotAValidOne:
        pass
    except:
        self.assertTrue(False)

Я намагався тут зробити, це переконатися, що якщо буде зроблена спроба інстанціювати об’єкт Application за допомогою другого аргументу пробілів, pySourceAidExceptions.PathIsNotAValidOne буде піднятий.

Я вірю, що використання цього коду (який базується на відповіді DGH) зробить це.


2
Оскільки ви роз'яснюєте своє запитання і не відповідаєте на нього, ви мусили його відредагувати (не відповіли на нього).
hiwaylon

13
Здається, це зовсім протилежне початковій проблемі. self.assertRaises(PathIsNotAValidOne, MyObject, sInvalidPath)повинен виконати роботу в цьому випадку.
Антоні Хеткінс

8

Ви можете визначити assertNotRaisesшляхом повторного використання близько 90% від первісної реалізації assertRaisesв unittestмодулі. При такому підході ви закінчуєте assertNotRaisesметод, який, окрім його зворотного стану відмови, поводиться однаково assertRaises.

TLDR та демо-версія

Додавати assertNotRaisesметод до напрочуд легко unittest.TestCase(мені знадобилося приблизно 4 рази більше часу, щоб написати цю відповідь, як це робив код). Ось жива демонстрація assertNotRaisesметоду в дії . Так само, якassertRaises ви можете або передати позивний і аргумент до assertNotRaises, або ви можете використовувати його у withзаяві. Демонстраційна демонстрація включає тестові приклади, які демонструють, що assertNotRaisesпрацює за призначенням.

Деталі

Реалізація assertRaisesв unittestдосить складна, але, маючи трохи розумного підкласингу, ви можете переосмислити та змінити умови його відмови.

assertRaisesце короткий метод, який в основному просто створює екземпляр unittest.case._AssertRaisesContextкласу і повертає його (див. його визначення в unittest.caseмодулі). Ви можете визначити свій власний _AssertNotRaisesContextклас, підкласифікувавши _AssertRaisesContextта змінивши його __exit__метод:

import traceback
from unittest.case import _AssertRaisesContext

class _AssertNotRaisesContext(_AssertRaisesContext):
    def __exit__(self, exc_type, exc_value, tb):
        if exc_type is not None:
            self.exception = exc_value.with_traceback(None)

            try:
                exc_name = self.expected.__name__
            except AttributeError:
                exc_name = str(self.expected)

            if self.obj_name:
                self._raiseFailure("{} raised by {}".format(exc_name,
                    self.obj_name))
            else:
                self._raiseFailure("{} raised".format(exc_name))

        else:
            traceback.clear_frames(tb)

        return True

Зазвичай ви визначаєте класи тестових випадків, успадковуючи їх TestCase. Якщо ви замість цього успадковуєте підклас MyTestCase:

class MyTestCase(unittest.TestCase):
    def assertNotRaises(self, expected_exception, *args, **kwargs):
        context = _AssertNotRaisesContext(expected_exception, self)
        try:
            return context.handle('assertNotRaises', args, kwargs)
        finally:
            context = None

у всіх ваших тестових випадках тепер буде assertNotRaisesдоступний метод.


Звідки ви tracebackу своєму elseзаяві?
NOhs

1
@NOhs Пропав безвісти import. Його встановлено
тел.

2
def _assertNotRaises(self, exception, obj, attr):                                                                                                                              
     try:                                                                                                                                                                       
         result = getattr(obj, attr)                                                                                                                                            
         if hasattr(result, '__call__'):                                                                                                                                        
             result()                                                                                                                                                           
     except Exception as e:                                                                                                                                                     
         if isinstance(e, exception):                                                                                                                                           
            raise AssertionError('{}.{} raises {}.'.format(obj, attr, exception)) 

може бути змінено, якщо вам потрібно прийняти параметри.

дзвінок як

self._assertNotRaises(IndexError, array, 'sort')

1

Я вважаю корисним мавпа-патч unittestнаступним чином:

def assertMayRaise(self, exception, expr):
  if exception is None:
    try:
      expr()
    except:
      info = sys.exc_info()
      self.fail('%s raised' % repr(info[0]))
  else:
    self.assertRaises(exception, expr)

unittest.TestCase.assertMayRaise = assertMayRaise

Це пояснює наміри під час тестування на відсутність винятку:

self.assertMayRaise(None, does_not_raise)

Це також спрощує тестування в циклі, що мені часто здається:

# ValueError is raised only for op(x,x), op(y,y) and op(z,z).
for i,(a,b) in enumerate(itertools.product([x,y,z], [x,y,z])):
  self.assertMayRaise(None if i%4 else ValueError, lambda: op(a, b))

Що таке мавпа-патч?
ScottMcC

1
Дивіться en.wikipedia.org/wiki/Monkey_patch . Після додавання assertMayRaiseдо unittest.TestSuiteвам може просто робити вигляд , що частина unittestбібліотеки.
AndyJost

0

Якщо ви assertRaises()передаєте клас винятку до , надається менеджер контексту. Це може покращити читабельність ваших тестів:

# raise exception if Application created with bad data
with self.assertRaises(pySourceAidExceptions.PathIsNotAValidOne):
    application = Application("abcdef", "")

Це дозволяє перевірити випадки помилок у вашому коді.

У цьому випадку ви тестуєте значення PathIsNotAValidOneпіднімається при передачі недійсних параметрів конструктору програми.


1
Ні, це не вдасться лише в тому випадку, якщо виняток не буде піднято в блоці менеджера контексту. Можна легко перевірити за допомогою "self.assertRaises (TypeError): підняти TypeError", який проходить.
Меттью Тревор

@MatthewTrevor Добрий дзвінок. Як я пам'ятаю, замість тестування коду виконується правильно, тобто не підвищується, я пропонував тестувати випадки помилок. Відповідно я відредагував відповідь. Сподіваюся, я можу заробити +1, щоб вийти з червоного. :)
hiwaylon

Зауважте, це також Python 2.7 та пізніші версії: docs.python.org/2/library/…
qneill

0

ви можете спробувати так. спробуйте: self.assertRaises (None, функція, arg1, arg2) за винятком: передайте, якщо ви не помістите код всередину блоку спробу, це буде винятком "AssertionError: None не піднято", і тестовий випадок буде невдалим. якщо поставити всередині блоку спробу, який очікується поведінки.


0

Один прямий спосіб забезпечити ініціалізацію об'єкта без будь-якої помилки - перевірка екземпляра типу об'єкта.

Ось приклад:

p = SomeClass(param1=_param1_value)
self.assertTrue(isinstance(p, SomeClass))
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.