Comment testez-vous qu'une fonction Python lève une exception?

Réponses:

679

Utilisez TestCase.assertRaises(ou TestCase.failUnlessRaises) à partir du module unittest, par exemple:

import mymod

class MyTestCase(unittest.TestCase):
    def test1(self):
        self.assertRaises(SomeCoolException, mymod.myfunc)
Moe
la source
9
existe-t-il un moyen de faire le contraire? Comme si cela échouait seulement si la fonction lançait l'exception?
BUInvent
67
Notez que pour transmettre des arguments, myfuncvous devez les ajouter en tant qu'arguments à l'appel assertRaises. Voir la réponse de Daryl Spitzer.
cbron
1
existe-t-il un moyen d'autoriser plusieurs types d'exceptions?
Dinesh
1
Vous pouvez utiliser les exceptions intégrées de Python pour tester rapidement l'assertion; en utilisant @ réponse de Moe - dessus par exemple: self.assertRaises(TypeError, mymod.myfunc). Vous pouvez trouver une liste complète des exceptions intégrées
Raymond Wachaga
3
Même chose pour tester les constructeurs de classes:self.assertRaises(SomeCoolException, Constructor, arg1)
tschumann
477

Depuis Python 2.7, vous pouvez utiliser le gestionnaire de contexte pour obtenir l'objet réel de l'exception levée:

import unittest

def broken_function():
    raise Exception('This is broken')

class MyTestCase(unittest.TestCase):
    def test(self):
        with self.assertRaises(Exception) as context:
            broken_function()

        self.assertTrue('This is broken' in context.exception)

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

http://docs.python.org/dev/library/unittest.html#unittest.TestCase.assertRaises


En Python 3.5 , vous devez envelopper context.exceptiondans str, sinon vous aurez unTypeError

self.assertTrue('This is broken' in str(context.exception))
Art
la source
6
J'utilise Python 2.7.10, et ce qui précède ne fonctionne pas; context.exceptionne donne pas le message; c'est un type.
LateCoder
6
Toujours en Python 2.7 (au moins dans ma 2.7.6) en utilisant import unittest2, vous devez utiliser str(), c'est-à-dire self.assertTrue('This is broken' in str(context.exception)).
Sohail Si
4
Deux choses: 1. Vous pouvez utiliser assertIn au lieu d'assertTrue. Par exemple self.assertIn ('Ceci est cassé', context.exception) 2. Dans mon cas, en utilisant 2.7.10, context.exception semble être un tableau de caractères. L'utilisation de str ne fonctionne pas. J'ai fini par faire ceci: '' .join (context.exception) Donc, mis en place: self.assertIn ('Ceci est cassé', '' .join (context.exception))
blockcipher
1
Est-il normal que votre méthode obstrue la console de test avec le Traceback de l'exception? Comment puis-je empêcher que cela se produise?
MadPhysicist
1
plus tard, j'ai trouvé un autre moyen d'obtenir le message en tant que str de l'exception, c'est err = context.exception.message. Et puis peut également utiliser self.assertEqual (err, 'This is broken') pour faire le test.
zhihong
326

Le code dans ma réponse précédente peut être simplifié pour:

def test_afunction_throws_exception(self):
    self.assertRaises(ExpectedException, afunction)

Et si une fonction prend des arguments, passez-les simplement dans assertRaises comme ceci:

def test_afunction_throws_exception(self):
    self.assertRaises(ExpectedException, afunction, arg1, arg2)
Daryl Spitzer
la source
17
Le second a expliqué ce qu'il fallait faire lorsque l'argument est passé était vraiment utile.
Sabyasachi
J'utilise 2.7.15. Si afunctiondans self.assertRaises(ExpectedException, afunction, arg1, arg2)est l'initialiseur de classe, vous devez passer selfcomme premier argument, par exemple, self.assertRaises(ExpectedException, Class, self, arg1, arg2)
Minh Tran
128

Comment testez-vous qu'une fonction Python lève une exception?

Comment écrire un test qui échoue uniquement si une fonction ne lève pas d'exception attendue?

Réponse courte:

Utilisez la self.assertRaisesméthode comme gestionnaire de contexte:

    def test_1_cannot_add_int_and_str(self):
        with self.assertRaises(TypeError):
            1 + '1'

Manifestation

L'approche des meilleures pratiques est assez facile à démontrer dans un shell Python.

La unittestbibliothèque

En Python 2.7 ou 3:

import unittest

Dans Python 2.6, vous pouvez installer un backport de la unittestbibliothèque de 2.7 , appelé unittest2 , et juste alias cela comme unittest:

import unittest2 as unittest

Exemples de tests

Maintenant, collez dans votre shell Python le test suivant de sécurité de type de Python:

class MyTestCase(unittest.TestCase):
    def test_1_cannot_add_int_and_str(self):
        with self.assertRaises(TypeError):
            1 + '1'
    def test_2_cannot_add_int_and_str(self):
        import operator
        self.assertRaises(TypeError, operator.add, 1, '1')

Le test que l'on utilise assertRaisescomme gestionnaire de contexte, ce qui garantit que l'erreur est correctement détectée et nettoyée pendant son enregistrement.

Nous pourrions également l'écrire sans le gestionnaire de contexte, voir test deux. Le premier argument serait le type d'erreur que vous vous attendez à soulever, le deuxième argument, la fonction que vous testez, et les arguments et les arguments de mot-clé restants seront passés à cette fonction.

Je pense qu'il est beaucoup plus simple, lisible et maintenable de simplement utiliser le gestionnaire de contexte.

Exécution des tests

Pour exécuter les tests:

unittest.main(exit=False)

Dans Python 2.6, vous aurez probablement besoin des éléments suivants :

unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromTestCase(MyTestCase))

Et votre terminal devrait produire les informations suivantes:

..
----------------------------------------------------------------------
Ran 2 tests in 0.007s

OK
<unittest2.runner.TextTestResult run=2 errors=0 failures=0>

Et nous voyons cela comme nous l'attendons, en essayant d'ajouter un 1et un '1'résultat dans un TypeError.


Pour une sortie plus détaillée, essayez ceci:

unittest.TextTestRunner(verbosity=2).run(unittest.TestLoader().loadTestsFromTestCase(MyTestCase))
Aaron Hall
la source
56

Votre code doit suivre ce modèle (il s'agit d'un test de style de module le plus simple):

def test_afunction_throws_exception(self):
    try:
        afunction()
    except ExpectedException:
        pass
    except Exception:
       self.fail('unexpected exception raised')
    else:
       self.fail('ExpectedException not raised')

Sur Python <2.7, cette construction est utile pour vérifier des valeurs spécifiques dans l'exception attendue. La fonction unittest assertRaisesvérifie uniquement si une exception a été déclenchée.

Daryl Spitzer
la source
3
et la méthode self.fail ne prend qu'un seul argument
mdob
3
Cela semble trop compliqué pour tester si une fonction lève une exception. Étant donné que toute exception autre que cette exception entraînera une erreur de test et ne pas lancer une exception échouera au test, il semble que la seule différence est que si vous obtenez une exception différente avec assertRaisesvous, vous obtiendrez une ERREUR au lieu d'un ÉCHEC.
unflores
23

sur: http://www.lengrand.fr/2011/12/pythonunittest-assertraises-raises-error/

Tout d'abord, voici la fonction correspondante (toujours dum: p) dans le fichier dum_function.py:

def square_value(a):
   """
   Returns the square value of a.
   """
   try:
       out = a*a
   except TypeError:
       raise TypeError("Input should be a string:")

   return out

Voici le test à effectuer (seul ce test est inséré):

import dum_function as df # import function module
import unittest
class Test(unittest.TestCase):
   """
      The class inherits from unittest
      """
   def setUp(self):
       """
       This method is called before each test
       """
       self.false_int = "A"

   def tearDown(self):
       """
       This method is called after each test
       """
       pass
      #---
         ## TESTS
   def test_square_value(self):
       # assertRaises(excClass, callableObj) prototype
       self.assertRaises(TypeError, df.square_value(self.false_int))

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

Nous sommes maintenant prêts à tester notre fonction! Voici ce qui se passe lorsque vous essayez d'exécuter le test:

======================================================================
ERROR: test_square_value (__main__.Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_dum_function.py", line 22, in test_square_value
    self.assertRaises(TypeError, df.square_value(self.false_int))
  File "/home/jlengrand/Desktop/function.py", line 8, in square_value
    raise TypeError("Input should be a string:")
TypeError: Input should be a string:

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

FAILED (errors=1)

L'erreur TypeError est déclenchée et génère un échec de test. Le problème est que c'est exactement le comportement que nous voulions: le par.

Pour éviter cette erreur, exécutez simplement la fonction à l'aide de lambda dans l'appel de test:

self.assertRaises(TypeError, lambda: df.square_value(self.false_int))

La sortie finale:

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

OK

Parfait!

... et pour moi c'est parfait aussi !!

Merci beaucoup M. Julien Lengrand-Lambert


Cette affirmation de test renvoie en fait un faux positif . Cela se produit parce que le lambda à l'intérieur des 'assertRaises' est l'unité qui déclenche l'erreur de type et non la fonction testée.

macm
la source
10
Juste une note, vous n'avez pas besoin du lambda. La ligne self.assertRaises(TypeError, df.square_value(self.false_int))appelle la méthode et renvoie le résultat. Ce que vous voulez, c'est passer la méthode et tous les arguments et laisser les plus instants l'appeler:self.assertRaises(TypeError, df.square_value, self.false_int)
Roman Kutlak
Merci beaucoup. fonctionne parfaitement
Chandan Kumar
14

Vous pouvez créer le vôtre contextmanagerpour vérifier si l'exception a été déclenchée.

import contextlib

@contextlib.contextmanager
def raises(exception):
    try:
        yield 
    except exception as e:
        assert True
    else:
        assert False

Et puis vous pouvez utiliser raisescomme ceci:

with raises(Exception):
    print "Hola"  # Calls assert False

with raises(Exception):
    raise Exception  # Calls assert True

Si vous utilisez pytest, cette chose est déjà implémentée. Vous pouvez faire pytest.raises(Exception):

Exemple:

def test_div_zero():
    with pytest.raises(ZeroDivisionError):
        1/0

Et le résultat:

pigueiras@pigueiras$ py.test
================= test session starts =================
platform linux2 -- Python 2.6.6 -- py-1.4.20 -- pytest-2.5.2 -- /usr/bin/python
collected 1 items 

tests/test_div_zero.py:6: test_div_zero PASSED
Pigueiras
la source
1
Merci d'avoir posté une réponse qui ne nécessite pas le unittestmodule!
Sherwood Callaway
10

J'utilise doctest [1] presque partout parce que j'aime le fait que je documente et teste mes fonctions en même temps.

Jetez un œil à ce code:

def throw_up(something, gowrong=False):
    """
    >>> throw_up('Fish n Chips')
    Traceback (most recent call last):
    ...
    Exception: Fish n Chips

    >>> throw_up('Fish n Chips', gowrong=True)
    'I feel fine!'
    """
    if gowrong:
        return "I feel fine!"
    raise Exception(something)

if __name__ == '__main__':
    import doctest
    doctest.testmod()

Si vous placez cet exemple dans un module et l'exécutez à partir de la ligne de commande, les deux cas de test sont évalués et vérifiés.

[1] Documentation Python: 23.2 doctest - Test d'exemples interactifs Python

pi.
la source
4
J'adore doctest, mais je trouve qu'il complète plutôt qu'il ne remplace unittest.
TimothyAWiseman
2
Doctest est-il moins susceptible de jouer bien avec le refactoring automatisé? Je suppose qu'un outil de refactoring conçu pour python devrait être conscient des docstrings. Quelqu'un peut-il commenter son expérience?
kdbanman
6

Je viens de découvrir que la bibliothèque Mock fournit une méthode assertRaisesWithMessage () (dans sa sous-classe unittest.TestCase), qui vérifiera non seulement que l'exception attendue est déclenchée, mais également qu'elle est déclenchée avec le message attendu:

from testcase import TestCase

import mymod

class MyTestCase(TestCase):
    def test1(self):
        self.assertRaisesWithMessage(SomeCoolException,
                                     'expected message',
                                     mymod.myfunc)
Daryl Spitzer
la source
Malheureusement, il ne le fournit plus .. Mais la réponse ci-dessus de @Art ( stackoverflow.com/a/3166985/1504046 ) donne le même résultat
Rmatt
6

Il y a beaucoup de réponses ici. Le code montre comment créer une exception, comment utiliser cette exception dans nos méthodes et, enfin, comment vérifier dans un test unitaire, les exceptions correctes étant levées.

import unittest

class DeviceException(Exception):
    def __init__(self, msg, code):
        self.msg = msg
        self.code = code
    def __str__(self):
        return repr("Error {}: {}".format(self.code, self.msg))

class MyDevice(object):
    def __init__(self):
        self.name = 'DefaultName'

    def setParameter(self, param, value):
        if isinstance(value, str):
            setattr(self, param , value)
        else:
            raise DeviceException('Incorrect type of argument passed. Name expects a string', 100001)

    def getParameter(self, param):
        return getattr(self, param)

class TestMyDevice(unittest.TestCase):

    def setUp(self):
        self.dev1 = MyDevice()

    def tearDown(self):
        del self.dev1

    def test_name(self):
        """ Test for valid input for name parameter """

        self.dev1.setParameter('name', 'MyDevice')
        name = self.dev1.getParameter('name')
        self.assertEqual(name, 'MyDevice')

    def test_invalid_name(self):
        """ Test to check if error is raised if invalid type of input is provided """

        self.assertRaises(DeviceException, self.dev1.setParameter, 'name', 1234)

    def test_exception_message(self):
        """ Test to check if correct exception message and code is raised when incorrect value is passed """

        with self.assertRaises(DeviceException) as cm:
            self.dev1.setParameter('name', 1234)
        self.assertEqual(cm.exception.msg, 'Incorrect type of argument passed. Name expects a string', 'mismatch in expected error message')
        self.assertEqual(cm.exception.code, 100001, 'mismatch in expected error code')


if __name__ == '__main__':
    unittest.main()
Arindam Roychowdhury
la source
3

Vous pouvez utiliser assertRaises à partir du module unittest

import unittest

class TestClass():
  def raises_exception(self):
    raise Exception("test")

class MyTestCase(unittest.TestCase):
  def test_if_method_raises_correct_exception(self):
    test_class = TestClass()
    # note that you dont use () when passing the method to assertRaises
    self.assertRaises(Exception, test_class.raises_exception)
Bruno Carvalho
la source
0

Comme je n'ai vu aucune explication détaillée sur la façon de vérifier si nous avons obtenu une exception spécifique parmi une liste acceptée à l'aide du gestionnaire de contexte, ou d'autres détails d'exception, j'ajouterai le mien (vérifié sur python 3.8).

Si je veux juste vérifier que la fonction augmente par exemple TypeError, j'écrirais:

with self.assertRaises(TypeError):
    function_raising_some_exception(parameters)

Si je veux vérifier que la fonction soulève l'un TypeErrorou l' autre IndexError, j'écrirais: avec self.assertRaises ((TypeError, IndexError)): function_raising_some_exception (parameters)

Et si je veux encore plus de détails sur l'exception levée, je pourrais l'attraper dans un contexte comme celui-ci:

# Here I catch any exception    
with self.assertRaises(Exception) as e:
    function_raising_some_exception(parameters)

# Here I check actual eception type (but I could check anything else about that specific exception, like it's actual message or values stored in the exception)
self.assertTrue(type(e.exception) in [TypeError,MatrixIsSingular])
kriss
la source
-5

Bien que toutes les réponses soient parfaitement correctes, je cherchais un moyen de tester si une fonction levait une exception sans s'appuyer sur des frameworks de tests unitaires et sans avoir à écrire des classes de test.

J'ai fini par écrire ce qui suit:

def assert_error(e, x):
    try:
        e(x)
    except:
        return
    raise AssertionError()

def failing_function(x):
    raise ValueError()

def dummy_function(x):
    return x

if __name__=="__main__":
    assert_error(failing_function, 0)
    assert_error(dummy_function, 0)

Et cela échoue sur la bonne ligne:

Traceback (most recent call last):
  File "assert_error.py", line 16, in <module>
    assert_error(dummy_function, 0)
  File "assert_error.py", line 6, in assert_error
    raise AssertionError()
AssertionError
RUser4512
la source