Skip to content

Instantly share code, notes, and snippets.

@imankulov
Created March 11, 2013 14:54
Show Gist options
  • Save imankulov/5134786 to your computer and use it in GitHub Desktop.
Save imankulov/5134786 to your computer and use it in GitHub Desktop.
py.test fixture creation problem.
# -*- coding: utf-8 -*-
# This is how I'd implement this using only bare pytest functionality
import pytest
from user import add_transaction, delete_transaction, get_user_balance
# Attempt 1.
# According to the documentation at http://pytest.org/latest/fixture.html
# try to start with a simple fixture
@pytest.fixture
def transaction(request):
transaction_id = add_transaction(user_id=1, amount=100)
request.addfinalizer(lambda: delete_transaction(transaction_id))
return transaction_id
# But how to pass extra parameters to the fixture?
# It's a stalemate
# Just keep this function here for the record
# Attempt 2.
# Make it smarter and create a gen function taking care of everything
@pytest.fixture
def transaction_gen(request):
created_ids = []
def gen(user_id=1, amount=100):
created_ids.append(add_transaction(amount=amount, user_id=user_id))
def destroy():
for id in created_ids:
delete_transaction(id)
request.addfinalizer(destroy)
return gen
def test_negative_transactions_are_taken_into_account__with_gen(transaction_gen):
transaction_gen(amount=100)
transaction_gen(amount=-100)
assert get_user_balance(1) == 0
def test_others_transactions_arent_taken_into_account__with_gen(transaction_gen):
transaction_gen(amount=100, user_id=1)
transaction_gen(amount=100, user_id=2)
assert get_user_balance(1) == 100
# It works, but it looks to me more like a hack than a ready-to-use solution :-(
# Attempt 3.
# I've been forced to return a function from a function and also use another
# function to clean up everything. To make it more explicit I could probably
# extract these two functions to a separate manager class
class TransactionMgr(object):
def __init__(self):
self.created_ids = []
def gen(self, user_id=1, amount=100):
self.created_ids.append(add_transaction(amount=amount, user_id=user_id))
def destroy(self):
for id in self.created_ids:
delete_transaction(id)
@pytest.fixture
def transaction_mgr(request):
mgr = TransactionMgr()
request.addfinalizer(mgr.destroy)
return mgr
def test_negative_transactions_are_taken_into_account__with_mgr(transaction_mgr):
transaction_mgr.gen(amount=100)
transaction_mgr.gen(amount=-100)
assert get_user_balance(1) == 0
def test_others_transactions_arent_taken_into_account__with_mgr(transaction_mgr):
transaction_mgr.gen(amount=100, user_id=1)
transaction_mgr.gen(amount=100, user_id=2)
assert get_user_balance(1) == 100
# Looks better, but anyway, I was forced to create the whole class managing my
# fixtures.
# It turned out that as long as I want to use only one instance of the
# fixture, I'm good with simple @pytest.fixture, but as soon as I need to manage
# more than one fixture instance in a test, I'm forced to create a boilerplate
# class, and this approach is neither obvious nor documented.
# Attempt 4.
# The problem could be solved easier ... Maybe I missed something obvious from
# the pytest documentation
# -*- coding: utf-8 -*-
#
# This is how it can be implemented with resources library
#
from user import add_transaction, delete_transaction, get_user_balance
from resources import resources
# Usually this code should live in a separate "resources_smth.py" file
@resources.register_func
def transaction(user_id=1, amount=100):
transaction_id = add_transaction(user_id, amount)
try:
yield transaction_id
finally:
delete_transaction(transaction_id)
# this is the test itself
resources.register_mod(__name__)
def test_negative_transactions_are_taken_into_account():
with resources.transaction_ctx(amount=100), \
resources.transaction_ctx(amount=-100):
assert get_user_balance(1) == 0
def test_others_transactions_arent_taken_into_account():
with resources.transaction_ctx(user_id=1), \
resources.transaction_ctx(user_id=2):
assert get_user_balance(1) == 100
# -*- coding: utf-8 -*-
# Let's pretend it's a database containing user payment transactions
#
# We can add a new transaction, remove transaction by id, and get the
# user balance
#
# We want to make sure that several transactions add up correctly (including
# transactions with negative amount), and that our codebase counts user balance
# correctly for each user, even in presence of transactions for other users.
db = []
def add_transaction(user_id, amount):
global db
if db:
id = max(t['id'] for t in db) + 1
else:
id = 1
db.append({'id': id, 'user_id': user_id, 'amount': amount})
return id
def delete_transaction(id):
global db
db = [t for t in db if t['id'] != id]
def get_user_balance(user_id):
global db
return sum(t['amount'] for t in db if t['user_id'] == user_id)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment