- Start a temporary http server locally with Pytest
- Implementation
- conftest.py
- index.html
- test_httpserver.py
- Execution result
- Other
- pytest-httpserver 1.0.8
- Example
- Handling a simple GET request
- Features
- Requests
- Responses
- Behave support
- Missing features
- Donation
- Testing python BaseHttpServer
- Passing dependencies on the constructor
- Test the server
Start a temporary http server locally with Pytest
By setting up a local server when executing a unit test, it is possible to perform tests that do not depend on the outside.
Convenient for API and crawler development.
—Describe http server start / end processing as fixture in conftest.py —Call a fixture in a test function
Implementation
conftest.py
import pytest from http.server import ( HTTPServer as SuperHTTPServer, SimpleHTTPRequestHandler ) import threading class HTTPServer(SuperHTTPServer): """ Class for wrapper to run SimpleHTTPServer on Thread. Ctrl +Only Thread remains dead when terminated with C. Keyboard Interrupt passes. """ def run(self): try: self.serve_forever() except KeyboardInterrupt: pass finally: self.server_close() @pytest.fixture() def http_server(): host, port = '127.0.0.1', 8888 url = f'http://:/index.html' # serve_Run forever under thread server = HTTPServer((host, port), SimpleHTTPRequestHandler) thread = threading.Thread(None, server.run) thread.start() yield url #Transition to test here #End thread server.shutdown() thread.join()
Transfer control to the test function with the yield statement. The setUp and tearDown in the unit test are before and after the yield statement, respectively.
Place the content in the execution directory.
index.html
Use fixture ( http_server ) in test function
test_httpserver.py
import requests def test_index(http_server): url = http_server response = requests.get(url) assert response.text == 'Hello pytest!'
Execution result
$ pytest --setup-show test_httpserver.py ========================================= test session starts =========================================platform linux -- Python 3.8.1, pytest-5.3.3, py-1.8.1, pluggy-0.13.1 rootdir: /home/skokado/workspace/sandbox collected 1 item test_httpserver.py SETUP F http_server test_httpserver.py::test_index (fixtures used: http_server). TEARDOWN F http_server ========================================== 1 passed in 0.60s ==========================================
You can trace the generation of fixtures with the —setup-show option. You can see that the test_index function uses the fixture http_server .
Other
Bankushi (@vaaaaanquish), I used it as a reference.
pytest-httpserver 1.0.8
This library is designed to help to test http clients without contacting the real http server. In other words, it is a fake http server which is accessible via localhost can be started with the pre-defined expected http requests and their responses.
Example
Handling a simple GET request
You can also use the library without pytest. There's a with statement to ensure that the server is stopped.
Please find the API documentation at https://pytest-httpserver.readthedocs.io/en/latest/.
Features
You can set up a dozen of expectations for the requests, and also what response should be sent by the server to the client.
Requests
There are three different types:
- permanent: this will be always served when there’s match for this request, you can make as many HTTP requests as you want
- oneshot: this will be served only once when there’s a match for this request, you can only make 1 HTTP request
- ordered: same as oneshot but the order must be strictly matched to the order of setting up
You can also fine-tune the expected request. The following can be specified:
- URI (this is a must)
- HTTP method
- headers
- query string
- data (HTTP body of the request)
- JSON (HTTP body loaded as JSON)
Responses
Once you have the expectations for the request set up, you should also define the response you want to send back. The following is supported currently:
- respond arbitrary data (string or bytearray)
- respond a json (a python dict converted in-place to json)
- respond a Response object of werkzeug
- use your own function
Similar to requests, you can fine-tune what response you want to send:
Behave support
Using the BlockingHTTPServer class, the assertion for a request and the response can be performed in real order. For more info, see the test, the howto and the API documentation.
Missing features
Donation
If you want to donate to this project, you can find the donate button at the top of the README.
Currently, this project is based heavily on werkzeug. Werkzeug does all the heavy lifting behind the scenes, parsing HTTP request and defining Request and Response objects, which are currently transparent in the API.
Testing python BaseHttpServer
While the development of https://www.agalera.eu/standalone-app-raspberry-pi/ I needed to use python’s BaseHttpServer and inject some dependencies into it.
It turns out, there’s no easy way of doing that. Moreover, I wanted to achieve 100% code coverage testing, so I should found a way of testing that code.
Here’s the code I need to test:
import socketserver from http import server class DogFeederServer(server.BaseHTTPRequestHandler): def __init__(self, camera_output, call_dog, servo, *args, **kwargs): self.camera_output = camera_output self.call_dog = call_dog self.servo = servo # BaseHTTPRequestHandler calls do_GET **inside** __init__ . # So we have to call super().__init__ after setting attributes. super().__init__(*args, **kwargs) def do_GET(self): if self.path == "/stream.mjpg": self.send_response(200) # do some magic with HTTP Streaming else: self.send_error(404) self.end_headers() def do_POST(self): if self.path == "/api/call": if self.call_dog(): self.send_response(200) else: self.send_response(500) elif self.path == "/api/treat": self.servo.open_and_close() self.send_response(200) else: self.send_error(404) self.end_headers() class StreamingServer(socketserver.ThreadingMixIn, server.HTTPServer): allow_reuse_address = True daemon_threads = True
As you can see, the code is really simple.
The problem comes when you realise there are no easy way of calling the constructor of the server and pass the dependencies
Passing dependencies on the constructor
Hopefully I discovered this StackOverflow post where someone has experience the same issue: https://stackoverflow.com/questions/21631799/how-can-i-pass-parameters-to-a-requesthandler
I really like the approach of the “partial” application: we pass the arguments before and once the app is created with the arguments, is passed to the server:
address = ("", 8000) handler = partial( DogFeederServer, camera_output, call_dog, servo, ) server = StreamingServer(address, handler) server.serve_forever()
Once we have the “partial” approach, we could easily provide mocks for the dependencies in the tests
Test the server
The only way of testing the base HTTP server I found is to create some sort of “integration testing”: provide mocks to the server but actually start the HTTP server. To test the whole logic, we could use requests library to do the HTTP calls:
import socket from functools import partial from threading import Thread from unittest import TestCase from unittest.mock import MagicMock import requests from dogfeeder.server import DogFeederServer, StreamingServer class ServerTest(TestCase): def setUp(self): super(ServerTest, self).setUp() self.get_free_port() self.camera_output_mock = MagicMock() self.call_dog_mock = MagicMock() self.servo_mock = MagicMock() address = ("", self.mock_server_port) handler = partial( DogFeederServer, self.camera_output_mock, self.call_dog_mock, self.servo_mock, ) self.mock_server = StreamingServer(address, handler) # Start running mock server in a separate thread. # Daemon threads automatically shut down when the main process exits. self.mock_server_thread = Thread(target=self.mock_server.serve_forever) self.mock_server_thread.setDaemon(True) self.mock_server_thread.start() def test_servo_open_close(self): url = f"http://localhost:self.mock_server_port>/api/treat" response = requests.post(url) self.servo_mock.open_and_close.assert_called_once() assert response.status_code == 200 def test_invalid_path(self): url = f"http://localhost:self.mock_server_port>/unknown" response = requests.post(url) assert response.status_code == 404 response = requests.get(url) assert response.status_code == 404 def tearDown(self): super(ServerTest, self).tearDown() def get_free_port(self): s = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM) s.bind(("localhost", 0)) __, port = s.getsockname() s.close() self.mock_server_port = port
The key here is to start a daemon thread (that will die when the test ends) to start the HTTP server