p4-mapreduce
Mocking
This tutorial explains Python mocking by walking through a Project 4 test case. By the end of this tutorial, you should be able to step through a test case that uses mocking and understand how it provides input to your code.
A mock object replaces part of your program for testing. For example, your MapReduce framework has a Manager and a Worker that communicate over the network. With mocking, we can independently test a Worker implementation by mocking the responses from the Manager.
Overview
There are 3 types of test cases in project 4 apart from code style and shell scripting.
Test case name | Description |
---|---|
test_manager_* |
Tests your Manager with a mock Worker |
test_worker_* |
Tests your Worker with a mock Manager |
test_integration_* |
Tests your Manager and Worker without mocking |
Example
Let’s take a look at test_worker_01.py
, which verifies that your Worker correctly registers with the Manager. This test uses your Worker code and mocks the messages of the Manager.
Message Generator
At the beginning of test_worker_01.py
, we see a function that hardcodes the messages sent by the Manager in this test. Each yield
statement transfers control back to your Worker code.
def manager_message_generator(mock_socket):
"""Fake Manager messages."""
# First message
utils.wait_for_register_messages(mock_socket)
yield json.dumps({
"message_type": "register_ack",
"worker_host": "localhost",
"worker_port": 6001,
}).encode('utf-8')
yield None
# Shutdown
yield json.dumps({
"message_type": "shutdown",
}).encode('utf-8')
yield None
Later, we’ll see that the test connects manager_message_generator()
to a mock client socket that would normally receive messages from the Manager. Whenever your Worker calls a function on a socket, it’s calling it on a mock socket instead of a real one.
Mock Socket
The test connects the message generator function to a mock socket.
We first mock the socket library’s socket class. We can then mock anything produced by the socket class, like the sendall()
function.
def test_register(mocker):
# Mock the socket library socket class
mock_socket = mocker.patch("socket.socket")
# sendall() records messages
mock_sendall = mock_socket.return_value.__enter__.return_value.sendall
Mock the socket accept()
function to return a mock client socket instead of a of real one.
# accept() returns a mock client socket
mock_clientsocket = mocker.MagicMock()
mock_accept = mock_socket.return_value.__enter__.return_value.accept
mock_accept.return_value = (mock_clientsocket, ("127.0.0.1", 10000))
Finally, connect our manager_message_generator()
function to the mock socket. When your code calls recv()
it will get the next value yield
ed by the generator.
# recv() returns values generated by manager_message_generator()
mock_recv = mock_clientsocket.recv
mock_recv.side_effect = manager_message_generator(mock_sendall)
Run unit under test
The test then runs your Worker implementation. Each time the Worker calls recv()
, manager_message_generator()
yields another message.
try:
mapreduce.worker.Worker(
host="localhost",
port=6001,
manager_host="localhost",
manager_port=6000,
)
utils.wait_for_threads()
except SystemExit as error:
assert error.code == 0
When the Worker shuts down, the test case continues.
Verify function calls
A mock object keeps track of function calls. At the end of the test, we verify that your Worker called the correct socket functions. This code checks that the Worker called 4 methods: the constructor, setsockopt
, bind
, and listen
with correct arguments.
mock_socket.assert_has_calls([
mocker.call(socket.AF_INET, socket.SOCK_STREAM),
mocker.call().__enter__().setsockopt(
socket.SOL_SOCKET,
socket.SO_REUSEADDR,
1,
),
mocker.call().__enter__().bind(('localhost', 6001)),
mocker.call().__enter__().listen(),
], any_order=True)
Verify messages
A mock object keeps track of function call arguments. The test gets a list of the messages your Worker sent to the mock Manager. Then, it verifies that the messages look OK.
messages = utils.get_messages(mock_socket)
messages = utils.filter_not_heartbeat_messages(messages)
assert messages == [
{
"message_type": "register",
"worker_host": "localhost",
"worker_port": 6001
},
]
The utils.get_messages()
function collects the messages sent from your Worker to the mock Manager using the socket library sendall()
.
Further reading
Check out this mocking tutorial if you would like to learn more.
Acknowledgments
Original document written by Zachary Papanastasopoulos and Andrew DeOrio awdeorio@umich.edu.
This document is licensed under a Creative Commons Attribution-NonCommercial 4.0 License. You’re free to copy and share this document, but not to sell it. You may not share source code provided with this document.