11

Given code like the following:

import flask
import time

app = flask.Flask(__name__)

def authorize():
    print('starting authorize io')
    time.sleep(1)
    print('done authorize io')

class BlockingIo():
    def __init__(self, n):
        self.n = n
    def do(self):
        print('starting blocking io')
        time.sleep(1)
        print('ending blocking io')

@app.route('/', methods=['GET'])
@app.route('/<int:n>/', methods=['GET'])
def foo(n=1):
    authorize()
    b = BlockingIo(n)
    b.do()
    return str(n), 200

#app.run(port=5000)

I want to be able to write several tests for GET /n/, each of which mocks authorize and BlockingIO(n):

app.testing = True
testapp = app.test_client()

import unittest
from unittest import mock

mock.patch('__main__.authorize')

class TestBlockingIo(unittest.TestCase):
    @mock.patch('__main__.authorize')
    @mock.patch('__main__.BlockingIo.do')
    def test_1(self, m, m2):
        r = testapp.get('/1/')
        self.assertEquals(r.data, b'1')
    @mock.patch('__main__.authorize')
    @mock.patch('__main__.BlockingIo.do')
    def test_2(self, m, m2):
        r = testapp.get('/2/')
        self.assertEquals(r.data, b'2')

unittest.main()

However, I do not want to write out @mock.patch decorator over and over again.

I know we can use a class decorator, and I can subclass for more reusability:

@mock.patch('__main__.authorize')
@mock.patch('__main__.BlockingIo.do')
class TestBlockingIo(unittest.TestCase):
    def test_1(self, m, m2):
        r = testapp.get('/1/')
        self.assertEquals(r.data, b'1')
    def test_2(self, m, m2):
        r = testapp.get('/2/')
        self.assertEquals(r.data, b'2')

But, this forces all the test functions in the class to take one extra argument for each mock. What if I have tests in this class that do not need mocks for BlockingIo or authorize?

I suppose what I would like is a way to do the following:

m = mock.something('__main__.authorize')
m2 = mock.something('__main__.BlockingIo.do')    

class TestBlockingIo(unittest.TestCase):
    def test_1(self):
        r = testapp.get('/1/')
        self.assertEquals(r.data, b'1')
    def test_2(self):
        r = testapp.get('/2/')
        self.assertEquals(r.data, b'2')

How can I reuse my @mock.patch('__main__.authorize') and @mock.patch('__main__.BlockingIo.do') to avoid repeating myself through the tests?

Matthew Moisen
  • 16,701
  • 27
  • 128
  • 231

2 Answers2

7

You could use patches and reuse them in a setUp block.

Patches are nice as you can "unpatch" things when you are done with the test, meaning you won't leave things mocked forever, as some other tests may require to run on the real code.

On the link above, you will see the following piece of code:

>>> class MyTest(TestCase):
...     def setUp(self):
...         patcher = patch('package.module.Class')
...         self.MockClass = patcher.start()
...         self.addCleanup(patcher.stop)
...
...     def test_something(self):
...         assert package.module.Class is self.MockClass
...

It works fine, but I don't really like to call patch(), start() and addCleanup() for every patch.

You can easily factor this in a base class that you could reuse in your test classes:

class PatchMixin:

    def patch(self, target, **kwargs):
        p = mock.patch(target, **kwargs)
        p.start()
        self.addCleanup(p.stop)

class TestBlockingIo(unittest.TestCase, PatchMixin):

    def setUp(self):
        self.patch('__main__.authorize')
        self.patch('__main__.BlockingIo.do')

    def test_1(self):
        r = testapp.get('/1/')
        self.assertEquals(r.data, b'1')

    def test_2(self):
        r = testapp.get('/2/')
        self.assertEquals(r.data, b'2')
Guillaume
  • 1,814
  • 1
  • 13
  • 29
  • 1
    In case you want to configure the mock in tests, store and return the `ps.start()` from `patch`. – Saurabh Jun 08 '20 at 07:47
2

For avoiding an extra argument for patched test methods, you can use the new argument of patch, e.g.:

@mock.patch('__main__.authorize', new=lambda: None)

The documentation is a bit hidden:

If patch() is used as a decorator and new is omitted, the created mock is passed in as an extra argument to the decorated function.

It's also possible to re-use patch objects (which is sometimes useful when repeatedly patching hard-to-remember modules in a lot of spots):

mocked_authorize = mock.patch('__main__.authorize', new=lambda: None)

@mocked_authorize
def test_authorize():
    pass

@mocked_authorize
class TestBlockingIo(unittest.TestCase):

    def test_1(self):
        ...
Pankrat
  • 5,206
  • 4
  • 31
  • 37