pact-foundation / pact-python

Python version of Pact. Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project.
http://pact.io
MIT License
569 stars 138 forks source link

Format().timestamp matcher usage results in TypeError #228

Closed mefellows closed 3 years ago

mefellows commented 3 years ago

Repro code base: https://github.com/pactflow/example-consumer-python/blob/issue/matcher-not-serialisable/tests/consumer/test_products_consumer.py#L48

Steps to repro:

pip install -r requirements.txt
make test

Error:

======================================================================================= test session starts =======================================================================================
platform darwin -- Python 3.9.1, pytest-5.4.1, py-1.10.0, pluggy-0.13.1
rootdir: /Users/matthewfellows/development/public/example-consumer-python
collected 1 item

tests/consumer/test_products_consumer.py F                                                                                                                                                  [100%]

============================================================================================ FAILURES =============================================================================================
________________________________________________________________________________________ test_get_product _________________________________________________________________________________________

pact = <pact.pact.Pact object at 0x109ddc520>, consumer = <src.consumer.ProductConsumer object at 0x109ddc070>

    def test_get_product(pact, consumer):
        expected = {
            'id': "27",
            'name': 'Margharita',
            'type': 'Pizza',
            'date': Format().timestamp
        }

        (pact
         .given('a product with ID 10 exists')
         .upon_receiving('a request to get a product')
         .with_request('GET', '/product/10')
         .will_respond_with(200, body=Like(expected)))

>       with pact:

tests/consumer/test_products_consumer.py:57:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/usr/local/lib/python3.9/site-packages/pact/pact.py:382: in __enter__
    self.setup()
/usr/local/lib/python3.9/site-packages/pact/pact.py:207: in setup
    resp = requests.put(
/usr/local/lib/python3.9/site-packages/requests/api.py:134: in put
    return request('put', url, data=data, **kwargs)
/usr/local/lib/python3.9/site-packages/requests/api.py:61: in request
    return session.request(method=method, url=url, **kwargs)
/usr/local/lib/python3.9/site-packages/requests/sessions.py:516: in request
    prep = self.prepare_request(req)
/usr/local/lib/python3.9/site-packages/requests/sessions.py:449: in prepare_request
    p.prepare(
/usr/local/lib/python3.9/site-packages/requests/models.py:317: in prepare
    self.prepare_body(data, files, json)
/usr/local/lib/python3.9/site-packages/requests/models.py:467: in prepare_body
    body = complexjson.dumps(json)
/usr/local/Cellar/python@3.9/3.9.1_6/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/__init__.py:231: in dumps
    return _default_encoder.encode(obj)
/usr/local/Cellar/python@3.9/3.9.1_6/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/encoder.py:199: in encode
    chunks = self.iterencode(o, _one_shot=True)
/usr/local/Cellar/python@3.9/3.9.1_6/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/encoder.py:257: in iterencode
    return _iterencode(o, 0)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <json.encoder.JSONEncoder object at 0x1094c5430>, o = datetime.datetime(2000, 2, 1, 12, 30)

    def default(self, o):
        """Implement this method in a subclass such that it returns
        a serializable object for ``o``, or calls the base implementation
        (to raise a ``TypeError``).

        For example, to support arbitrary iterators, you could
        implement default like this::

            def default(self, o):
                try:
                    iterable = iter(o)
                except TypeError:
                    pass
                else:
                    return list(iterable)
                # Let the base class default method raise the TypeError
                return JSONEncoder.default(self, o)

        """
>       raise TypeError(f'Object of type {o.__class__.__name__} '
                        f'is not JSON serializable')
E       TypeError: Object of type datetime is not JSON serializable

/usr/local/Cellar/python@3.9/3.9.1_6/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/encoder.py:179: TypeError
-------------------------------------------------------------------------------------- Captured stdout setup --------------------------------------------------------------------------------------
start service
INFO  WEBrick 1.3.1
INFO  ruby 2.2.2 (2015-04-13) [x86_64-darwin13]
INFO  WEBrick::HTTPServer#start: pid=52244 port=1234
-------------------------------------------------------------------------------------- Captured stderr setup --------------------------------------------------------------------------------------
WARNING:urllib3.connectionpool:Retrying (Retry(total=8, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<urllib3.connection.HTTPConnection object at 0x10968af10>: Failed to establish a new connection: [Errno 61] Connection refused')': /
WARNING:urllib3.connectionpool:Retrying (Retry(total=7, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<urllib3.connection.HTTPConnection object at 0x109e15400>: Failed to establish a new connection: [Errno 61] Connection refused')': /
WARNING:urllib3.connectionpool:Retrying (Retry(total=6, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<urllib3.connection.HTTPConnection object at 0x109e155b0>: Failed to establish a new connection: [Errno 61] Connection refused')': /
WARNING:urllib3.connectionpool:Retrying (Retry(total=5, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<urllib3.connection.HTTPConnection object at 0x109e15790>: Failed to establish a new connection: [Errno 61] Connection refused')': /
--------------------------------------------------------------------------------------- Captured log setup ----------------------------------------------------------------------------------------
WARNING  urllib3.connectionpool:connectionpool.py:751 Retrying (Retry(total=8, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<urllib3.connection.HTTPConnection object at 0x10968af10>: Failed to establish a new connection: [Errno 61] Connection refused')': /
WARNING  urllib3.connectionpool:connectionpool.py:751 Retrying (Retry(total=7, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<urllib3.connection.HTTPConnection object at 0x109e15400>: Failed to establish a new connection: [Errno 61] Connection refused')': /
WARNING  urllib3.connectionpool:connectionpool.py:751 Retrying (Retry(total=6, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<urllib3.connection.HTTPConnection object at 0x109e155b0>: Failed to establish a new connection: [Errno 61] Connection refused')': /
WARNING  urllib3.connectionpool:connectionpool.py:751 Retrying (Retry(total=5, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<urllib3.connection.HTTPConnection object at 0x109e15790>: Failed to establish a new connection: [Errno 61] Connection refused')': /
------------------------------------------------------------------------------------ Captured stdout teardown -------------------------------------------------------------------------------------
stop service
INFO: Writing pact before shutting down
INFO  going to shutdown ...
INFO  WEBrick::HTTPServer#start done.
===================================================================================== short test summary info =====================================================================================
FAILED tests/consumer/test_products_consumer.py::test_get_product - TypeError: Object of type datetime is not JSON serializable
======================================================================================== 1 failed in 2.16s ========================================================================================
➜  example-consumer-python git:(issue/matcher-not-serialisable)
DawoudSheraz commented 3 years ago

I am facing the same issue on my project. I have tried adding headers but it does not seem to work. The workaround I am using is datetime.now().isoformat() but it doesn't allow the datetime format checks.

mefellows commented 3 years ago

Yep.

That should work, all these matchers are @DawoudSheraz is sugar over the Term matcher (accepts a regex and an example value).

So in your case, something like this should work:

   // regex from matchers.py
    regex = r'^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3(' \
            r'[12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-' \
            r'9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2' \
            r'[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d' \
            r'([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$'

    expected = {
        'id': "27",
        'name': 'Margharita',
        'type': 'Pizza',
        'date': Term(
            regex, datetime.datetime(
                2000, 2, 1, 12, 30, 0, 0
            ).isoformat()
        )
    }

You can see the problem here: https://github.com/pact-foundation/pact-python/blob/d348a9c9b1126fa6beb417293db0964f2284686c/pact/matchers.py#L332

Basically, the return values of the generated examples are functions, not values:

>>> import datetime
>>> datetime.datetime( 2000, 2, 1, 12, 30, 0, 0 )
datetime.datetime(2000, 2, 1, 12, 30)
>>> datetime.datetime( 2000, 2, 1, 12, 30, 0, 0 ).isoformat()
'2000-02-01T12:30:00'
DawoudSheraz commented 3 years ago

Right, makes sense.

DawoudSheraz commented 3 years ago

although this should have been caught within the tests.

DawoudSheraz commented 3 years ago

Created https://github.com/pact-foundation/pact-python/pull/230

elliottmurray commented 3 years ago

230 fixes this.