Neil
Neil

Reputation: 1036

How do you run repeated routines/functions inside a test in pytest?

I know what pytest fixtures do and how they work, but they do things before and after the test. How do you do things inside the test in a composable fashion? I need to run tests composed of various smaller functions:

@pytest.mark.django_db
def my_example_test():
    # Check if user does not exist
    assert not Users.objects.filter(email='[email protected]').exists()

    # Do a bunch of things to sign up and commit a user to the db
    sign_up_routine()

    # Check if user exists.
    assert Users.objects.filter(email='[email protected]').exists()
    
    # Checkout a shopping cart
    checkout_shopping_cart(item="toothpaste", qty=10)
    
    # do some more checks
    ...

Now, in my case, fixture doesn't work because it runs even before the test case starts. In general, I want to compose hundreds of tests like this:

  1. Run a bunch of assert statements

  2. Run a composable routine <--- how? function? fixture?

  3. Assert more conditions

  4. Run a different routine

What is a good way to structure composable tests like this in pytest? I am thinking of just writing a bunch of functions and give them database access?

I am sorry if this is just an obvious solution to run functions but I thought there was a pytest way to do this.

Upvotes: 0

Views: 945

Answers (2)

Neil
Neil

Reputation: 1036

I restructured the test with fixtures as follows. Instead of running one test with steps in a linear fashion, I read thoroughly through the fixtures documentation to end up with this:


@pytest.fixture(scope="function")
def sign_up_user(db)
    # Check if user does not exist
    assert not Users.objects.filter(email='[email protected]').exists()

    # Do a bunch of things to sign up and commit a user to the db
    # that were part of the sign_up_routine() here. Just showing an example below:

    client = Client()
    resp = client.post('url', kwargs={form:form})
    
@pytest.fixture(scope="function")
def assert_user_exists(db, sign_up_user):
    # Check if user exists. You can imagine a lot more things to assert here, just example from my original post here.
    assert Users.objects.filter(email='[email protected]').exists()


@pytest.fixture(scope="function")
def checkout_shopping_cart(db, assert_user_exists):
    # Checkout shopping cart with 10 quantity of toothpaste
    ...


def test_order_in_database(db, checkout_shopping_cart):
    # Check if order exists in the database
    assert Orders.objects.filter(order__user__email='[email protected]').exists()

    # This is the final test that calls all previous fixtures. 

    # Now, fixtures can be used to compose tests in various ways. For example, repeat 'sign_up_user' but checkout toothbrush instead of toothpaste.

I think this is pretty clean, not sure if it this is the intended way to use pytest. I welcome feedback. I can now compose smaller bits of tests that can run as fixtures by calling other fixtures in a long chain.

This is a toy example but you can imagine testing a lot of conditions in the database in each of these fixtures. Please note db fixture is needed for django-pytest package for database to work properly in fixtures. Otherwise, you'll get errors that won't be obvious ("use django_db mark" which doesn't fix the problem), see here: pytest django: unable to access db in fixture teardown

Also, the pytest.fixture scope must be function so each fixture runs again instead of caching.

More reading here: https://docs.pytest.org/en/6.2.x/fixture.html#running-multiple-assert-statements-safely

Upvotes: 1

ccchoy
ccchoy

Reputation: 832

I think the short answer you're looking for is just use plain old functions! There's nothing wrong with that. If you want these reusable chunks of code to have access to other fixtures, just pass them through on invocation.

@pytest.mark.fixture
def db_session():
    ...

def create_some_users(session):
    ...

def my_test(db_session):
    expected = ...
    create_some_users(db_session)
    actual = do_thing()
    assert actual == expected

I like to think of tests with the AAA pattern - Arrange, Act, & Assert. First we get the universe in order, then we fire our cannon off at it, and finally we check to see that everything is how we'd expect it to be at the end. It's an ideal world if all tests are kept simple like this. Fixtures are pytest's way of managing and sharing sets of resources and the instructions to arrange them in some way. This is why they always run at start and -b/c we often want to do some related disposal afterwards- at end. And a nice side-effect is you can more explicitly state the dependencies of a given test within its declaration and can move common surrounding (beginning & end) "Arrange" code so that the test is more easily parsed as "do this X and expect Y".

For what you're looking for, you'd have to have some way to tell pytest when to run your reusable thing as it could be at any midpoint within the test function and at that point, mind as well just use a normal function. For example, you could write a fixture that returns a callable(function) and then just invoke the fixture with (..), but there's not a ton of difference. You could also have fixtures return classes and encapsulate reusable logic in that way. Any of these things would work fine.

Upvotes: 1

Related Questions