jamescooke / factory_djoy

Factories for Django, creating valid model instances every time
https://factory-djoy.readthedocs.io/
MIT License
27 stars 0 forks source link

Provide standalone access to UserFactory username generation #20

Open k0nG opened 7 years ago

k0nG commented 7 years ago

When using a custom user model I'd love to still use your unique username generation.

Would it be possible to provide the Username generator as it's own standalone module/provider?

Something like:

from factory.django import DjangoModelFactory
from factory_djoy import UniqueUsername

class AccountFactory(DjangoModelFactory):

    username = UniqueUsername()
jamescooke commented 5 years ago

Had some time to think about this :joy:

This week I've wished for a unique generator for strings when working with Factory Boy. I'm not satisfied with using Sequences to guarantee unique values - especially for stringy values which are not meant to contain numbers.

I've started work on uFaker to add functionality to Faker to make it easier to generate unique values and or exclude undesired ones.

Quick recap: factory_djoy does two things when generating user names for Django Users:

So, back to the original question:

Would it be possible to provide the Username generator as it's own standalone module/provider?

Do you care about values in the database already?

If no - then would ufake.user_name() work? My plan is to wire a ufake instance into factory_djoy so that unique values can be easily generated across multiple calls to factories.

If yes - then UniqueUsername() could be made available. My main concern would be that this would not be generic enough to be helpful - for example if the model field name or database column name change.

What I'm thinking of for the "yes I care about existing values in the database" situation is a recipe that uses a ban list:

from factory_djoy import CleanModelFactory, ufake

from project.accounts import Account

class AccountFactory(CleanModelFactory):
    class Meta:
        model = Account

    @lazy_attribute
    def username(self):
        all_usernames = Account.objects.values_list('username', flat=True)
        return ufake.user_name(ban=all_usernames)

What do you think? Would the recipe be powerful enough for what you're looking for?

Either way, my plan is to build out uFaker to the API in its README and then replace the username generation in this library with the recipe above.

k0nG commented 5 years ago

I'm so glad that you got to this within 3 years, I was worried we'd go into the 4th year..... 😆

I understand your thinking and I think I do care about existing values in the database sometimes. So a ban list does not sound like a bad idea.

jamescooke commented 4 years ago

Just a note to self with an example for testing.

Given a Game model that keeps a single unique value pointing a Server FK on another system:

class Game(models.Model):
    server_id = models.IntegerField(unique=True)
    code = models.CharField(max_length=255)
    created_at = models.DateTimeField(auto_now=True)

Then we would have a factory (note the change of import name from the Username example above - this is so that the API matches Factory Boy):

from factory import Faker
from factory_djoy import CleanModelFactory, uFaker

from games.models import Game

class GameFactory(CleanModelFactory):
    class Meta:
        model = Game

    server_id = uFaker('pyint')  # This guarantees unique values for this ID
    code = Faker('word')  # We don't care about uniqueness here.

This would then mean that a large number of valid Game instances can be created without the server_id accidentally colliding and without using Factory Boy's sequence solution: GameFactory.create_batch(999).

It also means that we can easily test what happens when there are missing values without the chance of collision, but maintaining some randomness:

def test_missing():
    """
    When no games can be found, serializer is valid, but `_game` is `None`.
    """
    GameFactory.create_batch(3, server_id__ban=[29])
    serializer = GameSearchSerializer(data={'server_id': 29})

    result = serializer.is_valid()

    assert result is True, serializer.errors
    assert serializer._game is None