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.

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:

object "fixture: MyFixture" as fixture
object "shopping_cart: ShoppingCart" as shopping_cart
object "credit_card: CreditCard" as credit_card
object "user: User" as user
fixture -- shopping_cart
fixture -- credit_card
fixture -- user
credit_card -right- 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_fixtures decorator in which you list all the Fixture classes needed by your test method. When your test method is called, each Fixture class is first instantiated, then passed into the method, and finally torn down when the method exits. The number of Fixture classes passed to with_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 in with_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):
     ...