Skip to main content

Python mocking on steroids

Project description

Python mocking on steroids.

Docs Docs
CI/CD CI - Test codecov
Package PyPI - Version PyPI - Downloads PyPI - Python Version
Meta linting - Black types - Mypy License GitHub Sponsors

What

Pymox is mocking on steroids. It's a powerful mock object framework for Python, providing many tools to help with your unit tests, so you can write them in an easy, quick and intuitive way.

Why

Why Pymox? Python already has batteries included. It has its own mock library, widely used in Python applications.

So, why pytest, given we have Python's unittest? Why arrow or pendulum given we have Python's datetime? Why X, given we have Python's Y?

You got it 😉!

Coming Soon

  • Async support
  • Decorator
  • Ignore args
  • String imports

How

Install

pip install pymox

Cool Stuff

New Elegant Way

# conftest.py
pytest_plugins = ("mox.testing.pytest_mox",)


# test.py
from mox import expect, stubout


class TestOs:
     def test_getcwd(self):
        with stubout(os, 'getcwd') as m_getcwd, expect:
            m_getcwd.to_be.called_with().and_return('/mox/path')

        assert os.getcwd() == '/mox/path'
        mox.verify(m_getcwd)

If you want to be less verbose:

class TestOs:
     def test_getcwd(self):
        with stubout(os, 'getcwd') as m_getcwd:
            m_getcwd().returns('/mox/path')

        assert os.getcwd() == '/mox/path'
        mox.verify(m_getcwd)

Anything you put inside the context manager is a call expectation, so to not expect any call you can:

class TestOs:
     def test_getcwd(self):
        with stubout(os, 'getcwd') as m_getcwd:
            pass

        # will raise a UnexpectedMethodCallError
        assert os.getcwd() == '/mox/path'
        mox.verify(m_getcwd)

Dict Access

class TestDict:
    def test_dict_access(self):
        config = {'env': 'dev', 'reload': True}

        # doing in another way using create, but you can do with stubout too
        mock_config = mox.create(config)

        mock_config['env'].returns('prod')
        mock_config['reload'].returns(False)

        mox.replay(mock_config)
        assert mock_config['env'] == 'prod'
        assert mock_config['reload'] is False
        mox.verify(mock_config)

Comparators

class Client:
    def get(self, url, params):
        return requests.get(url, params)


class Service:
    def get_contacts(self):
        url = 'https://my.reallylong.service/api/v1/contacts/'
        params = {'added': '7days', 'order_by': '-created'}
        return Client().get(url, params)


class TestSevice:
    def test_get_contacts_comparators_str_and_key_value(self):
        with stubout(Client, 'get') as m_get:
            url = mox.str_contains('/api/v1/contacts')
            params = mox.contains_key_value('added', '7days')
            m_get(url, params).returns({})

        service = Service()
        assert service.get_contacts() == {}
        mox.verify(m_get)

    def test_get_contacts_comparators_and_func_in_is_a(self):
        with stubout(Client, 'get') as m_get:
            url = mox.func(lambda v: str.startswith(v, 'https://my.reallylong.service/'))
            params = mox.and_(
                mox.is_a(dict),
                mox.in_('added'),
            )
            m_get(url, params).returns({})

        service = Service()
        assert service.get_contacts() == {}
        mox.verify(m_get)

    def test_get_contacts_comparators_ignore_arg_not(self):
        with stubout(Client, 'get') as m_get:
            url = mox.ignore_arg()
            params = mox.not_(mox.is_(None))
            m_get(url, params).returns({})

        service = Service()
        assert service.get_contacts() == {}
        mox.verify(m_get)

Other comparators: contains_attribute_value, in_, is_, is_almost, or_, same_elements_as, regex

And Raises

class TestOs:
     def test_getcwd(self):
        with stubout(os, 'getcwd') as m_getcwd:
            # .and_raise(..) also works
            os.getcwd().raises(Exception('error'))

        with pytest.raises(Exception, match='error'):
            os.getcwd()
        mox.verify(m_getcwd)

Multiple Times

class TestOs:
     def test_getcwd(self):
        with stubout(os, 'getcwd') as m_getcwd:
            m_getcwd().returns('/mox/path')
            # the second call will return a different value
            m_getcwd().returns('/mox/another/path')
            # the three subsequent calls will return "/"
            # if no argument is passed, multiple_times doesn't limit the number of calls
            m_getcwd().multiple_times(3).returns('/')

        assert os.getcwd() == '/mox/path'
        assert os.getcwd() == '/mox/another/path'
        mox.verify(m_getcwd)

Any order

If you stub out multiple, the order os calls is enforced, unless you use any_order

class TestOs:
    def test_getcwd(self):
        with stubout.many([os, 'getcwd'], [os, 'cpu_count']) as (m_getcwd, m_cpu_count):
            m_getcwd().returns('/mox/path')
            m_cpu_count().returns('10')

        # will raise a UnexpectedMethodCallError
        assert os.cpu_count() == '10'
        assert os.getcwd() == '/mox/path'
        mox.verify(m_getcwd, m_cpu_count)

    def test_getcwd_anyorder(self):
        with stubout.many([os, 'getcwd'], [os, 'cpu_count']) as (m_getcwd, m_cpu_count):
            m_getcwd().any_order().returns('/mox/path')
            m_cpu_count().any_order().returns('10')

        assert os.cpu_count() == '10'
        assert os.getcwd() == '/mox/path'
        mox.verify(m_getcwd, m_cpu_count)

Remember/Value

The Remember and Value are comparators, but they deserve their own section. They can be useful to retrieve some values from deeper levels of your codebase, and bring to the test for comparison. Let's see an example:

class Handler:
    def modify(self, d):
        # any integer key less than 5 is removed from the dict
        keys_to_remove = [key for key in d if isinstance(key, int) and key < 5]
        for key in keys_to_remove:
            del d[key]
        return d

    def send(self, d):
        return d


class Manager:
    def __init__(self, handlers=None):
        self.handlers = handlers or []

    def process(self, d):
        for handler in self.handlers:
            modified = handler.modify(d)
            handler.send(modified)


class TestManager(mox.MoxTestBase):
    def test_manager_process(self):
        mydict = {1: "apple", 4: "banana", 6: {2: 3, 4: {1: "orange", 7: 8}}, 8: 3}
        myvalue = mox.value()

        with mox.stubout(Handler, 'send') as mock_send:
            # so we use remember in the send call, and its value then the function is
            # called will go to `myvalue`
            mock_send(mox.remember(myvalue))

        Manager([Handler()]).process(mydict)
        mox.verify(mock_send)

        # now we can compare myvalue with what we think its value must be
        assert myvalue == {6: {2: 3, 4: {1: 'orange', 7: 8}}, 8: 3}

Classic Way

import mox
import os

class TestOs:
    def test_getcwd(self):
        m = mox.Mox()

        m.stubout(os, 'getcwd')
        # calls
        os.getcwd().returns('/mox/path')

        m.replay_all()
        assert os.getcwd() == '/mox/path'
        m.verify_all()


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

Jurassic Way

import mox
import os


class TestOs(mox.MoxTestBase):
    def test_getcwd(self):
        self.mox.StubOutWithMock(os, 'getcwd')
        # calls
        os.getcwd().AndReturn('/mox/path')

        self.mox.ReplayAll()
        self.assertEqual(os.getcwd(), '/mox/path')
        self.mox.VerifyAll()


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

Project Links

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pymox-1.4.1.tar.gz (30.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pymox-1.4.1-py2.py3-none-any.whl (34.1 kB view details)

Uploaded Python 2Python 3

File details

Details for the file pymox-1.4.1.tar.gz.

File metadata

  • Download URL: pymox-1.4.1.tar.gz
  • Upload date:
  • Size: 30.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.9.18

File hashes

Hashes for pymox-1.4.1.tar.gz
Algorithm Hash digest
SHA256 9d406f9b5456748492762e1b381ec9714c1c10f35d675ec1da99ab04dfbb78c1
MD5 4db13bbb991f836247da4394c840d410
BLAKE2b-256 09e3ffea23dd31b664f4edb401b54e8e496926b8f09e5c0f32f197b1d7154b6d

See more details on using hashes here.

File details

Details for the file pymox-1.4.1-py2.py3-none-any.whl.

File metadata

  • Download URL: pymox-1.4.1-py2.py3-none-any.whl
  • Upload date:
  • Size: 34.1 kB
  • Tags: Python 2, Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.9.18

File hashes

Hashes for pymox-1.4.1-py2.py3-none-any.whl
Algorithm Hash digest
SHA256 15fc530d746b11b15db0a75b8b0f6fc8b0ee81570772cdcff614e7d869b91795
MD5 91d4ddf66f06e77ad5ec3b6f7101f1cf
BLAKE2b-256 68b976980b227b97984cdbdf0a345a0da22a193ac3118cbaffcec18ab96d60f7

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page