|
| 1 | +--- |
| 2 | +order: 9 |
| 3 | +--- |
| 4 | + |
| 5 | +# Testing with taskiq |
| 6 | + |
| 7 | +Everytime we write programs, we want them to be correct. To achieve this, we use tests. |
| 8 | +Taskiq allows you to write tests easily as if tasks were normal functions. |
| 9 | + |
| 10 | +Let's dive into examples. |
| 11 | + |
| 12 | +## Preparations |
| 13 | + |
| 14 | +### Environment setup |
| 15 | +For testing you maybe don't want to use actual distributed broker. But still you want to validate your logic. |
| 16 | +Since python is an interpreted language, you can easily replace you broker with another one if the expression is correct. |
| 17 | + |
| 18 | +We can set an environment variable, that indicates that currently we're running in testing environment. |
| 19 | + |
| 20 | +::: tabs |
| 21 | + |
| 22 | +@tab linux|macos |
| 23 | + |
| 24 | + |
| 25 | +```bash |
| 26 | +export ENVIRONMENT="pytest" |
| 27 | +pytest -vv |
| 28 | +``` |
| 29 | + |
| 30 | +@tab windows |
| 31 | + |
| 32 | +```powershell |
| 33 | +$env:ENVIRONMENT = 'pytest' |
| 34 | +pytest -vv |
| 35 | +``` |
| 36 | + |
| 37 | +::: |
| 38 | + |
| 39 | + |
| 40 | +Or we can even tell pytest to set this environment for us, just before executing tests using [pytest-env](https://pypi.org/project/pytest-env/) plugin. |
| 41 | + |
| 42 | +::: tabs |
| 43 | + |
| 44 | +@tab pytest.ini |
| 45 | + |
| 46 | +```ini |
| 47 | +[pytest] |
| 48 | +env = |
| 49 | + ENVIRONMENT=pytest |
| 50 | +``` |
| 51 | + |
| 52 | +@tab pyproject.toml |
| 53 | + |
| 54 | +```toml |
| 55 | +[tool.pytest.ini_options] |
| 56 | +env = [ |
| 57 | + "ENVIRONMENT=pytest", |
| 58 | +] |
| 59 | +``` |
| 60 | + |
| 61 | +::: |
| 62 | + |
| 63 | +### Async tests |
| 64 | + |
| 65 | +Since taskiq is fully async, we suggest using [anyio](https://anyio.readthedocs.io/en/stable/testing.html) to run async functions in pytest. Install the [lib](https://pypi.org/project/anyio/) and place this fixture somewhere in your root `conftest.py` file. |
| 66 | + |
| 67 | +```python |
| 68 | +@pytest.fixture |
| 69 | +def anyio_backend(): |
| 70 | + return 'asyncio' |
| 71 | +``` |
| 72 | + |
| 73 | +After the preparations are done, we need to modify the broker's file in your project. |
| 74 | + |
| 75 | +@[code python](../examples/testing/main_file.py) |
| 76 | + |
| 77 | +As you can see, we added an `if` statement. If the expression is true, we replace our broker with an imemory broker. |
| 78 | +The main point here is to not have an actual connection during testing. It's useful because inmemory broker has |
| 79 | +the same interface as a real broker, but it doesn't send tasks acutally. |
| 80 | + |
| 81 | +## Testing tasks |
| 82 | + |
| 83 | +Let's define a task. |
| 84 | + |
| 85 | +```python |
| 86 | +from your_project.taskiq import broker |
| 87 | + |
| 88 | +@broker.task |
| 89 | +async def parse_int(val: str) -> int: |
| 90 | + return int(val) |
| 91 | +``` |
| 92 | + |
| 93 | +This simple task may be defined anywhere in your project. If you want to test it, |
| 94 | +just import it and call as a normal function. |
| 95 | + |
| 96 | +```python |
| 97 | +import pytest |
| 98 | +from your_project.tasks import parse_int |
| 99 | + |
| 100 | +@pytest.mark.anyio |
| 101 | +async def test_task(): |
| 102 | + assert await parse_int("11") == 11 |
| 103 | +``` |
| 104 | + |
| 105 | +And that's it. Test should pass. |
| 106 | + |
| 107 | +What if you want to test a function that uses task. Let's define such function. |
| 108 | + |
| 109 | +```python |
| 110 | +from your_project.taskiq import broker |
| 111 | + |
| 112 | +@broker.task |
| 113 | +async def parse_int(val: str) -> int: |
| 114 | + return int(val) |
| 115 | + |
| 116 | + |
| 117 | +async def parse_and_add_one(val: str) -> int: |
| 118 | + task = await parse_int.kiq(val) |
| 119 | + result = await task.wait_result() |
| 120 | + return result.return_value + 1 |
| 121 | +``` |
| 122 | + |
| 123 | +And since we replaced our broker with `InMemoryBroker`, we can just call it. |
| 124 | +It would work as you expect and tests should pass. |
| 125 | + |
| 126 | +```python |
| 127 | +@pytest.mark.anyio |
| 128 | +async def test_add_one(): |
| 129 | + assert await parse_and_add_one("11") == 12 |
| 130 | +``` |
| 131 | + |
| 132 | +## Dependency injection |
| 133 | + |
| 134 | +If you use dependencies in your tasks, you may think that this can become a problem. But it's not. |
| 135 | +Here's what we came up with. We added a method called `add_dependency_context` to the broker. |
| 136 | +It sets base dependencies for dependency resolution. You can use it for tests. |
| 137 | + |
| 138 | +Let's add a task that depends on `Path`. I guess this example is not meant to be used in production code bases, but it's suitable for illustration purposes. |
| 139 | + |
| 140 | +```python |
| 141 | +from pathlib import Path |
| 142 | +from taskiq import TaskiqDepends |
| 143 | + |
| 144 | +from your_project.taskiq import broker |
| 145 | + |
| 146 | + |
| 147 | +@broker.task |
| 148 | +async def modify_path(some_path: Path = TaskiqDepends()): |
| 149 | + return some_path.parent / "taskiq.py" |
| 150 | + |
| 151 | +``` |
| 152 | + |
| 153 | +To test the task itself, it's not different to the example without dependencies, but we jsut need to pass all |
| 154 | +expected dependencies manually as function's arguments or key-word arguments. |
| 155 | + |
| 156 | +```python |
| 157 | +import pytest |
| 158 | +from your_project.taskiq import broker |
| 159 | + |
| 160 | +from pathlib import Path |
| 161 | + |
| 162 | +@pytest.mark.anyio |
| 163 | +async def test_modify_path(): |
| 164 | + modified = await modify_path(Path.cwd()) |
| 165 | + assert str(modified).endswith("taskiq.py") |
| 166 | + |
| 167 | +``` |
| 168 | + |
| 169 | +But what if we want to test task execution? Well, you don't need to provide dependencies manually, you |
| 170 | +must mutate dependency_context before calling a task. We suggest to do it in fixtures. |
| 171 | + |
| 172 | +```python |
| 173 | +import pytest |
| 174 | +from your_project.taskiq import broker |
| 175 | +from pathlib import Path |
| 176 | + |
| 177 | + |
| 178 | +# We use autouse, so this fixture |
| 179 | +# is called automatically before all tests. |
| 180 | +@pytest.fixture(scope="function", autouse=True) |
| 181 | +async def init_taskiq_dependencies(): |
| 182 | + # Here we use Path, but you can use other |
| 183 | + # pytest fixtures here. E.G. FastAPI app. |
| 184 | + broker.add_dependency_context({Path: Path.cwd()}) |
| 185 | + |
| 186 | + yield |
| 187 | + |
| 188 | + # After the test we clear all custom dependencies. |
| 189 | + broker.custom_dependency_context = {} |
| 190 | + |
| 191 | +``` |
| 192 | + |
| 193 | +This fixture will update dependency context for our broker before |
| 194 | +every test. Now tasks with dependencies can be used. Let's try it out. |
| 195 | + |
| 196 | +```python |
| 197 | +@pytest.mark.anyio |
| 198 | +async def test_modify_path(): |
| 199 | + task = await modify_path.kiq() |
| 200 | + result = await task.wait_result() |
| 201 | + assert str(result.return_value).endswith("taskiq.py") |
| 202 | + |
| 203 | +``` |
| 204 | + |
| 205 | +This should pass. And that's it for now. |
0 commit comments