Pithikos
Pithikos

Reputation: 20300

Pytest fixtures interfering with each other

I am using Pytest with Django and came to this weird behaviour. I have two user fixtures, one being a superset of the other. Everything works as expected until I use both fixtures in the same test case.

Fixtures:

@pytest.fixture
def user_without_password():
    return User.objects.create_user(username=fake.name(), email=fake.email())

@pytest.fixture
def user_with_password(user_without_password):
    user = user_without_password
    user.set_password('topsecret')
    user.save()
    return user

Tests

@pytest.mark.django_db()
def test_without_pass(user_without_password):
    assert not user_without_password.has_usable_password()


@pytest.mark.django_db()
def test_with_pass(user_with_password):
    assert user_with_password.has_usable_password()

# THIS FAILS!!
@pytest.mark.django_db()
def test_both(user_with_password, user_without_password):
    assert not user_without_password.has_usable_password()
    assert user_with_password.has_usable_password()

The last test doesn't work since apparently user_with_password and user_without_password end up being the same object. Is there a way to ensure that they are new objects each time? This behavior feels counter-intuitive.

Upvotes: 2

Views: 1305

Answers (1)

daphtdazz
daphtdazz

Reputation: 8159

pytest fixtures are designed to be efficient – i.e. if a fixture is requested multiple times it is only created once. That means you can always request a fixture from another fixture and be sure you're using the same object as the test.

Further, if you read your user_with_password fixture like this:

  1. Give me the fixture with the user without the password
  2. Change the user without the password to have a password

Then it makes sense that the fixture which returns the user it created without a password continues to return that user, but now it's had a password added.

If you want to get around this, then create a fixture that creates new objects rather than just a single object, something like:

@pytest.fixture
def user_without_password_factory():
    def create_user_without_password(): 
        return User.objects.create_user(username=fake.name(), email=fake.email())
    return create_user_without_password

@pytest.fixture
def user_with_password_factory():
    def create_user_with_password(): 
        user = User.objects.create_user(username=fake.name(), email=fake.email())
        user.set_password('topsecret')
        user.save()
        return user
    return create_user_with_password

@pytest.mark.django_db()
def test_without_pass(user_without_password_factory):
    assert not user_without_password_factory().has_usable_password()


@pytest.mark.django_db()
def test_with_pass(user_with_password_factory):
    assert user_with_password_factory().has_usable_password()

# Succeeds!!
@pytest.mark.django_db()
def test_both(user_with_password_factory, user_without_password_factory):
    assert not user_without_password_factory().has_usable_password()
    assert user_with_password_factory().has_usable_password()

Upvotes: 3

Related Questions