Tofu (reahl-tofu)¶
Tofu provides class-based Fixtures for use in tests. Tofu’s Fixtures 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 Fixtures 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 Fixtures 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_fixturesdecorator in which you list all theFixtureclasses needed by your test method. When your test method is called, eachFixtureclass is first instantiated, then passed into the method, and finally torn down when the method exits. The number ofFixtureclasses passed towith_fixturesshould match the number of arguments of your test function. The names of the arguments have no significance,Fixtureinstances 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 Fixtures, a Fixture can also use other Fixtures. Decorate your
Fixture class with uses to specify the other Fixtures 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 Fixtures 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):
...
