85

I have a following function in Python and I want to test with unittest that if the function gets 0 as argument, it throws a warning. I already tried assertRaises, but since I don't raise the warning, that doesn't work.

def isZero(i):
    if i != 0:
        print "OK"
    else:
        warning = Warning("the input is 0!") 
        print warning
    return i
jpyams
  • 4,030
  • 9
  • 41
  • 66
Tomas Novotny
  • 7,547
  • 9
  • 26
  • 23
  • 1
    Regarding working around *...if a warning has already been raised because of a once/default rule, then no matter what filters are set the warning will not be seen again unless the warnings registry related to the warning has been cleared.* ([docs](https://docs.python.org/3/library/warnings.html#testing-warnings)) see [this article](https://blog.ionelmc.ro/2013/06/26/testing-python-warnings/) about module level `__warningregistry__` (the registry docs mentions). – saaj Sep 16 '16 at 15:12

7 Answers7

91

Starting with Python 3.2, you can simply use assertWarns() method.

with self.assertWarns(Warning):
    do_something()
Melebius
  • 6,183
  • 4
  • 39
  • 52
  • 1
    This works regardless of which warning filters are active. Therefore, I would use this answer rather than the accepted one. – NOhs Jan 21 '19 at 00:27
  • 1
    Is there a similar way of asserting that a warning hasn't been triggered or would that require falling back on mocking or `warnings.catch_warnings`? – Lokal_Profil Jun 09 '19 at 20:12
  • 2
    ... or running `self.assertWarns()` but sticking a `@unittest.expectedFailure` on the test. – Lokal_Profil Jun 09 '19 at 20:33
  • where is the 'self' in self.assertWarns coming from? – lightbox142 Jan 25 '21 at 19:45
  • @lightbox142 It is your testing class, a child of `unittest.TestCase`. If you are not familiar with it, you should read the page https://docs.python.org/3/library/unittest.html from its beginning. – Melebius Jan 29 '21 at 11:29
63

You can use the catch_warnings context manager. Essentially this allows you to mock the warnings handler, so that you can verify details of the warning. See the official docs for a fuller explanation and sample test code.

import warnings

def fxn():
    warnings.warn("deprecated", DeprecationWarning)

with warnings.catch_warnings(record=True) as w:
    # Cause all warnings to always be triggered.
    warnings.simplefilter("always")
    # Trigger a warning.
    fxn()
    # Verify some things
    assert len(w) == 1
    assert issubclass(w[-1].category, DeprecationWarning)
    assert "deprecated" in str(w[-1].message)
Mark Hildreth
  • 42,023
  • 11
  • 120
  • 109
ire_and_curses
  • 68,372
  • 23
  • 116
  • 141
  • 5
    Take note that this is NOT thread safe, because it modifies a `global state` - if you use this in a test suite and other warnings are emitted, these will also show up in `catch_warnings`, which may cause false negatives. – deepbrook Dec 17 '17 at 09:58
  • Upvoted because example shows assertions for interesting properties - number, type, and message contents. – Chris Keefe Jul 13 '21 at 22:32
18

You can write your own assertWarns function to incapsulate catch_warnings context. I've just implemented it the following way, with a mixin:

class WarningTestMixin(object):
    'A test which checks if the specified warning was raised'

    def assertWarns(self, warning, callable, *args, **kwds):
        with warnings.catch_warnings(record=True) as warning_list:
            warnings.simplefilter('always')

            result = callable(*args, **kwds)

            self.assertTrue(any(item.category == warning for item in warning_list))

A usage example:

class SomeTest(WarningTestMixin, TestCase):
    'Your testcase'

    def test_something(self):
        self.assertWarns(
            UserWarning,
            your_function_which_issues_a_warning,
            5, 10, 'john', # args
            foo='bar'      # kwargs
        )

The test will pass if at least one of the warnings issued by your_function is of type UserWarning.

Stephan
  • 41,764
  • 65
  • 238
  • 329
Anatoly Scherbakov
  • 1,672
  • 1
  • 13
  • 20
7

@ire_and_curses' answer is quite useful and, I think, canonical. Here is another way to do the same thing. This one requires Michael Foord's excellent Mock library.

import unittest, warnings
from mock import patch_object

def isZero( i):
   if i != 0:
     print "OK"
   else:
     warnings.warn( "the input is 0!")
   return i

class Foo(unittest.TestCase):
    @patch_object(warnings, 'warn')
    def test_is_zero_raises_warning(self, mock_warn):
        isZero(0)
        self.assertTrue(mock_warn.called)

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

The nifty patch_object lets you mock out the warn method.

Community
  • 1
  • 1
Manoj Govindan
  • 72,339
  • 21
  • 134
  • 141
  • +1 - This solution is better than the accepted answer, as it does not use the global state of the `warnings` library - which may cause false negatives. – deepbrook Dec 17 '17 at 10:00
  • 1
    This is a reasonable answer, but the assertion in more recent versions of the mock library would be better this way, if you are not yet on Python 3 and can't use `assertWarns`: `mock_warn.assert_called_once()`. This would catch cases where some other random module unexpectedly raised a warning. – Nik Haldimann Dec 03 '18 at 20:59
  • I'm just getting an error and can't find a proper reference: ` from mock import patch_object ImportError: cannot import name 'patch_object' from 'mock' (/usr/local/lib/python3.8/site-packages/mock/__init__.py) ` So I decided to use `with self.assertWarns(Warning):` – Qohelet Apr 11 '22 at 11:41
0

One problem with the warnings.catch_warnings approach is that warnings produced in different tests can interact in strange ways through global state kept in __warningregistry__ attributes.

To address this, we should clear the __warningregistry__ attribute of every module before every test that checks warnings.

class MyTest(unittest.TestCase):

  def setUp(self):
    # The __warningregistry__'s need to be in a pristine state for tests
    # to work properly.
    for v in sys.modules.values():
      if getattr(v, '__warningregistry__', None):
        v.__warningregistry__ = {}

  def test_something(self):
    with warnings.catch_warnings(record=True) as w:
      warnings.simplefilter("always", MySpecialWarning)
      ...
      self.assertEqual(len(w), 1)
      self.assertIsInstance(w[0].message, MySpecialWarning)

This is how Python 3's assertWarns() method is implemented.

ostrokach
  • 17,993
  • 11
  • 78
  • 90
0

Building off the answer from @ire_and_curses,

class AssertWarns(warnings.catch_warnings):
    """A Python 2 compatible version of `unittest.TestCase.assertWarns`."""
    def __init__(self, test_case, warning_type):
        self.test_case = test_case
        self.warning_type = warning_type
        self.log = None
        super(AssertWarns, self).__init__(record=True, module=None)

    def __enter__(self):
        self.log = super(AssertWarns, self).__enter__()
        return self.log

    def __exit__(self, *exc_info):
        super(AssertWarns, self).__exit__(*exc_info)
        self.test_case.assertEqual(type(self.log[0]), self.warning_type)

This can be called similarly to unittest.TestCase.assertWarns:

with AssertWarns(self, warnings.WarningMessage):
    warnings.warn('test warning!') 

where self is a unittest.TestCase.

mathandy
  • 1,892
  • 25
  • 32
0

Per Melebius' answer, you can use self.assertWarns().

Additionally, if you want to check the warning message as well, you can use self.assertWarnsRegex() for that greater specificity:

import warnings
from unittest import TestCase


class MyCustomWarning(Warning):
    ...


def is_zero(i: int) -> int:
    if i != 0:
        print("OK")
    else:
        warnings.warn("the input is 0!", MyCustomWarning)
    return i



class TestIsZero(TestCase):

    def test_when_then_input_is_zero(self):
        regex = "the input is 0"
        with self.assertWarnsRegex(MyCustomWarning, expected_regex=regex):
            _ = is_zero(0)

This test will fail if the regex is not found in the warning message.

Alexus Wong
  • 347
  • 4
  • 9