# aioresponses gives aiohttp tests a mocking decorator that speaks async

> aioresponses is a small helper for mocking HTTP requests made through aiohttp, usable as a decorator or a context manager, covering the six common methods, JSON payloads, headers, regex URLs, redirects and repeated responses for retry tests. It is MIT licensed, it needs Python 3.10 and aiohttp 3.8 or later, and its last tagged release is from 2022 while the branch was pushed in 2026.

**pnuckowski/aioresponses** — Aioresponses is a helper for mock/fake web requests in python aiohttp package.

- Repository: https://github.com/pnuckowski/aioresponses
- Stars: 556 · Forks: 105
- Language: Python
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/pnuckowski-aioresponses

## The gap it fills was in the async world

aioresponses is a helper to mock or fake web requests in the Python aiohttp package.

The reason it exists is stated plainly in the documentation. For the requests module there are a lot of packages that help with testing, naming httpretty, responses and requests-mock. When it comes to testing asynchronous HTTP requests it is a bit harder, at least at the beginning. The purpose of this package is to provide an easy way to test asynchronous HTTP requests.

So the design brief is narrow on purpose. It is not a general HTTP mocking framework with adapters for every client. It mocks aiohttp, and it does that in a way that fits how aiohttp code is written, which is asynchronously and usually inside a coroutine.

What it can mock is a status code, a body, a payload for a JSON response and headers, and the supported HTTP methods are GET, POST, PUT, PATCH, DELETE and OPTIONS.

The supported versions are Python 3.10 or newer and aiohttp 3.8 up to but not including 4.0, which is worth noting because the aiohttp 4 line is exactly where a mock built on aiohttp internals would be most likely to break.

## A decorator or a context manager, and the same assertions either way

To mock out an HTTP request you use aioresponses as a method decorator or as a context manager, and the two forms support the same assertions.

The decorator form takes the mock as an argument to the test function:

```python
    import aiohttp
    import asyncio
    from aioresponses import aioresponses

    @aioresponses()
    def test_request(mocked):
        loop = asyncio.get_event_loop()
        mocked.get('http://example.com', status=200, body='test')
        session = aiohttp.ClientSession()
        resp = loop.run_until_complete(session.get('http://example.com'))

        assert resp.status == 200
        mocked.assert_called_once_with('http://example.com')
```

The context manager form is the same idea with a with block, and the documentation recommends the payload argument for a JSON response:

```python
    def test_ctx():
        loop = asyncio.get_event_loop()
        session = aiohttp.ClientSession()
        with aioresponses() as m:
            m.get('http://test.example.com', payload=dict(foo='bar'))

            resp = loop.run_until_complete(session.get('http://test.example.com'))
            data = loop.run_until_complete(resp.json())

            assert dict(foo='bar') == data
            m.assert_called_once_with('http://test.example.com')
```

The assert_called_once_with check at the end is the part that makes these worth writing, because it verifies the URL that was actually requested, not just that a response came back.

## Headers are mocked too, with a case detail that will bite you

Any HTTP headers can be mocked, and the example includes a note that saves an afternoon of confusion. You pass connection as the header name, but you get Connection back capitalised, because underneath multidict is used to work with HTTP headers.

```python
    import asyncio
    import aiohttp
    from aioresponses import aioresponses

    @aioresponses()
    def test_http_headers(m):
        loop = asyncio.get_event_loop()
        session = aiohttp.ClientSession()
        m.post(
            'http://example.com',
            payload=dict(),
            headers=dict(connection='keep-alive'),
        )

        resp = loop.run_until_complete(session.post('http://example.com'))

        # note that we pass 'connection' but get 'Connection' (capitalized)
        # under the neath `multidict` is used to work with HTTP headers
        assert resp.headers['Connection'] == 'keep-alive'
        m.assert_called_once_with('http://example.com', method='POST')
```

The capitalisation behaviour comes from the header container aiohttp itself uses, so it is not an arbitrary choice by the mocking library. It matches what a real response would do, which is the right behaviour, but it means a test asserting on a lowercase header name will fail against a mocked response and pass against a real one.

The method is also part of the assertion, so a test can verify that a POST went out as a POST rather than relying on the URL to identify the call.

## Different responses for the same URL, and repeats for retry logic

Registering different responses for the same URL is supported, and the response that is used is the first one that has not been used yet. Two registrations for the same URL with status 500 and then status 200 mean the first request gets the error and the second gets the success.

On top of that there is the repeat argument, which exists for testing retrying mechanisms. By default repeat=False means the response is not repeated, and repeat=1 does the same. repeat=n repeats a response n times, and repeat=True repeats it indefinitely.

```python
    import asyncio
    import aiohttp
    from aioresponses import aioresponses

    @aioresponses()
    def test_multiple_responses(m):
        loop = asyncio.get_event_loop()
        session = aiohttp.ClientSession()
        m.get('http://example.com', status=500, repeat=2)
        m.get('http://example.com', status=200)  # will take effect after two preceding calls

        resp1 = loop.run_until_complete(session.get('http://example.com'))
        resp2 = loop.run_until_complete(session.get('http://example.com'))
        resp3 = loop.run_until_complete(session.get('http://example.com'))

        assert resp1.status == 500
        assert resp2.status == 500
        assert resp3.status == 200
```

The comment in that example is the rule: the second registration takes effect after the two preceding calls have consumed the repeated response. So the semantics are a queue of registrations, and repeat controls how many times each entry is served before the next one starts.

This is the feature that makes it possible to test a client with a backoff or retry loop, since a single static mock cannot express two failures followed by a success.

## Regex URLs, redirects, and letting real requests through

URLs can be matched with regular expressions rather than exact strings, which matters when the query string varies. The example compiles a pattern and registers it in place of a literal URL:

```python
    def test_regexp_example(m):
        loop = asyncio.get_event_loop()
        session = aiohttp.ClientSession()
        pattern = re.compile(r'^http://example\.com/api\?foo=.*$')
        m.get(pattern, status=200)

        resp = loop.run_until_complete(session.get('http://example.com/api?foo=bar'))

        assert resp.status == 200
```

Redirects are mocked too, and both absolute and relative Location headers are supported, with the response URL asserted after following them:

```python
    @aioresponses()
    def test_redirect_example(m):
        loop = asyncio.get_event_loop()
        session = aiohttp.ClientSession()

        # absolute urls are supported
        m.get(
            'http://example.com/',
            headers={'Location': 'http://another.com/'},
            status=307
        )

        resp = loop.run_until_complete(
            session.get('http://example.com/', allow_redirects=True)
        )
        assert resp.url == 'http://another.com/'

        # and also relative
        m.get(
            'http://example.com/',
            headers={'Location': '/test'},
            status=307
        )
        resp = loop.run_until_complete(
            session.get('http://example.com/', allow_redirects=True)
        )
        assert resp.url == 'http://example.com/test'
```

Then there is the passthrough, which lets you mock some hosts and hit the real ones. Passing a list of servers to passthrough makes requests to those servers actually happen:

```python
    import asyncio
    import aiohttp
    from aioresponses import aioresponses

    @aioresponses(passthrough=['http://backend'])
    def test_passthrough(m, test_client):
        session = aiohttp.ClientSession()
        # this will actually perform a request
        resp = loop.run_until_complete(session.get('http://backend/api'))
```

And passthrough_unmatched passes through every request that has not been explicitly mocked, which is the setting for a test that mocks one endpoint and lets the rest of the client behave normally.

## A small package, and a release history that has stopped moving

The repository is small and well organised, and it shows the shape of a project that is small because its scope is small.

There is an aioresponses package directory, a tests directory, a setup.py that uses pbr for metadata with package_data pointing at a py.typed file, which is what gives the package inline type hints for consumers. There is a tox.ini for running the test suite across Python versions, a unittest.cfg, a .coveragerc, requirements.txt and requirements-dev.txt, a .landscape.yml configuration for a code quality service, a .pyup.yml for dependency updates, an AUTHORS.rst, a HISTORY.rst for the changelog and a CONTRIBUTING.rst.

The runtime requirements are two lines, packaging 22.0 or newer and aiohttp 3.8 up to but not including 4.0. The Makefile offers the usual set of targets plus lint with flake8, coverage with the coverage tool, and a release target that builds and uploads both an sdist and a wheel.

The release history is the part to weigh. The tagged versions are 0.7.4 from 2022-12-14, 0.7.3 from the same day, and 0.7.2 from 2021-04-13. The last push was on 2026-06-23. So the branch has been touched recently while the published releases have not moved since the end of 2022, and that combination usually means maintenance without new features. The project is MIT licensed and it is not archived, and the topics are aiohttp, asyncio, mocking and test.

For a test helper, that is a reasonable position to be in, since the aiohttp 3.x API it targets is stable, but it should be a decision rather than a surprise.

## Conclusion

Adopt aioresponses if you test asyncio code that calls HTTP APIs through aiohttp and want to assert on status, body and headers without a network or a running server. Do not adopt it if you use httpx or requests rather than aiohttp, since it hooks into aiohttp specifically, and expect the project to be in maintenance mode rather than actively developed. Before you pin it, check three things: the aiohttp version you run, since the supported range is 3.8 up to but not including 4.0, whether your team needs the passthrough options, and whether the last tagged release is recent enough for your policy, because the newest published version is 0.7.4 from 2022-12-14.

## FAQ

### How do I install aioresponses?

Run pip install aioresponses. The supported versions are Python 3.10 or newer and aiohttp 3.8 up to but not including 4.0, and the runtime requirements also include packaging 22.0 or newer.

### How do I mock an aiohttp request in aioresponses?

Use aioresponses as a method decorator or as a context manager, register the response with m.get, m.post and so on for the URL, and then assert with m.assert_called_once_with. The status code, body, payload for JSON and headers can all be mocked, for the GET, POST, PUT, PATCH, DELETE and OPTIONS methods.

### How do I test retry logic with aioresponses?

Use the repeat argument on a registration. repeat=False or repeat=1 serves the response once, repeat=n serves it n times and repeat=True serves it indefinitely. Register a second response for the same URL to take effect once the preceding calls have been consumed.

### Can aioresponses match URLs with a regular expression?

Yes. Compile a pattern and register it in place of a URL string, for example pattern = re.compile(r'^http://example\.com/api\?foo=.*$') followed by m.get(pattern, status=200). That is useful when the query string varies between calls.

### Is aioresponses still maintained?

The last push was on 2026-06-23, but the newest published release is 0.7.4 from 2022-12-14, with 0.7.3 from the same day and 0.7.2 from 2021-04-13. The project is MIT licensed and is not archived, so it looks like maintenance rather than active feature work.

## Sources

- [Issues](https://github.com/pnuckowski/aioresponses/issues)
- [License: MIT](https://github.com/pnuckowski/aioresponses/blob/master/LICENSE)
- [pnuckowski/aioresponses on GitHub](https://github.com/pnuckowski/aioresponses)
- [README](https://github.com/pnuckowski/aioresponses/blob/master/README.md)
- [Releases](https://github.com/pnuckowski/aioresponses/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/pnuckowski-aioresponses
