Kraigolas
Kraigolas

Reputation: 5590

Is there a function that allows for verbose assertions?

Frequently when I am writing code I add assertions for my own sanity, specifically for when I am first writing the code to ensure I do not implement it with bugs (I understand assertions are ignored in production builds of code). Whenever I write an assertion, I find myself verbosely writing something along the lines of:

assert a == b, f"{a} != {b}"

So that if my assertion fails, I have some way of interpreting why. Unfortunately, this often clutters my code, as if the variable names are long (or I am in nested structurees), adding the message at the end often makes the line too long to be consistent with PEP 8. So I spend the time breaking it up over several lines, which also makes a basic assert a multi-lined statement.

unittest provides helpful assert methods like assertEqual which shorten this work, make it less prone to me leaving out the error message, and allows me to continue on with my work rather than worrying about assertions. The only two options I can come up with are:

  1. Write my own function to be verbose. This would require re-writing many of the assert functions; or
  2. Instantiate an instance of unittest.TestCase() each time I want to make an assertion.

The problem with number 2 is it is unbelievably slow:

>>> min(timeit.repeat('unittest.TestCase().assertEqual(a, b)', setup='import unittest; a = 1; b = 1'))
1.7452130000000352
>>> min(timeit.repeat('assert a == b, f"{a} != {b}"', setup='a = 1; b = 1'))
0.01641979999999421

Which is a hint that this would be bad practice ie. unittest should be used for unit testing, not generic assertions.

I'll admit this question is a bit nit-picky, but StackOverflow often has the ability the enlighten me on libraries/ideas that make programming faster, safer, and better, so I'd like to know: is there a better (built-in?) method for verbose assertions, or should I simply continue writing them verbosely as I have before?

Upvotes: 0

Views: 817

Answers (1)

Lenormju
Lenormju

Reputation: 4368

You could adapt some code from unittest to create some functions which do not require to instantiate a full-blown TestCase, here is a small example :

import difflib
import pprint
from unittest.util import safe_repr, _common_shorten_repr


class Asserter:  # this class consists mostly of code from `unittest.TestCase`

    def assertEqual(self, first, second, msg=None):
        assertion_func = self._getAssertEqualityFunc(first, second)
        assertion_func(first, second, msg=msg)

    def _getAssertEqualityFunc(self, first, second):
        if type(first) is type(second):
            asserter = {
                dict: self.assertDictEqual,
                # ...
            }.get(type(first))
            if asserter is not None:
                return asserter
        return self._baseAssertEqual

    def _baseAssertEqual(self, first, second, msg=None):
        """The default assertEqual implementation, not type specific."""
        if not first == second:
            standardMsg = '%s != %s' % _common_shorten_repr(first, second)
            raise AssertionError('%s : %s' % (standardMsg, msg))

    def assertDictEqual(self, d1, d2, msg=None):
        self.assertIsInstance(d1, dict, 'First argument is not a dictionary')
        self.assertIsInstance(d2, dict, 'Second argument is not a dictionary')
        if d1 != d2:
            standardMsg = '%s != %s' % _common_shorten_repr(d1, d2)
            diff = ('\n' + '\n'.join(difflib.ndiff(
                           pprint.pformat(d1).splitlines(),
                           pprint.pformat(d2).splitlines())))
            standardMsg = standardMsg + diff
            raise AssertionError('%s : %s' % (standardMsg, msg))

    def assertIsInstance(self, obj, cls, msg=None):
        if not isinstance(obj, cls):
            standardMsg = '%s is not an instance of %r' % (safe_repr(obj), cls)
            raise AssertionError('%s : %s' % (standardMsg, msg))


__ASSERTER = Asserter()  # only one required
assertEqual = __ASSERTER.assertEqual  # get a reference to the method


def main():
    assertEqual({'a': 1, 'b': 2},
                {'a': 1, 'b': 456789})


if __name__ == "__main__":
    main()

which produces :

Traceback (most recent call last):
  File "/home/stack_overflow/so70675609.py", line 56, in <module>
    main()
  File "/home/stack_overflow/so70675609.py", line 51, in main
    assertEqual({'a': 1, 'b': 2},
  File "/home/stack_overflow/so70675609.py", line 11, in assertEqual
    assertion_func(first, second, msg=msg)
  File "/home/stack_overflow/so70675609.py", line 38, in assertDictEqual
    raise AssertionError('%s : %s' % (standardMsg, msg))
AssertionError: {'a': 1, 'b': 2} != {'a': 1, 'b': 456789}
- {'a': 1, 'b': 2}
?               ^

+ {'a': 1, 'b': 456789}
?               ^^^^^^
 : None

Upvotes: 2

Related Questions