Skip to main content
PyPI Version Supported Python versions https://img.shields.io/badge/code%20style-black-000000.svg pre-commit.ci status

pytest-localserver

pytest-localserver is a plugin for the pytest testing framework which enables you to test server connections locally.

Sometimes monkeypatching urllib2.urlopen() just does not cut it, for instance if you work with urllib2.Request, define your own openers/handlers or work with httplib. In these cases it may come in handy to have an HTTP server running locally which behaves just like the real thing [1]. Well, look no further!

Quickstart

myproject.py

import requests


def make_request(url):
    """A function that makes a web request."""
    rsp = requests.get(url)
    return rsp.text

test_myproject.py

from myproject import request


def test_make_request(httpserver):
    httpserver.serve_content(
        content="success",
        code=200,
    )

    assert make_request(httpserver.url) == "success"

How-To

Let’s say you have a function to scrape HTML which only required to be pointed at a URL :

import requests

def scrape(url):
  html = requests.get(url).text
  # some parsing happens here
  # ...
  return result

You want to test this function in its entirety without having to rely on a remote server whose content you cannot control, neither do you want to waste time setting up a complex mechanism to mock or patch the underlying Python modules dealing with the actual HTTP request (of which there are more than one BTW). So what do you do?

You simply use pytest’s fixture feature and simulate an entire server locally!

def test_retrieve_some_content(httpserver):
    httpserver.serve_content(open("cached-content.html").read())
    assert scrape(httpserver.url) == "Found it!"

What happened here is that for the duration of your tests an HTTP server is started on a random port on localhost which will serve the content you tell it to and behaves just like the real thing.

The added bonus is that you can test whether your code behaves gracefully if there is a network problem:

def test_content_retrieval_fails_graciously(httpserver):
  httpserver.serve_content("File not found!", 404)
  pytest.raises(ContentNotFoundException, scrape, httpserver.url)

The same thing works for SMTP servers, too:

def test_sending_some_message(smtpserver):
    mailer = MyMailer(host=smtpserver.addr[0], port=smtpserver.addr[1])
    mailer.send(
        to="bob@example.com",
        from_="alice@example.com",
        subject="MyMailer v1.0",
        body="Check out my mailer!"
    )
    assert len(smtpserver.outbox)==1

Here an SMTP server is started which accepts e-mails being sent to it. The nice feature here is that you can actually check if the message was received and what was sent by looking into the smtpserver’s outbox.

It is really that easy!

Fixtures

Here is a short overview of the available pytest fixtures and their usage. This information is also available via pytest –fixtures. For more details I suggest poking around in the code itself.

httpserver

provides a threaded HTTP server instance running on localhost. It has the following attributes:

  • code - HTTP response code (int)

  • content - content of next response (str, bytes, or iterable of either)

  • headers - response headers (dict)

  • chunked - whether to chunk-encode the response (enumeration)

  • store_request_data - whether to store request data for later use

Once these attributes are set, all subsequent requests will be answered with these values until they are changed or the server is stopped. A more convenient way to change these is :

httpserver.serve_content(
    content=None,
    code=200,
    headers=None,
    chunked=pytest_localserver.http.Chunked.NO,
    store_request_data=True
)

The chunked attribute or parameter can be set to

  • Chunked.YES, telling the server to always apply chunk encoding

  • Chunked.NO, telling the server to never apply chunk encoding

  • Chunked.AUTO, telling the server to apply chunk encoding only if the Transfer-Encoding header includes chunked

If chunk encoding is applied, each str or bytes in content becomes one chunk in the response.

You can use store_request_data=False to disable loading the request data into memory. This will make it impossible to check the request data using httpserver.requests[index].data but may make sense when posting a larger amount of data and you don’t need to check this.

The server address can be found in property

  • url

which is the string representation of tuple server_address (host as str, port as int).

If you want to check which form fields have been POSTed, Try

httpserver.serve_content(..., show_post_vars=True)

which will display them as parsable text.

If you need to inspect the requests sent to the server, a list of all received requests can be found in property

  • requests

which is a list of werkzeug.wrappers.Request objects.

httpsserver

is the same as httpserver only with SSL encryption.

smtpserver

provides a threaded SMTP server, with an API similar to smtpd.SMTPServer, (the deprecated class from the Python standard library) running on localhost. It has the following attributes:

  • addr - server address as tuple (host as str, port as int)

  • outbox - list of email.message.Message instances received.

Using your a WSGI application as test server

As of version 0.3 you can now use a WSGI application to run on the test server :

import pytest
from pytest_localserver.http import WSGIServer

from myproject import make_request


def simple_app(environ, start_response):
    """Respond with success."""
    status = "200 OK"
    response_headers = [("Content-type", "text/plain")]
    start_response(status, response_headers)
    return ["success".encode("utf-8")]


@pytest.fixture
def testserver():
    """Server for simple_app."""
    server = WSGIServer(application=simple_app)
    server.start()
    yield server
    server.stop()


def test_make_request(testserver):
    """make_request() should return "success"."""
    assert make_request(testserver.url) == "success"

Have a look at the following page for more information on WSGI: http://wsgi.readthedocs.org/en/latest/learn.html

Download and Installation

You can install the plugin by running

pip install pytest-localserver

Alternatively, get the latest stable version from PyPI or the latest bleeding-edge from Github.

License and Credits

This plugin is released under the MIT license. You can find the full text of the license in the LICENSE file.

Copyright (C) 2010-2022 Sebastian Rahlf and others (see AUTHORS).

Some parts of this package is based on ideas or code from other people:

  • I borrowed some implementation ideas for the httpserver from linkchecker.

  • The implementation for the SMTP server is based on the Mailsink recipe by Adam Feuer, Matt Branthwaite and Troy Frever.

  • The HTTPS implementation is based on work by Sebastien Martini.

Thanks guys!

Development and future plans

Feel free to clone the repository and add your own changes. Pull requests are always welcome!:

git clone https://github.com/pytest-dev/pytest-localserver

If you find any bugs, please file a report.

Test can be run with tox.

I already have a couple of ideas for future versions:

  • support for FTP, SSH (maybe base all on twisted?)

  • making the SMTP outbox as convenient to use as django.core.mail.outbox

  • add your own here!

Preparing a release

For package maintainers, here is how we release a new version:

  1. Ensure that the CHANGES file is up to date with the latest changes.

  2. Make sure that all tests pass on the version you want to release.

  3. Use the new release form on Github (or some other equivalent method) to create a new release, following the pattern of previous releases.

    • Each release has to be based on a tag. You can either create the tag first (e.g. using git tag) and then make a release from that tag, or you can have Github create the tag as part of the process of making a release; either way works.

    • The tag name must be the PEP 440-compliant version number prefixed by v, making sure to include at least three version number components (e.g. v0.6.0).

    • The “Auto-generate release notes” button will be useful in summarizing the changes since the last release.

  4. Using either the release workflows page or the link in the email you received about a “Deployment review”, go to the workflow run created for the new release and click “Review deployments”, then either approve or reject the two deployments, one to Test PyPI and one to real PyPI. (It should not be necessary to reject a deployment unless something really weird happens.) Once the deployment is approved, Github will automatically upload the files.


Release files for pytest-localserver 0.10.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pytest-localserver 0.10.0
File Size Uploaded
pytest_localserver-0.10.0.tar.gz 30.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pytest-localserver 0.10.0
File Interpreter ABI Platform
pytest_localserver-0.10.0-py3-none-any.whl Python 3 none any Details

Total release size: 50.6 kB

Release files / pytest_localserver-0.10.0.tar.gz

Download URL pytest_localserver-0.10.0.tar.gz
Size 30.8 kB
Tags Source
SHA-256 checksum
How to use checksums
2607197f390912ab25525d129ac43c3c875049257368b3fe09b5cd03dcc526af
BLAKE2b-256 checksum
How to use checksums
3ef0415ed723f04749c3e3417b51797fc5aebe4149ee4b23997e63e6196708bd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 24, 2025.

Transparency log

Release files / pytest_localserver-0.10.0-py3-none-any.whl

Download URL pytest_localserver-0.10.0-py3-none-any.whl
Size 19.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
de526dc5fb26395fb7bbf26bfc14dde5ac3390ba8d8c7de42dfa492d65e0c448
BLAKE2b-256 checksum
How to use checksums
029a544c5163c56f3e461021bf408c947567f67ec8a83a800efb5b89d808bbae
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 24, 2025.

Transparency log

Release history Release notifications | RSS feed

This release

0.10.0 This release

2 release files

0.9.0

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.1

2 release files

0.5.0

1 release file

0.4.2

1 release file

0.4.1

1 release file

0.4.0

0.3.7

1 release file

0.3.6

1 release file

0.3.5

1 release file

0.3.4

1 release file

0.3.3

1 release file

0.3.2

1 release file

0.3

1 release file

0.1.5

1 release file

0.1.4

1 release file

0.1.3

1 release file

0.1.2

1 release file

0.1.1

1 release file

0.1

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page