Tofu (reahl-tofu)¶
Tofu provides class-based Fixture
s for use in tests. Tofu’s Fixture
s can be used on
their own with any test framework, but Tofu also provides an extension to pytest
that makes using it with pytest seamless.
See also
Note
Do not confuse Reahl Tofu Fixture
s with Django’s Fixtures or with
pytest’s Fixtures. They are all related, but work very
differently.
What is a class-based Fixture
?¶
A class-based Fixture
is an object that contains several of related objects that are often used together
in tests.
For example, a shopping cart application may have a lot of tests in which a ShoppingCart, a User and a CreditCard are routinely used. What’s more, the CreditCard used in the tests should be linked to the User:
Given the example above, the following assumptions can be made inside tests:
assert fixture.user is fixture.credit_card.owner
assert fixture.shopping_cart.payment_method is fixture.credit_card
A Fixture
is written as a class, hence it can also contain useful methods re-used by several tests:
assert fixture.last_payment_is(fixure.credit_card, 145.42)
How are Fixture
s used?¶
A Fixture
can be used in many ways:
Inside a with statement
with MyFixture() as fixture: ...
Used outside of a test framework, using a Fixture inside a with statement ensures that its set up and tear down logic is executed.
- Supplied to a pytest test function
@with_fixtures(MyFixture) def test_things(fixture): ...
When using pytest, decorate your test method using a
with_fixtures
decorator in which you list all theFixture
classes needed by your test method. When your test method is called, eachFixture
class is first instantiated, then passed into the method, and finally torn down when the method exits. The number ofFixture
classes passed towith_fixtures
should match the number of arguments of your test function. The names of the arguments have no significance,Fixture
instances are passed as attributes in the order listed inwith_fixtures
.
Writing a Fixture
¶
To write a Fixture
, inherit a new class from Fixture
. For each element of the Fixture
, add a method that
creates the element. Prepend new_ to the method name to signal that it is a factory method.
class MyFixture(Fixture):
def new_user(self):
return User(name='sam')
Whenever an attribute is accessed on the Fixture
, it checks whether a new_-method exists for that name. If so,
it invokes the method to create the object in question. Subsequent accesses just return the first object so created:
@with_fixtures(MyFixture)
def test_fixture_attributes(f):
assert f.user is f.user # The first use of .user calls new_user(), the next one just returns the first object
assert user.name == 'sam'
The ability of a Fixture
to create an object on first access can greatly simplify a setup where several objects
on the Fixture
depend on one another:
class MyFixture(Fixture):
def new_user(self):
return User(name='sam')
def new_credit_card(self):
return CreditCard('123456224', self.user)
@with_fixtures(MyFixture)
def test_interrelated_setup(f):
assert f.credit_card.owner is f.user # User is first instantiated when the Fixture calls .user on itself,
# yet, the same .user is returned when accessed again directly on the
# Fixture in a test.
Set-up and tear down logic¶
If your factory method needs to set up or tear down the object it creates, it can yield the object and perform tear down after the yield:
class MyFixture(Fixture):
def new_shopping_cart(self):
print('Setting up')
cart = ChoppingCart()
yield cart
print('Tearing down')
You can also explicitly mark certain methods on your Fixture
to be executed on set up or tear down:
class MyFixture(Fixture):
@set_up
def start_cart_server(self):
WebServer.start()
@tear_down
def stop_cart_server(self):
WebServer.stop()
Scenarios¶
To run the same test for multiple scenarios, create a no-argument method decorated with scenario
for each scenario
which sets up the data relevant to that scenario:
class MyFixture(Fixture):
@scenario
def out_of_stock(self):
self.stock_room.set_items(0)
self.expected_exception = OutOfStock
@scenario
def insufficient_funds(self):
self.credit_card.set_balance(0)
self.expected_exception = InSufficientFunds
@with_fixtures(MyFixture)
def test_purchase_failure(f):
try:
f.shopping_cart.checkout()
assert None, 'Expected an exception to be raised, but did not get one'
except f.expected_exception:
pass
The above test will be executed twice. THe first time, a MyFixture is instantiated and set up, then its out_of_stock scenario method is called before it is passed to the test method. On the second run, a new MyFixture is created, set up, and its insufficient_funds method is executed before being passed to the test method.
Interdependencies between Fixtures¶
Just like a test method can use one or more Fixture
s, a Fixture
can also use other Fixture
s. Decorate your
Fixture
class with uses
to specify the other Fixture
s it depends on. When your Fixture
is instantiated,
each Fixture
it depends on is first instantiated, set up, and then set as an attribute on your Fixture
. The
attribute is named as per your invocation of uses
:
class RoleFixture(Fixture):
def new_shopper_role(self):
return Role('shopper')
@uses(access_control_fixture=RoleFixture)
class MyFixture(Fixture):
def new_user(self):
return User('sam', roles=self.access_control_fixture.shopper_role)
Scope¶
Most Fixture
s live only for the duration of a single test. To deal with resources that are expensive to set up and
tear down, you can make a Fixture
be set up only once, and be torn down only after all tests have run. Decorate the
Fixture
with the scope
decorator:
@scope('session')
class WebServerFixture(Fixture):
@set_up
def start_web_server(self):
...
@uses(webserver_fixture=WebServerFixture)
class MyFixture(Fixture):
...