1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
|
# Testing
Testing **Typer** applications is very easy with <a href="https://docs.pytest.org/en/latest/" class="external-link" target="_blank">pytest</a>.
Let's say you have an application `app/main.py` with:
{* docs_src/testing/app01/main.py *}
So, you would use it like:
<div class="termy">
```console
$ python main.py Camila --city Berlin
Hello Camila
Let's have a coffee in Berlin
```
</div>
And the directory also has an empty `app/__init__.py` file.
So, the `app` is a "Python package".
## Test the app
### Import and create a `CliRunner`
Create another file/module `app/test_main.py`.
Import `CliRunner` and create a `runner` object.
This runner is what will "invoke" or "call" your command line application.
{* docs_src/testing/app01/test_main.py hl[1,5] *}
/// tip
It's important that the name of the file starts with `test_`, that way pytest will be able to detect it and use it automatically.
///
### Call the app
Then create a function `test_app()`.
And inside of the function, use the `runner` to `invoke` the application.
The first parameter to `runner.invoke()` is a `Typer` app.
The second parameter is a `list` of `str`, with all the text you would pass in the command line, right as you would pass it:
{* docs_src/testing/app01/test_main.py hl[8,9] *}
/// tip
The name of the function has to start with `test_`, that way pytest can detect it and use it automatically.
///
### Check the result
Then, inside of the test function, add `assert` statements to ensure that everything in the result of the call is as it should be.
{* docs_src/testing/app01/test_main.py hl[10,11,12] *}
Here we are checking that the exit code is 0, as it is for programs that exit without errors.
Then we check that the text printed to "standard output" contains the text that our CLI program prints.
/// tip
You could also check the output sent to "standard error" (`stderr`) or "standard output" (`stdout`) independently by accessing `result.stdout` and `result.stderr` in your tests.
///
/// info
If you need a refresher about what is "standard output" and "standard error" check the section in [Printing and Colors: "Standard Output" and "Standard Error"](printing.md#standard-output-and-standard-error){.internal-link target=_blank}.
///
### Call `pytest`
Then you can call `pytest` in your directory and it will run your tests:
<div class="termy">
```console
$ pytest
================ test session starts ================
platform linux -- Python 3.10, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /home/user/code/superawesome-cli/app
plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1
collected 1 item
---> 100%
test_main.py <span style="color: green; white-space: pre;">. [100%]</span>
<span style="color: green;">================= 1 passed in 0.03s =================</span>
```
</div>
## Testing input
If you have a CLI with prompts, like:
{* docs_src/testing/app02_an/main.py hl[8] *}
That you would use like:
<div class="termy">
```console
$ python main.py Camila
# Email: $ camila@example.com
Hello Camila, your email is: camila@example.com
```
</div>
You can test the input typed in the terminal using `input="camila@example.com\n"`.
This is because what you type in the terminal goes to "**standard input**" and is handled by the operating system as if it was a "virtual file".
/// info
If you need a refresher about what is "standard output", "standard error", and "standard input" check the section in [Printing and Colors: "Standard Output" and "Standard Error"](printing.md#standard-output-and-standard-error){.internal-link target=_blank}.
///
When you hit the <kbd>ENTER</kbd> key after typing the email, that is just a "new line character". And in Python that is represented with `"\n"`.
So, if you use `input="camila@example.com\n"` it means: "type `camila@example.com` in the terminal, then hit the <kbd>ENTER</kbd> key":
{* docs_src/testing/app02/test_main.py hl[9] *}
## Test a function
If you have a script and you never created an explicit `typer.Typer` app, like:
{* docs_src/testing/app03/main.py hl[9] *}
...you can still test it, by creating an app during testing:
{* docs_src/testing/app03/test_main.py hl[6,7,13] *}
Of course, if you are testing that script, it's probably easier/cleaner to just create the explicit `typer.Typer` app in `main.py` instead of creating it just during the test.
But if you want to keep it that way, e.g. because it's a simple example in documentation, then you can use that trick.
### About the `app.command` decorator
Notice the `app.command()(main)`.
If it's not obvious what it's doing, continue reading...
You would normally write something like:
```Python
@app.command()
def main(name: str = "World"):
# Some code here
```
But `@app.command()` is just a decorator.
That's equivalent to:
```Python
def main(name: str = "World"):
# Some code here
decorator = app.command()
new_main = decorator(main)
main = new_main
```
`app.command()` returns a function (`decorator`) that takes another function as it's only parameter (`main`).
And by using the `@something` you normally tell Python to replace the thing below (the function `main`) with the return of the `decorator` function (`new_main`).
Now, in the specific case of **Typer**, the decorator doesn't change the original function. It registers it internally and returns it unmodified.
So, `new_main` is actually the same original `main`.
So, in the case of **Typer**, as it doesn't really modify the decorated function, that would be equivalent to:
```Python
def main(name: str = "World"):
# Some code here
decorator = app.command()
decorator(main)
```
But then we don't need to create the variable `decorator` to use it below, we can just use it directly:
```Python
def main(name: str = "World"):
# Some code here
app.command()(main)
```
...that's it. It's still probably simpler to just create the explicit `typer.Typer` in the `main.py` file 😅.
|