Testing External APIs with a Mock Server
When building software applications that rely on external APIs, testing can often be a challenge. You may encounter issues such as rate limits, server downtime, and slow response times.
These can all affect the speed and reliability of your testing, making it difficult to identify and fix problems. A mock server is an effective way to simulate the behavior of an external API, reducing testing complexities and making it easier to identify and fix issues.
Mock Objects and Building a Mock Server
Mock Objects are simulated objects that mimic the behavior of a real object in controlled ways.
They are powerful tools for testing software applications that rely on external APIs. One of the most commonly used types of mock objects is a mock server. A mock server is a simulation of a real server that can be used to test how your application will interact with the external API.
Programming a Basic Mock Server and Tests
To build a mock server, all you need is a computer with Python installed. You can use the HTTPServer module in Python to create a basic mock server that responds to requests.
Here is an example of how to create a mock server:
from http.server import HTTPServer, BaseHTTPRequestHandler
import socket
class MockServer(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(b'Hello, World!')
def get_free_port():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('localhost', 0))
return s.getsockname()[1]
class TestMockServer:
@classmethod
def setup_class(cls):
cls.mock_server_port = get_free_port()
cls.mock_server = HTTPServer(('localhost', cls.mock_server_port), MockServer)
cls.mock_server._thread.daemon = True
cls.mock_server._thread.start()
def test_request_response(self):
response = requests.get(f'http://localhost:{self.mock_server_port}/')
assert response.status_code == 200
assert response.text == 'Hello, World!'
In this example, we have defined a simple mock server that responds to GET requests with a “Hello, World!” message. We also have a test that sends a GET request to the mock server and expects a 200 response code and the “Hello, World!” message.
Refactoring Code to Pull API Base URL into a Constant and Encapsulating Logic into a Function
To make our code more maintainable, we can refactor it to encapsulate the mock server logic into a function and pull the API base URL into a constant. Here’s what our refactored code will look like:
import requests
API_BASE_URL = 'https://api.example.com'
def get_users():
response = requests.get(f'{API_BASE_URL}/users')
return response.json()
class TestMockServer:
@classmethod
def setup_class(cls):
cls.mock_server_port = get_free_port()
cls.mock_server = HTTPServer(('localhost', cls.mock_server_port), MockServer)
cls.mock_server._thread.daemon = True
cls.mock_server._thread.start()
def test_request_response(self):
with patch.dict('os.environ', {'API_BASE_URL': f'http://localhost:{self.mock_server_port}/'}):
assert get_users() == [{'id': 1, 'name': 'John Doe'}, {'id': 2, 'name': 'Jane Smith'}]
In this refactored code, we have created a function that makes a call to our external API and returns the response in JSON format. We have also encapsulated the logic related to the mock server into our test case setup method.
By pulling the API base URL into a constant, we can easily modify our code to use a real API in production and a mock server for testing.
Updating Tests to Use Service Function and Patching the URL with Mock Server Address
Finally, we need to update our tests to use our service function instead of making requests directly to the external API. We also need to patch the URL with the mock server address.
Here’s what our updated test looks like:
import requests
API_BASE_URL = 'https://api.example.com'
def get_users():
response = requests.get(f'{API_BASE_URL}/users')
return response.json()
class TestMockServer:
@classmethod
def setup_class(cls):
cls.mock_server_port = get_free_port()
cls.mock_server = HTTPServer(('localhost', cls.mock_server_port), MockServer)
cls.mock_server._thread.daemon = True
cls.mock_server._thread.start()
def test_request_response(self):
with patch.dict('os.environ', {'API_BASE_URL': f'http://localhost:{self.mock_server_port}/'}):
with patch('requests.get') as mock_request:
mock_request.return_value.json.return_value = [{'id': 1, 'name': 'John Doe'}, {'id': 2, 'name': 'Jane Smith'}]
assert get_users() == [{'id': 1, 'name': 'John Doe'}, {'id': 2, 'name': 'Jane Smith'}]
In this updated test, we are patching the requests.get
method so that it returns the same response that we would expect from the external API. We are also patching the os.environ
dictionary to substitute the mock server’s URL for the real API’s URL.
Next Steps for Expanding Mock Server Functionality
Our mock server is currently very simple, responding with the same message for every GET request. However, we can expand its functionality to handle other types of requests, such as POST and PUT.
We can also simulate error responses such as HTTP 404 and HTTP 405. Additionally, we can create mock responses that mimic actual data returned by the external API, which can be useful for testing edge cases and conditions that may not be present in our mock data.
Conclusion
In conclusion, using a mock server is a powerful tool for testing software applications that rely on external APIs. By creating a simulation of the external API, we can reduce testing complexities and make it easier to identify and fix issues. Furthermore, techniques such as encapsulation and code refactoring can make testing and maintenance of our codebase easier in the long run.
As we continue to expand and improve our mock server functionality, we can achieve more accurate testing results and build better software applications. In conclusion, testing external APIs with a mock server is a helpful and effective way to simulate the behavior of an external API, reducing testing complexities and making it easier to identify and fix issues.
This article highlights how to build a basic mock server in Python and how to leverage mock objects to test software applications that rely on external APIs. We also explained how to encapsulate code and refactor it to enable better testing and maintenance of codebases. Mock servers can help test edge cases and conditions that may not be present in the actual data, allowing for more accurate testing results.
By continually expanding and improving mock server functionality, we can build better software applications.