joythief package

Purpose

A critical part of testing is providing useful diagnostics on failure. One of the great things about test-driven development (TDD) is that you always get a preview of the feedback your test will give when it fails. This also allows you to consider whether this is going to be useful in the future, if some other change to the codebase causes a regression in the tested functionality.

See also

image/svg+xml

From Growing Object-Oriented Software by Nat Pryce and Steve Freeman.

© 2010 Nat Pryce, licensed under CC BY-SA 4.0.

Example

Consider the following test:

import unittest


def my_func() -> list[str]:
    return []


class TestMyFunction(unittest.TestCase):

    def test_my_function_returns_three_items(self):
        self.assertTrue(len(my_func()) == 3)


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

This is absolutely fine when my_func works correctly - it describes the intended behaviour, and ensures that it keeps happening. But what if something breaks? OK, the test fails, but what does it tell us about why it failed?

    self.assertTrue(len(my_func()) == 3)
AssertionError: False is not true

We can do better than this; how many items did it return, then?

class TestMyFunction(unittest.TestCase):

    def test_my_function_returns_three_items(self):
        self.assertEqual(len(my_func()), 3)
    self.assertEqual(len(my_func()), 3)
AssertionError: 0 != 3

This is a bit better, and in the trivial empty list case that’s all the information we need, but what if it said e.g. 1 != 3; what one item is in the list?

This is one of the great things about pytest; even from a plain assert it tries to tell you as much as possible about what went wrong:

>       assert len(my_func()) == 3
E       assert 0 == 3
E        +  where 0 = len([])
E        +    where [] = my_func()

But even in vanilla unittest we can use JoyThief’s matchers to write a more diagnostically useful test. For example, we can use InstanceOf to say “it must be a list containing three strings”:

from joythief.objects import InstanceOf

# ...

class TestMyFunction(unittest.TestCase):

    def test_my_function_returns_three_items(self):
        self.assertEqual(
            my_func(),
            [InstanceOf(str), InstanceOf(str), InstanceOf(str)],
        )

Now we’re passing the whole value to assertEqual(), so the feedback actually shows the list and any content it does have:

    self.assertEqual(
AssertionError: Lists differ: [] != [InstanceOf(<class 'str'>), InstanceOf(<cl[34 chars]r'>)]

Second list contains 3 additional elements.
First extra element 0:
InstanceOf(<class 'str'>)

- []
+ [InstanceOf(<class 'str'>),
+  InstanceOf(<class 'str'>),
+  InstanceOf(<class 'str'>)]

Visualisation

The trick up JoyThief’s sleeve is that, if a matcher has been compared equal to a single value and never to any other, it represents itself as that value. Extending the above example to show how this can be useful: if the list contains three items, but they’re not all strings:

def my_func() -> list[str]:
    return ["foo", "bar", 123]

then the values that were acceptable are shown as such in the output:

    self.assertEqual(
AssertionError: Lists differ: ['foo', 'bar', 123] != ['foo', 'bar', InstanceOf(<class 'str'>)]

First differing element 2:
123
InstanceOf(<class 'str'>)

- ['foo', 'bar', 123]
+ ['foo', 'bar', InstanceOf(<class 'str'>)]

This is helpful anywhere else a visual diff is shown, for example in pytest’s output:

>       assert my_func() == [InstanceOf(str), InstanceOf(str), InstanceOf(str)]
E       AssertionError: assert ['foo', 'bar', 123] == ['foo', 'bar'...class 'str'>)]
E
E         At index 2 diff: 123 != InstanceOf(<class 'str'>)
E         Use -v to get more diff

or in tools like the PyCharm test runner.

To aid in this process, compound matchers (in e.g. joythief.compound and joythief.data_structures) are not implemented lazily - comparison continues even once a mismatch is found, so that any inner matchers that _are_ equal can have their representations resolved.

class joythief.Matcher(*args, **kwargs)[source]

Bases: Generic[T], ABC

The core generic matcher type.

Added in version 0.5.0: previously only exposed from joythief.core

Can be extended, to create your own custom matchers, or used in type definitions.

from joythief import Matcher
from joythief.strings import StringContaining


def contains_only(valid_chars: str) -> Matcher[str]:
    return StringContaining(rf"^[{valid_chars}]+$")
Parameters:
  • args (tp.Any)

  • kwargs (tp.Any)

property not_implemented: bool

The value NotImplemented, force-cast to bool.

NotImplemented is special-cased in e.g. __eq__, but cannot be returned from compare() without a cast.

Submodules