Skip to content
This repository has been archived by the owner on Dec 16, 2023. It is now read-only.

Commit

Permalink
Add test section
Browse files Browse the repository at this point in the history
  • Loading branch information
Ondrej Unger committed Sep 9, 2022
1 parent 3609446 commit d3f4fad
Showing 1 changed file with 205 additions and 0 deletions.
205 changes: 205 additions & 0 deletions docs/_posts/2022-09-09-05-add-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
---
title: 05 - Add tests
author: Ondrej Unger
date: 2022-09-09
category: implementation
layout: post
---

> **_BRANCH:_** You can start from the branch `all-requests`.
>
> `git checkout all-requests`
Finally, we are going to add some tests.

## Add tests for fetch

We are going to use very popular test framework `pytest`.

 1. Create a test directory and test file.

```
mkdir tests
touch __init__.py
touch test_fetch.py
```

 2. Install `pytest`, `pytest-asyncio` and `pytest-httpserver`.

```
pip install pytest pytest-httpserver
```

 3. `pytest-httpserver` allows us to create a local server, which we will use in tests. To be able to use it, we
need to
modify `fetch` function.

```python
...


async def fetch(delay_seconds: float, url: str = BLOOMREACH_SERVER) -> tuple[Success, dict]:
...
```

We will pass the URL as parameter, so we can simply modify it.

> **_QUESTION:_** Do you know how this pattern is called?
{% details Click on me for the answer! %}

This pattern is called Dependency Injection, where an object or function receives other objects or functions that it
depends on.

{% enddetails %}

 4. Create fetch tests.

```python
import asyncio
import json
import time
from functools import partial

import pytest
from pytest_httpserver import HTTPServer
from werkzeug import Response

from app import fetch, get_first_successful_request


def slow_response(request, time_to_wait_seconds: float, status: int = 200) -> Response:
time.sleep(time_to_wait_seconds)
return Response(json.dumps({"time": time_to_wait_seconds}), status, content_type="application/json")


@pytest.mark.asyncio
async def test_successful_fetch(httpserver: HTTPServer):
httpserver.expect_request("/foobar").respond_with_handler(partial(slow_response, time_to_wait_seconds=0.2))

success, result = await fetch(0, httpserver.url_for("/foobar"))

assert success
assert {"time": 0.2} == result


@pytest.mark.asyncio
async def test_unsuccessful_fetch(httpserver: HTTPServer):
httpserver.expect_request("/foobar").respond_with_handler(
partial(slow_response, status=400, time_to_wait_seconds=0.2)
)

success, result = await fetch(0, httpserver.url_for("/foobar"))

assert not success
```

We created a simple handler that returns similar responses to our Bloomreach testing server. In the first test, we
expect that server returns success and correct json. In the second one, we expect that success will be `false`.

 4. Let's create now test for first successful response. First we need to do a small refactoring.

First move the code, which we would like to test, from app to a separate function.

```python
async def get_first_successful_request(unfinished_tasks: list[asyncio.Task], timeout: None | float):
timeout_remaining = timeout

while not out_of_time(timeout_remaining) and unfinished_tasks:
start = time.monotonic()
finished_tasks, unfinished_tasks = await asyncio.wait(
unfinished_tasks, return_when=asyncio.FIRST_COMPLETED, timeout=timeout_remaining
)

for finished_task in finished_tasks:
success, result = finished_task.result()
if success:
return success, result

end = time.monotonic()
timeout_remaining = timeout_remaining - (end - start) if timeout_remaining is not None else timeout_remaining

for unfinished_task in unfinished_tasks:
unfinished_task.cancel()

if out_of_time(timeout_remaining):
raise RequestTimeout()

return False, {}
```

Then update the app function, so it uses our new function.

```python
@app.get("/api/smart")
async def smart_api_requester():
timeout_seconds = get_and_validate_timeout()
unfinished_tasks = [
asyncio.create_task(fetch(delay_seconds=0)),
asyncio.create_task(fetch(delay_seconds=WAIT_BEFORE_NEXT_REQUEST_SECONDS)),
asyncio.create_task(fetch(delay_seconds=WAIT_BEFORE_NEXT_REQUEST_SECONDS)),
]

success, result = await get_first_successful_request(unfinished_tasks, timeout_seconds)

return jsonify(success=success, result=result)
```

 5. We cannot use one server as we did with Bloomreach testing server. The reason is that `pytest-httpserver`
cannot process more than one server. So, we overcome this problem by creating multiple servers. Each running in
different thread.

```python
@pytest.fixture
def httpserver1(httpserver_ssl_context, httpserver_listen_address):
server = HTTPServer(host=HTTPServer.DEFAULT_LISTEN_HOST, port=7001, ssl_context=httpserver_ssl_context)
server.start()
yield server
server.clear()
if server.is_running():
server.stop()


@pytest.fixture
def httpserver2(httpserver_ssl_context, httpserver_listen_address):
server = HTTPServer(host=HTTPServer.DEFAULT_LISTEN_HOST, port=7002, ssl_context=httpserver_ssl_context)
server.start()
yield server
server.clear()
if server.is_running():
server.stop()
```

 6. The last test that we will implement will be getting the first successful response.

```python
@pytest.mark.asyncio
async def test_first_successful_requests(httpserver: HTTPServer, httpserver1, httpserver2):
httpserver.expect_request("/foo").respond_with_handler(partial(slow_response, time_to_wait_seconds=0.8))
httpserver1.expect_request("/foo").respond_with_handler(partial(slow_response, time_to_wait_seconds=0.2))
httpserver2.expect_request("/foo").respond_with_handler(partial(slow_response, time_to_wait_seconds=0.4))

unfinished_tasks = [
asyncio.create_task(fetch(delay_seconds=0, url=httpserver.url_for("/foo"))),
asyncio.create_task(fetch(delay_seconds=0, url=httpserver1.url_for("/foo"))),
asyncio.create_task(fetch(delay_seconds=0, url=httpserver2.url_for("/foo"))),
]
success, result = await get_first_successful_request(unfinished_tasks, timeout=None)

assert success
assert {"time": 0.2} == result
```

Each http server has same route, but with different sleep time. This means that the server with the lowest sleep time
should answer first.


> **_QUESTION:_** Do you think we need more tests? If yes, what would they test?
{% details Click on me for the answer! %}

We definitely miss tests that would validate if `get_first_successful_request` ends always in specified timeout. We also
could add some tests that would validate what happens if the testing server reaches timeout or will respond with invalid
content.

{% enddetails %}

0 comments on commit d3f4fad

Please sign in to comment.