Search code examples
pythonpattern-matching

Is there an equivalent to Jest's `expect` pattern matching in Python?


One of the big features of Jest is its advanced expect matchers, which allow for very finely grained matching of JS objects.

// We expect an object (dict) which strictly equals the following
expect(myObject).toStrictEqual({
  foo: "Hello",
  // Any valid number will match for the bar property
  bar: expect.any(Number),
});

expect(myObject).toStrictEqual({
  // Any string that matches the given regex will work
  foo: expect.stringMatching("Some regex"),
  bar: 1,
});

// We expect myObject to at least have the properties of the following object
// It may have more, but we don't care about them
expect(myObject).toMatchObject({
  foo: "Hello",
});

While the above cases can all be checked trivially in Python from first principles due to their simplicity, as soon as the objects/dicts I am trying to match become remotely complex, the code for testing them becomes much more difficult to write, read and understand in Python compared to the equivalent in Jest.

In the following example in Python, it is nearly impossible to quickly understand the data structure we are looking for. In order to understand it, I need to spend much more time parsing the code to figure out what I am testing.

assert isinstance(my_object['foo'], str)
assert abs(my_object - 1) < 0.1
assert isinstance(my_object['baz'], list)

def object_matches(o):
    return (
        isinstance(o['some_value'], bool)
        and o['other_value'] == 'Hello'
        and isinstance(o['yet_another_value'], dict)
        and isinstance(o['yet_another_value']['contents'], int)
        and isinstance(o['yet_another_value']['more_contents'], str)
        and 'Goodbye' not in o['yet_another_value']['more_contents']
        and len(o['yet_another_value']) == 2
    )

assert len(filter(object_matches), my_object['baz'])) >= 1

Contrastingly in Jest, I can use expect expressions to cleanly check for all these properties in a manner which also allows me to concisely express the full data structure I am checking. It is clear what data should go where, and I can read it much more quickly, even though I have much less experience with JS compared to Python.

expect(myObject).toMatchObject({
  foo: expect.any(String),
  bar: expect.closeTo(1),
  baz: expect.arrayContaining({
    some_value: expect.any(Boolean),
    other_value: "Hello",
    yet_another_value: {
      contents: expect.any(Number),
      more_contents: expect.not.stringContaining("Goodbye"),
    },
  }),
});

Is there a way I can get similar functionality in Python? I am happy to install a pip package if required, but I would prefer something built into Pytest or the Python standard library.


Solution

  • Since I haven't been able to find anything I went and implemented it myself.

    It can be installed using pip install jestspectation and the source code is available on GitHub.

    Here is an implementation of the pattern matcher listed above, written using the library.

    from jestspectation import (
        Any,
        FloatApprox,
        ListContaining,
        Not,
        StringMatchingRegex,
        DictContainingItems,
    )
    
    def test_advanced():
        my_dict = {
            "foo": "Hello",
            "bar": 1.002,
            "baz": [
                {
                    "this": 1,
                    "shouldn't": 2,
                    "match": 3,
                },
                {
                    "some_value": True,
                    "other_value": "Hello",
                    "yet_another_value": {
                        "contents": 42,
                        "more_contents": "Hello",
                    }
                }
            ]
        }
        expected = DictContainingItems({
            "foo": Any(str),
            "bar": FloatApprox(1, percent=1),
            "baz": ListContaining([{
                "some_value": Any(bool),
                "other_value": "Hello",
                "yet_another_value": {
                    "contents": Any(int),
                    "more_contents": Not(StringMatchingRegex("Goodbye*"))
                }
            }])
        })
        assert expected == my_dict
    

    I would still appreciate knowing if someone else has already implemented this elsewhere in a more popular package, since I don't want to reinvent the wheel unless it's absolutely necessary.