Tips & Tricks Tutorials

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.

image description