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.
Do not confuse Reahl Tofu
Fixtures with Django’s Fixtures or with
pytest’s Fixtures. They are all related, but work very
What is a class-based
Fixture is an object that contains several of related objects that are often used together
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
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)
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 the
Fixtureclasses needed by your test method. When your test method is called, each
Fixtureclass is first instantiated, then passed into the method, and finally torn down when the method exits. The number of
Fixtureclasses passed to
with_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 in
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
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()
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 can also use other
Fixtures. Decorate your
Fixture class with
uses to specify the other
Fixtures it depends on. When your
Fixture is instantiated,
Fixture it depends on is first instantiated, set up, and then set as an attribute on your
attribute is named as per your invocation of
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)
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('session') class WebServerFixture(Fixture): @set_up def start_web_server(self): ... @uses(webserver_fixture=WebServerFixture) class MyFixture(Fixture): ...