Michael Stachura
Michael Stachura

Reputation: 1370

Django Tests - User Factory with create_batch

Ok I've faced interesting problem with my tests files.

I'm using this simple code to check User's created by UserFactory

UserFactory.create_batch(4)

for u in User.objects.all():
    print(u.email)

UserFactory - this works fine

Creates 4 users in create_batch(4) process

from django.contrib.auth import get_user_model
from factory import Faker
from factory.django import DjangoModelFactory

class UserFactory(DjangoModelFactory):
    email = Faker("email")
    password = Faker(
        "password",
        length=42,
        special_chars=True,
        digits=True,
        upper_case=True,
        lower_case=True,
    )

    class Meta:
        model = get_user_model()
        django_get_or_create = ["email"]

UserFactory - this one doesn't work

Creates only 1 user in create_batch(4) process

from django.contrib.auth import get_user_model
from faker import Faker
from factory.django import DjangoModelFactory

fake = Faker()

class UserFactory(DjangoModelFactory):

email = fake.email()
password = fake.password()

class Meta:
    model = get_user_model()
    django_get_or_create = ["email"]

The only difference is the way I generate user's email. factory.Faker("email) works fine but faker.email() doesn't.

Maybe someone had the same issue?


Upvotes: 1

Views: 2348

Answers (3)

Yatt Developer
Yatt Developer

Reputation: 11

Use factory.LazyFunction(fake.name) intead of fake.name() and don't add () when you wrappe with factory.LazyFunction.

Upvotes: 1

Xelnor
Xelnor

Reputation: 3599

Your issue comes from the way Python works.

When you write the following:

class Foo(Bar):
  email = fake.email()
  name = fake.name()

What happens is, more or less:

  1. Python calls fake.email(), and receives a value (let's say [email protected])

  2. Python calls fake.name(), and receives a value (for instance "Johannes")

  3. Python builds the dict of all class-level declarations for the target class — here {"email": "[email protected]", "name": "Johannes"}

  4. This is passed to type to create the class:

    Foo = type(
      "Foo",  # Name of the class being created
      [Bar],  # List of its bases
      {"email": "[email protected]", "name": "Johannes"},  # Methods & attributes
    )
    
  5. The class is now defined, and available for the rest of the code.

In other words, the lines are executed as they would be for the following code:

fake = faker.Faker()
fake_email = fake.email()
fake_name = fake.name()

class UserFactory(DjangoModelFactory):
  email = fake_email
  name = fake_name
  ...

As you can see, the UserFactory receives the results from the calls to faker, it has no way to know how those values were generated.

This is the reason for the various declarations (factory.LazyAttribute, factory.Faker, etc): here, we pass factory-specific objects with a custom method which is called by FactoryBoy's builder code whenever a new instance needs building:

fake_email_maker = factory.Faker("email")
fake_name_maker = factory.Faker("name")

class UserFactory(DjangoModelFactory):
  email = fake_email_maker
  name = fake_name_maker
  ...

When you call UserFactory(), what happens looks like the following:

# UserFactory()
>>> fields = {}
>>> fields["name"] = UserFactory._meta.declarations...name.evaluate(...)
>>> fields["email"] = UserFactory._meta.declarations...evaluate(...)
>>> return User.objects.create(**fields)

Upvotes: 3

Oin
Oin

Reputation: 7529

The reason for getting the same password every time is that you're only getting the value of the call to fake.password(). That only gets called once, when the file is imported.

Upvotes: 0

Related Questions