File: testing.md

package info (click to toggle)
typer 0.19.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,688 kB
  • sloc: python: 16,702; javascript: 280; sh: 28; makefile: 27
file content (214 lines) | stat: -rw-r--r-- 6,003 bytes parent folder | download
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 😅.