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:

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

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:

Python

1

2

3

4

5

6

7

8

9

10

@dataclass

classPlayer:

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:

Python

1

2

3

4

5

deftest_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:

Python

1

2

3

4

@pytest.fixture

defplayer_one()->Player:

""" Return a sample player Mary Smith #10 """

yieldPlayer(first_name='Mary',last_name='Smith',jersey=10)

Now it’s a lot easier to test construction, along with anything else on the Player:

Python

1

2

deftest_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:

Python

1

2

3

4

@pytest.fixture(name='mary')

defplayer_one()->Player:

""" Return a sample player Mary Smith #10 """

yieldPlayer(first_name='Mary',last_name='Smith',jersey=10)

The test would then ask for mary instead of player_one:

Python

1

2

deftest_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:

Python

1

2

deftest_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:

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

@dataclass

classGame:

home_team:Team

visitor_team:Team

home_score:Optional[int]=None

visitor_score:Optional[int]=None

defrecord_score(self,home_score:int,visitor_score:int):

self.home_score=home_score

self.visitor_score=visitor_score

@property

defwinner(self)->Union[Team,None]:

ifself.home_score>self.visitor_score:

returnself.home_team

elifself.home_score<self.visitor_score:

returnself.visitor_team

else:

returnNone

Rather than write one test to see if home wins and another to see if visitor wins, let’s write one test using parametrize: