PyCharm 2018.2 and pytest Fixtures
Python has long had a culture of testing and pytest has emerged as the clear favorite for testing frameworks. PyCharm has long had very good “visual testing” features, including good support for pytest. One place we were weak: pytest “fixtures”, a wonderful feature that streamlines test setup. PyCharm 2018.2 put a lot of work and emphasis towards making pytest fixtures a pleasure to work with, as shown in the What’s New video.
This tutorial walks you through the pytest fixture support added to PyCharm 2018.2. Except for “visual coverage”, PyCharm Community and Professional Editions share all the same pytest features. We’ll use Community Edition for this tutorial and demonstrate:
- Autocomplete fixtures from various sources
- Quick documentation and navigation to fixtures
- Renaming a fixture from either the definition or a usage
- Support for pytest’s parametrize
Want the finished code? It’s in available in a GitHub repo.
What are pytest Fixtures?
In pytest you write your tests as functions (or methods.) When writing a lot of tests, you frequently have the same boilerplate over and over as you setup data. Fixtures let you move that out of your test, into a callable which returns what you need.
Sounds simple enough, but pytest adds a bunch of facilities tailored to the kinds of things you run into when writing a big pile of tests:
- Simply put the name of the fixture in your test function’s arguments and pytest will find it and pass it in
- Fixtures can be located from various places: local file, a conftest.py in the current (or any parent) directory, any imported code that has a @pytest.fixture decorator, and pytest built-in fixtures
- Fixtures can do a return or a yield, the latter leading to useful teardown-like patterns
- You can speed up your tests by flagging how often a fixture should be computed
- Interesting ways to parameterize fixtures for reuse
Read the documentation on fixtures and be dazzled by how many itches they’ve scratched over the decade of development, plus the vast ecosystem of pytest plugins.
Tutorial Scenario
This tutorial needs a sample application, which you can browse from the sample repo. It’s a tiny application for managing a girl’s lacrosse league: Players, Teams, Games. Tiny means tiny: it only has enough to illustrate pytest fixtures. No database, no UI. But it includes those things needed for actual business policies that make sense for testing.
Specifically: add a Player to a Team, create a Game between a Home Team and Visitor team, record the score, and show who won (or tie.) Surprisingly, it’s enough to exercise some of the pytest features (and was actually written with test-driven development.)
Setup
To follow along at home, make sure you have Python 3.7 (it uses dataclasses) and Pipenv installed. Clone the repo at https://github.com/pauleveritt/laxleague and make sure Pipenv has created an interpreter for you. (You can do both of those steps from within PyCharm.)
Then, open the directory in PyCharm and make sure you have set the Python Integrated Tools -> Default Test Runner
to pytest.
Now you’re ready to follow the material below. Right-click on the tests directory and choose Run ‘pytest in tests’
. If all the tests pass correctly, you’re setup.
Using Fixtures
Before getting to PyCharm 2018.2’s (frankly, awesome) fixture support, let’s get you situated with the code and existing tests.
We want to test a player, which is implemented as a Python 3.7 dataclass:
@dataclass class Player: first_name: str last_name: str jersey: int def __post_init__(self): ln = self.last_name.lower() fn = self.first_name.lower() self.id = f'{ln}-{fn}-{self.jersey}'
It’s simple enough to write a test to see if the id was constructed correctly:
def test_constructor(): p = Player(first_name='Jane', last_name='Jones', jersey=11) assert 'Jane' == p.first_name assert 'Jones' == p.last_name assert 'jones-jane-11' == p.id
But we might write lots of tests with a sample player. Let’s make a pytest fixture with a sample player:
@pytest.fixture def player_one() -> Player: """ Return a sample player Mary Smith #10 """ yield Player(first_name='Mary', last_name='Smith', jersey=10)
Now it’s a lot easier to test construction, along with anything else on the Player:
def test_player(player_one): assert 'Mary' == player_one.first_name
We can get more re-use by moving this fixture to a conftest.py file in that test’s directory, or a parent directory. And along the way, we could give that player’s fixture a friendlier name:
@pytest.fixture(name='mary') def player_one() -> Player: """ Return a sample player Mary Smith #10 """ yield Player(first_name='Mary', last_name='Smith', jersey=10)
The test would then ask for mary instead of player_one:
def test_player(mary): assert 'Mary' == mary.first_name
We’ll go back to the simple form of player_one, without name=’mary’, for the rest of this tutorial.
Autocompletion
When you really get into the testing flow, you’ll be cranking out tests. The boilerplate becomes a chore. It would be nice if your tool both helped you specify fixtures and visually warn you when you did something wrong.
First, make sure you configure PyCharm to use pytest as its test framework:
When writing the test_player
test above, player_one
is a function argument. It also happens to be a pytest fixture. PyCharm can autocomplete on pytest test functions and provide known fixture names. So as you start to type pla
, PyCharm offers to autocomplete:
PyCharm is smart about this list, because it isn’t an editor just matching strings. Ever get bugged by the “Indexing….” time of PyCharm? Well, here’s your payback. PyCharm knows what are valid symbols inside that pytest-flavored function. For example, in the code block of the test function, type di
and see that PyCharm autocompletes on dir
, the Python built-in function:
Now put the cursor in the test function’s arguments and type di
. You get a different, context-sensitive list. One without dir
, but with names of known fixtures — in this case, built-into pytest:
So there you go, next time someone says PyCharm is “heavy”, just think also of the word “productive”. PyCharm works hard to semantically discover what should go in that autocomplete, from across your code and your dependencies.
What’s That Fixture?
Big test code bases mean lots of fixtures in lots of places. You might not know where Mary put that fixture. Hell, in a month, you won’t know where you put that fixture. PyCharm has several ways to help without pushing you out of your test flow.
For example, you see a test:
def test_another_player(player_one): assert 'Mary' == player_one.first_name
You’re not sure what is player_one
. Put your cursor on it and press F1
(Quick Info). PyCharm gives you an inline popup with:
- The path to the file, as a clickable link
- The function definition
- The function return value, as a clickable link
- The docstring…rendered as HTML, for goodness sake
And it does all this without interrupting your flow. You didn’t have to hunt for the file, or do a “find” operation high up in your project with tons of false positives. You didn’t have a dialog to dismiss. You got the answer, no muss no fuss. It’s this commitment to “flow” that distinguishes PyCharm for serious development.
If you want to just jump to that symbol, put your cursor on player_one
and hit Cmd-B
(Ctrl+B
on Windows & Linux). PyCharm will open that file with the cursor on the definition, even if the argument referenced a named fixture.
Refactor -> Rename
As you refactor code, you refactor tests. And as you refactor tests, you refactor fixtures. Over, and over, and over….
PyCharm does some of the janitorial work for you. For example, renaming a fixture. In your test function, put your cursor on the player_one
argument and hit Ctrl-T
(Ctrl+Alt+Shift+T
on Windows & Linux), then choose Rename
and provide player_uno
as the new name. PyCharm confirms with you all the places in your code where it found that symbol (not that string, though it can do that too), letting you confirm with the Do Refactor
button.
This action found the definition and all the usages, starting from a usage. You could also start in conftest.py
with the definition and refactor rename from there.
“Wait, player_uno
is wrong, put it back to player_one
!” you might say. Piece of cake. The IDE treated all of that as one transaction, so a single Undo
reverts the renaming.
Note: Refactor Rename doesn’t work on fixtures defined with the name parameter
Using Parametrize
Let’s look at another piece of pytest machinery: parametrize
, that oddly-named pytest.mark
function. It’s useful when you want to run the same test, with different inputs and expected results.
For example, our lacrosse league application tells us which team won a game, or None
if there was a tie:
@dataclass class Game: home_team: Team visitor_team: Team home_score: Optional[int] = None visitor_score: Optional[int] = None def record_score(self, home_score: int, visitor_score: int): self.home_score = home_score self.visitor_score = visitor_score @property def winner(self) -> Union[Team, None]: if self.home_score > self.visitor_score: return self.home_team elif self.home_score < self.visitor_score: return self.visitor_team else: return None
Rather than write one test to see if home wins and another to see if visitor wins, let’s write one test using parametrize
:
@pytest.mark.parametrize( 'home, visitor, winner', [ (10, 5, 'green'), (5, 10, 'blue'), ] ) def test_winner(game_blue_at_green, home, visitor, winner): game_blue_at_green.record_score(home_score=home, visitor_score=visitor) assert winner == game_blue_at_green.winner.name
PyCharm has support for parametrize
in several ways.
Foremost, PyCharm now autocompletes on code>@pytest.mark</code itself, as well as the mark values such as parametrize
.
Next, if you omit one of the parameters, PyCharm warns you:
Then, when you go to fill in the parameter arguments, PyCharm autocompletes on the remaining unused parameters:
Finally, PyCharm correctly infers the type of the parameter, which can then help typing in the test function. Below we see the popup from Cmd-Hover
(meaning, hover over a symbol and press Cmd
):
Conclusion
Getting into the test flow is important and productive. PyCharm’s support for pytest has long been a help for that and the fixture-related additions in 2018.2 are very useful improvements.
For more on pytest in general, see our friend Brian Okken’s Python Testing with pytest book (I’ve read it multiple times) and Test and Code podcast. The PyCharm help has a good page on pytest (including the new fixtures and pytest-bdd support.)
If you have any questions on this tutorial, feel free to file them as a comment on this blog post or in the the repo’s Issues.