Skip to content

Commit

Permalink
apply feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
SalmonMode committed Nov 7, 2020
1 parent b3bc5c6 commit c01a077
Showing 1 changed file with 76 additions and 47 deletions.
123 changes: 76 additions & 47 deletions doc/en/fixture.rst
Expand Up @@ -127,26 +127,14 @@ What fixtures are
Before we dive into what fixtures are, let's first look at what a test is.

In the simplest terms, a test is meant to look at the result of a particular
"behavior", and make sure that result aligns with what you would expect.
behavior, and make sure that result aligns with what you would expect.
Behavior is not something that can be empirically measured, which is why writing
tests can be challenging.

You may have heard the expression "test behavior, not implementation". But if
behavior can't be empirically measured, how are we supposed to test it? Is it a
thing that exists? Is it a series of actions? Is it a description of what's
happening? Where does behavior even happen?

"Behavior" is the way in which some system **acts in response** to a particular
situation and/or stimuli. But exactly *how* or *why* something is done is not
quite as important as *what* was done.

.. note::

Testing implementation isn't always bad, but when it's good to test
implementation it means that behavior is being tested, just from a
different perspective. One person's "implementation" can be another person's
"behavior".

You can think of a test as being broken down into four steps:

1. **Arrange**
Expand All @@ -162,10 +150,10 @@ or even things like defining a URL to query, generating some credentials for a
user that doesn't exist yet, or just waiting for some process to finish.

**Act** is the singular, state-changing action that kicks off the **behavior**
we want to test. This behavior is what carries out the changing of the SUT's
state, and it's the resulting changed state that we can look at to make a
judgement about the behavior. This typically takes the form of a function/method
call.
we want to test. This behavior is what carries out the changing of the state of
the system under test (SUT), and it's the resulting changed state that we can
look at to make a judgement about the behavior. This typically takes the form of
a function/method call.

**Assert** is where we look at that resulting state and check if it looks how
we'd expect after the dust has settled. It's where we gather evidence to say the
Expand All @@ -180,18 +168,15 @@ At it's core, the test is ultimately the **act** and **assert** steps, with the
**arrange** step only providing the context. **Behavior** exists between **act**
and **assert**.

.. note::

For functional programming, the **act** will still be a function call, but
the **assert** will be looking at the output of that function, rather than
the resulting state. In that case fixtures are often used to produce input data for the function.

Back to fixtures
^^^^^^^^^^^^^^^^

"Fixtures", in the literal sense, are each of the **arrange** steps and data. They're
everything that test needs to do its thing.

At a basic level, test functions request fixtures by declaring them as
arguments, as in the ``test_ehlo(smtp_connection):`` in the previous example.

In pytest, "fixtures" are functions you define that serve this purpose. But they
don't have to be limited to just the **arrange** steps. They can provide the
**act** step, as well, and this can be a powerful technique for designing more
Expand All @@ -207,23 +192,46 @@ what a fixture in pytest might look like:
import pytest
class Fruit:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
@pytest.fixture
def order():
return list()
def my_fruit():
return Fruit("apple")
@pytest.fixture
def fruit_basket(my_fruit):
return [Fruit("banana"), my_fruit]
def test_my_fruit_in_basket(my_fruit, fruit_basket):
assert my_fruit in fruit_basket
Tests don't have to be limited to a single fixture, either. They can depend on
as many fixtures as you want, and fixtures can use other fixtures, as well. This
is where pytest's fixture system really shines.

Don't be afraid to break things up and have tons of fixtures. Fixtures in pytest
are meant to be modular and flexible. The more, the merrier!
Don't be afraid to break things up if it makes things cleaner.

"Requesting" fixtures
---------------------

So fixtures are how we *prepare* for a test, but how do we tell pytest what
tests and fixtures need which fixtures?

At a basic level, test functions request fixtures by declaring them as
arguments, as in the ``test_my_fruit_in_basket(my_fruit, fruit_basket):`` in the
previous example.

At a basic level, pytest depends on a test to tell it what fixtures it needs, so
we have to build that information into the test itself. We have to make the test
"**request**" the fixtures it depends on, and to do this, we have to
Expand All @@ -244,43 +252,65 @@ Quick example
import pytest
class Fruit:
def __init__(self, name):
self.name = name
self.cubed = False
def cube(self):
self.cubed = True
class FruitSalad:
def __init__(self, *fruit_bowl):
self.fruit = fruit_bowl
self._cube_fruit()
def _cube_fruit(self):
for fruit in self.fruit:
fruit.cube()
# Arrange
@pytest.fixture
def order():
return ["a"]
def fruit_bowl():
return [Fruit("apple"), Fruit("banana")]
def test_string(order):
def test_fruit_salad(fruit_bowl):
# Act
order.append("b")
fruit_salad = FruitSalad(*fruit_bowl)
# Assert
assert order == ["a", "b"]
assert all(fruit.cubed for fruit in fruit_salad.fruit)
In this example, ``test_string`` "**requests**" ``order`` (i.e.
``def test_string(order):``), and when pytest sees this, it will execute
the ``order`` fixture function and pass the object it returns into ``test_string`` as the ``order`` argument.
In this example, ``test_fruit_salad`` "**requests**" ``fruit_bowl`` (i.e.
``def test_fruit_salad(fruit_bowl):``), and when pytest sees this, it will
execute the ``fruit_bowl`` fixture function and pass the object it returns into
``test_fruit_salad`` as the ``fruit_bowl`` argument.

Here's roughly
what's happening if we were to do it by hand:

.. code-block:: python
def order():
return ["a"]
def fruit_bowl():
return [Fruit("apple"), Fruit("banana")]
def test_string(order):
def test_fruit_salad(fruit_bowl):
# Act
order.append("b")
fruit_salad = FruitSalad(*fruit_bowl)
# Assert
assert order == ["a", "b"]
assert all(fruit.cubed for fruit in fruit_salad.fruit)
# Arrange
the_list = order()
test_string(order=the_list)
bowl = fruit_bowl()
test_fruit_salad(fruit_bowl=bowl)
Fixtures can **request** other fixtures
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down Expand Up @@ -809,10 +839,9 @@ fixture/test, just like with the other fixtures. The only differences are:
1. ``return`` is swapped out for ``yield``.
2. Any teardown code for that fixture is placed *after* the ``yield``.

pytest does its best to put all the fixtures for a given test in a linear order
so that it can see which fixture happens first, second, third, and so on. Once
this is done, pytest will run each fixture up until it returns or yields, and
then move on to the next fixture in the list to do the same thing.
Once pytest figures out a linear order for the fixtures, it will run each one up
until it returns or yields, and then move on to the next fixture in the list to
do the same thing.

Once the test is finished, pytest will go back down the list of fixtures, but in
the *reverse order*, taking each one that yielded, and running the code inside
Expand Down Expand Up @@ -1014,7 +1043,7 @@ access to an admin API where we can generate users. For our test, we want to:
5. Assert that their name is in the header of the landing page

We wouldn't want to leave that user in the system, nor would we want to leave
that browser session runnning, so we'll want to make sure the fixtures that
that browser session running, so we'll want to make sure the fixtures that
create those things clean up after themselves.

Here's what that might look like:
Expand Down

0 comments on commit c01a077

Please sign in to comment.