Summary: in this tutorial, you’ll learn how to use Python stubs to isolate parts of your program from each other for unit testing.
Introduction to the Python stubs
Stubs are test doubles that return hard-coded values. The primary purpose of stubs is to prepare a specific state of the system under test.
Stubs are beneficial because they return consistent results, making the test easier to write. Also, you can run tests even if the components that stubs are present are not working yet.
Suppose you need to develop an alarm system that monitors the temperature of a room like a server room.
To do that you need to set up a temperature sensor device and use the data from that sensor to alert if the temperature is below or above a specific temperature.
First, define a Sensor
class in the sensor.py
module:
import random
class Sensor:
@property
def temperature(self):
return random.randint(10, 45)
Code language: Python (python)
The Sensor
class has a temperature property that returns a random temperature between 10 and 45. In the real world, the Sensor
class needs to connect to the sensor device to get the actual temperature.
Second, define the Alarm
class that uses a Sensor
object:
from sensor import Sensor
class Alarm:
def __init__(self, sensor=None) -> None:
self._low = 18
self._high = 24
self._sensor = sensor or Sensor()
self._is_on = False
def check(self):
temperature = self._sensor.temperature
if temperature < self._low or temperature > self._high:
self._is_on = True
@property
def is_on(self):
return self._is_on
Code language: Python (python)
By default, the is_on
property of the Alarm
is off (False
). The check()
method turns the alarm on if the temperature is lower than 18 or higher than 42.
Once the is_on
property of an Alarm
object is on, you can send it to the alarm device to alert accordingly.
Because the temperature()
method of the Sensor
returns a random temperature, it’ll be difficult to test various scenarios to ensure the Alarm
class works properly.
To resolve it, you can define a stub for the Sensor
class called TestSensor
. The TestSensor
has the temperature
property that returns a value provided when its object is initialized.
Third, define the TestSensor
in test_sensor.py
module:
class TestSensor:
def __init__(self, temperature) -> None:
self._temperature = temperature
@property
def temperature(self):
return self._temperature
Code language: Python (python)
The TestSensor
class is like the Sensor
class except that the temperature
property returns a value specified in the constructor.
Fourth, define a TestAlarm
class in the test_alarm.py
test module and import the Alarm
and TestSensor
from the alarm.py
and sensor.py
modules:
import unittest
from alarm import Alarm
from test_sensor import TestSensor
class TestAlarm(unittest.TestCase):
pass
Code language: Python (python)
Fifth, test if the alarm is off by default:
import unittest
from alarm import Alarm
from test_sensor import TestSensor
class TestAlarm(unittest.TestCase):
def test_is_alarm_off_by_default(self):
alarm = Alarm()
self.assertFalse(alarm.is_on)
Code language: Python (python)
In the test_is_alarm_off_by_default
we create a new alarm instance and use the assertFalse()
method to check if the is_on
property of the alarm object is False
.
Run the test:
python -m unittest -v
Code language: Python (python)
Output:
test_is_alarm_off_by_default (test_alarm.TestAlarm) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Code language: Python (python)
Sixth, test the check()
method of the Alarm
class in case the temperature is too high:
import unittest
from alarm import Alarm
from test_sensor import TestSensor
class TestAlarm(unittest.TestCase):
def test_is_alarm_off_by_default(self):
alarm = Alarm()
self.assertFalse(alarm.is_on)
def test_check_temperature_too_high(self):
alarm = Alarm(TestSensor(25))
alarm.check()
self.assertTrue(alarm.is_on)
Code language: Python (python)
In the test_check_temperature_too_high()
test method:
- Create an instance of the
TestSensor
with temperature 25 and passes it to theAlarm
constructor. - Call the
check()
method of the alarm object. - Use the
assertTrue()
to test if theis_on
property of the alarm isTrue
.
Run the test:
python -m unittest -v
Code language: Python (python)
Output:
test_check_temperature_too_high (test_alarm.TestAlarm) ... ok
test_is_alarm_off_by_default (test_alarm.TestAlarm) ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
Code language: Python (python)
The alarm is on because the temperature is higher than 24.
Seventh, test the check()
method of the Alarm class when the temperature is too low:
import unittest
from alarm import Alarm
from test_sensor import TestSensor
class TestAlarm(unittest.TestCase):
def test_is_alarm_off_by_default(self):
alarm = Alarm()
self.assertFalse(alarm.is_on)
def test_check_temperature_too_high(self):
alarm = Alarm(TestSensor(25))
alarm.check()
self.assertTrue(alarm.is_on)
def test_check_temperature_too_low(self):
alarm = Alarm(TestSensor(17))
alarm.check()
self.assertTrue(alarm.is_on)
Code language: Python (python)
In the test_check_temperature_too_low()
test method:
- Create an instance of the
TestSensor
with temperature 17 and passes it to the Alarm constructor. - Call the
check()
method of the alarm object. - Use the
assertTrue()
to test if the is_on property of the alarm is True.
Run the test:
python -m unittest -v
Code language: Python (python)
Output:
test_check_temperature_too_high (test_alarm.TestAlarm) ... ok
test_check_temperature_too_low (test_alarm.TestAlarm) ... ok
test_is_alarm_off_by_default (test_alarm.TestAlarm) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK
Code language: Python (python)
Seventh, test the check()
method of the Alarm
class if the temperature is in the safe range (18, 24):
import unittest
from alarm import Alarm
from test_sensor import TestSensor
class TestAlarm(unittest.TestCase):
def test_is_alarm_off_by_default(self):
alarm = Alarm()
self.assertFalse(alarm.is_on)
def test_check_temperature_too_high(self):
alarm = Alarm(TestSensor(25))
alarm.check()
self.assertTrue(alarm.is_on)
def test_check_temperature_too_low(self):
alarm = Alarm(TestSensor(15))
alarm.check()
self.assertTrue(alarm.is_on)
def test_check_normal_temperature(self):
alarm = Alarm(TestSensor(20))
alarm.check()
self.assertFalse(alarm.is_on)
Code language: Python (python)
In the test_check_normal_temperature()
method we create a TestSensor with the temperature 20 and pass it to the Alarm constructor. Since the temperature is in the range (18, 24), the alarm should be off.
Run the test:
python -m unittest -v
Code language: Python (python)
Output:
test_check_normal_temperature (test_alarm.TestAlarm) ... ok
test_check_temperature_too_high (test_alarm.TestAlarm) ... ok
test_check_temperature_too_low (test_alarm.TestAlarm) ... ok
test_is_alarm_off_by_default (test_alarm.TestAlarm) ... ok
----------------------------------------------------------------------
Ran 4 tests in 0.001s
OK
Code language: Python (python)
Using MagicMock class to create stubs
Python provides you with the MagicMock
object in the unittest.mock
module that allows you to create stubs more easily.
To create a stub for the Sensor
class using the MagicMock
class, you pass the Sensor
class to the MagicMock()
constructor:
mock_sensor = MagicMock(Sensor)
Code language: Python (python)
The mock_sensor
is the new instance of the MagicMock
class that mocks the Sensor
class.
By using the mock_sensor
object, you can set its property or call a method. For example, you can assign a specific temperature e.g., 25 to the temperature
property of the mock sensor like this:
mock_sensor.temperature = 25
Code language: Python (python)
The following shows the new version of the TestAlarm
that uses the MagicMock
class:
import unittest
from unittest.mock import MagicMock
from alarm import Alarm
from sensor import Sensor
class TestAlarm(unittest.TestCase):
def setUp(self):
self.mock_sensor = MagicMock(Sensor)
self.alarm = Alarm(self.mock_sensor)
def test_is_alarm_off_by_default(self):
alarm = Alarm()
self.assertFalse(alarm.is_on)
def test_check_temperature_too_high(self):
self.mock_sensor.temperature = 25
self.alarm.check()
self.assertTrue(self.alarm.is_on)
def test_check_temperature_too_low(self):
self.mock_sensor.temperature = 15
self.alarm.check()
self.assertTrue(self.alarm.is_on)
def test_check_normal_temperature(self):
self.mock_sensor.temperature = 20
self.alarm.check()
self.assertFalse(self.alarm.is_on)
Code language: Python (python)
Using patch() method
To make it easier to work with MagicMock
, you can use the patch()
as a decorator. For example:
import unittest
from unittest.mock import patch
from alarm import Alarm
class TestAlarm(unittest.TestCase):
@patch('sensor.Sensor')
def test_check_temperature_too_low(self, sensor):
sensor.temperature = 10
alarm = Alarm(sensor)
alarm.check()
self.assertTrue(alarm.is_on)
Code language: Python (python)
In this example, we use a @patch
decorator on the test_check_temperature_too_low()
method. In the decorator, we pass the sensor.Sensor
as a target to patch.
Once we use the @patch
decorator, the test method will have the second parameter which is an instance of the MagicMock
that mocks the sensor.Sensor
class.
Inside the test method, we set the temperature property of the sensor to 10, create a new instance of the Alarm class, and call check()
method, and use the assertTrue()
method to test if the alarm is on.
Run the test:
python -m unittest -v
Code language: Python (python)
Output:
test_check_temperature_too_low (test_alarm_with_patch.TestAlarm) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.001s
Code language: Python (python)
Summary
- Use stubs to return hard-coded values for testing.
- Use
MagicMock
class ofunittest.mock
module to create stubs. - Use
patch()
to createMagicMock
more easily.