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 yielded 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.