Django Unit Testing - Part 2 Mocking

2018/06/04 posted in  Django comments

Mocking(模仿)對Testing十分重要,它可以取代本來system會做的某些功能,通常用於:

  1. Network Request(太費時了)
  2. 一些複雜的Dependencies(避免需要準備太多東西/不想被其他module影響到)
  3. 修改當前時間(尤其是依賴datetime.now()的function)

各種mock

unittest.mock 官方文檔

from django.test import mock
# 本來是 from unittest import mock

Mock

建立一個Mock object來test:

m = Mock()
m.method()
m.method.assert_called()  # OK

m.hahaha()  # 甚麼名字也會自動變成valid method
m.hahaha.assert_called_once()  # OK

最主要有幾種用法:

  1. spec
  2. side_effect
  3. return_value

spec

設定這個mock是甚麼:

mock = Mock(spec=3)
isinstance(mock, int)  # True
def f(a, b, c): pass
mock = Mock(spec=f)
mock(1, 2, c=3)
mock.assert_called_with(1, 2, 3)  # OK
mock.assert_called_with(a=1, b=2, c=3)  # OK

side_effect

每次mock被call的時候都會run一次。

可用作三種東西:

Exception
mock = Mock(side_effect=KeyError('foo'))
mock()  # KeyError
Function
side_effect = lambda value: value + 1
mock = Mock(side_effect=side_effect)
mock(1)  # 2
mock(-5)  # -4
Iterable
mock = Mock(side_effect=[1,2,3])
mock()  # 1
mock()  # 2
mock()  # 3
mock()  # StopIteration Error

return_value

最簡單的功能了,就是call的時候return一些固定的東西。

(有side_effect的話,side_effect優先)

mock = Mock(return_value=3)
mock()  # 3
mock()  # 3

MagicMock

MagicMockMock的subclass,通常會直接這個而不用Mock,因為它支援了很多python的原生功能,例如__str__,詳細可見這裡

StackOverflow - Mock vs MagicMock

其他Mock的詳細用法可參見官方文檔

patch

替換module裡的任何東西,可用作decorator或with-block。

https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch

Patch一個class進去function argument

class SomeClass:
    pass

@patch('__main__.SomeClass')
def function(normal_argument, mock_class):
    print(normal_argument)
    print(mock_class is SomeClass)

function(123)
# 123
# True

Patch一個class成為Mock object:

class Class:
    def method(self):
        pass

with patch('__main__.Class') as Mock:
    instance = MockClass.return_value
    instance.method.return_value = 'foo'
    assert Class() is instance  # OK
    assert Class().method() == 'foo'

替換成new_callable

thing = object()
with patch('__main__.thing', new_callable=NonCallableMock) as mock_thing:
    assert thing is mock_thing  # OK
    thing()  # Error

return_value

from datetime import datetime
with patch('__main__.datetime.now', return_value=datetime.now().replace(hour=0, minute=0)):
        datetime.now()  # datetime(2018, xx, xx, 0, 0, x, xxxxxx)

side_effect

@mock.patch('requests.get', side_effect=mocked_requests_get)

實用例子

Mock network request

很好的例子:https://stackoverflow.com/a/28507806/6025730

Mock datetime

from django.test import TestCase, mock
from django.utils import timezone

from some_app.models import Restaurant

class SomeTestCase(TestCase):
    def setUp(self):
        Restaurant.objects.create(
            name='Test1',
            auto_open_at='18:00',  # should auto open at 18:00
        )

    def test_auto_open_restaurants(self):
        restaurant = Restaurant.active_objects.get(name='Test1')
        self.assertEqual(restaurant.is_open, False)  # OK: Not opened yet
        with mock.patch(
                'some_app.models.timezone.now',  # which will be used in auto_open_restaurants()
                return_value=timezone.now().replace(hour=18, minute=0)  # Should consider timezone if django.settings.USE_TZ=true (e.g. 18:00 UTC+8 => 10:00 UTC)
        ):
            auto_open_restaurants()  # Should auto open here no matter when I run the test
            restaurant = Restaurant.active_objects.get(name='Test1')
            self.assertEqual(restaurant.is_open, True)  # OK