From 7547183e1e4e970441e4404a1a11ca1e895d4f9e Mon Sep 17 00:00:00 2001 From: Tim Connor Date: Thu, 2 Feb 2023 13:03:01 +1300 Subject: [PATCH] Feat/add unique feature arg in faker factory Since Faker 4.9.0 there has been support to generate unique values. This is really helpful when dealing with unique constraint on a field generated by factory boy. The test can be flaky if you use faker without the "unique" feature on an ORM field with an unique constraint. The usage with factory boy is simple as this: ``` class UserFactory(fatory.Factory): class Meta: model = User arrival = factory.Faker( 'date_between_dates', date_start=datetime.date(2020, 1, 1), date_end=datetime.date(2020, 5, 31), unique=True # The generated date is guaranteed to be unique inside the test execution. ) ``` The unique keyword can be passed on every faker providers. If `True` the faker object passes through the Faker Unique Proxy, making sure the generated value has not been already generated before. Note that the default unique keyword value is `False`. Co-authored-by: Arthur Hamon --- docs/changelog.rst | 1 + docs/reference.rst | 18 +++++++ factory/faker.py | 20 +++++--- tests/test_faker.py | 112 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 94fe8699..7cbb7950 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -59,6 +59,7 @@ ChangeLog - Add support for Django 3.1 - Add support for Python 3.9 + - Add support for `unique` Faker feature *Removed:* diff --git a/docs/reference.rst b/docs/reference.rst index a7e9be01..dd1b8222 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -713,6 +713,24 @@ Faker date_end=datetime.date(2020, 5, 31), ) + Since Faker 4.9.0 version (see `Faker documentation `_), + on every provider, you can specify whether to return an unique value or not: + + .. code-block:: python + + class UserFactory(fatory.Factory): + class Meta: + model = User + + arrival = factory.Faker( + 'date_between_dates', + date_start=datetime.date(2020, 1, 1), + date_end=datetime.date(2020, 5, 31), + unique=True # The generated date is guaranteed to be unique inside the test execution. + ) + + Note that an `UniquenessException` will be thrown if Faker fails to generate an unique value. + As with :class:`~factory.SubFactory`, the parameters can be any valid declaration. This does not apply to the provider name or the locale. diff --git a/factory/faker.py b/factory/faker.py index 6ed2e28c..e8db0af7 100644 --- a/factory/faker.py +++ b/factory/faker.py @@ -27,6 +27,7 @@ class Faker(declarations.BaseDeclaration): Args: provider (str): the name of the Faker field locale (str): the locale to use for the faker + unique (bool): whether generated values must be unique All other kwargs will be passed to the underlying provider (e.g ``factory.Faker('ean', length=10)`` @@ -37,14 +38,17 @@ class Faker(declarations.BaseDeclaration): """ def __init__(self, provider, **kwargs): locale = kwargs.pop('locale', None) + unique = kwargs.pop('unique', False) self.provider = provider super().__init__( locale=locale, + unique=unique, **kwargs) def evaluate(self, instance, step, extra): locale = extra.pop('locale') - subfaker = self._get_faker(locale) + unique = extra.pop('unique') + subfaker = self._get_faker(locale, unique) return subfaker.format(self.provider, **extra) _FAKER_REGISTRY = {} @@ -61,17 +65,21 @@ def override_default_locale(cls, locale): cls._DEFAULT_LOCALE = old_locale @classmethod - def _get_faker(cls, locale=None): + def _get_faker(cls, locale=None, unique=False): if locale is None: locale = cls._DEFAULT_LOCALE - if locale not in cls._FAKER_REGISTRY: + cache_key = f"{locale}_{unique}" + if cache_key not in cls._FAKER_REGISTRY: subfaker = faker.Faker(locale=locale) - cls._FAKER_REGISTRY[locale] = subfaker + if unique: + subfaker = subfaker.unique + cls._FAKER_REGISTRY[cache_key] = subfaker - return cls._FAKER_REGISTRY[locale] + return cls._FAKER_REGISTRY[cache_key] @classmethod def add_provider(cls, provider, locale=None): """Add a new Faker provider for the specified locale""" - cls._get_faker(locale).add_provider(provider) + cls._get_faker(locale, True).add_provider(provider) + cls._get_faker(locale, False).add_provider(provider) diff --git a/tests/test_faker.py b/tests/test_faker.py index d1a16da0..3ebf3876 100644 --- a/tests/test_faker.py +++ b/tests/test_faker.py @@ -2,14 +2,27 @@ import collections import datetime + import random import unittest +from unittest import mock import faker.providers +from faker.exceptions import UniquenessException import factory +class MockUniqueProxy: + + def __init__(self, expected): + self.expected = expected + self.random = random.Random() + + def format(self, provider, **kwargs): + return "unique {}".format(self.expected[provider]) + + class MockFaker: def __init__(self, expected): self.expected = expected @@ -18,6 +31,10 @@ def __init__(self, expected): def format(self, provider, **kwargs): return self.expected[provider] + @property + def unique(self): + return MockUniqueProxy(self.expected) + class AdvancedMockFaker: def __init__(self, handlers): @@ -168,3 +185,98 @@ def fake_select_date(start_date, end_date): self.assertEqual(may_4th, trip.departure) self.assertEqual(october_19th, trip.transfer) self.assertEqual(may_25th, trip.arrival) + + def test_faker_unique(self): + self._setup_mock_faker(name="John Doe", unique=True) + with mock.patch("factory.faker.faker_lib.Faker") as faker_mock: + faker_mock.return_value = MockFaker(dict(name="John Doe")) + faker_field = factory.Faker('name', unique=True) + self.assertEqual( + "unique John Doe", + faker_field.generate({'locale': None, 'unique': True}) + ) + + +class RealFakerTest(unittest.TestCase): + + def test_faker_not_unique_not_raising_exception(self): + faker_field = factory.Faker('pyint') + # Make sure that without unique we can still create duplicated faker values. + self.assertEqual(1, faker_field.generate({'locale': None, 'min_value': 1, 'max_value': 1})) + self.assertEqual(1, faker_field.generate({'locale': None, 'min_value': 1, 'max_value': 1})) + + def test_faker_unique_raising_exception(self): + faker_field = factory.Faker('pyint', min_value=1, max_value=1, unique=True) + # Make sure creating duplicated values raises an exception on the second call + # (which produces an identical value to the previous one). + self.assertEqual(1, faker_field.generate({'locale': None, 'min_value': 1, 'max_value': 1, 'unique': True})) + self.assertRaises( + UniquenessException, + faker_field.generate, + {'locale': None, 'min_value': 1, 'max_value': 1, 'unique': True} + ) + + def test_faker_shared_faker_instance(self): + class Foo: + def __init__(self, val): + self.val = val + + class Bar: + def __init__(self, val): + self.val = val + + class Factory1(factory.Factory): + val = factory.Faker('pyint', min_value=1, max_value=1, unique=True) + + class Meta: + model = Foo + + class Factory2(factory.Factory): + val = factory.Faker('pyint', min_value=1, max_value=1, unique=True) + + class Meta: + model = Bar + + f1 = Factory1.build() + f2 = Factory2.build() + self.assertEqual(f1.val, 1) + self.assertEqual(f2.val, 1) + + def test_faker_inherited_faker_instance(self): + class Foo: + def __init__(self, val): + self.val = val + + class Bar(Foo): + def __init__(self, val): + super().__init__(val) + + class Factory1(factory.Factory): + val = factory.Faker('pyint', min_value=1, max_value=1, unique=True) + + class Meta: + model = Foo + + class Factory2(Factory1): + + class Meta: + model = Bar + + Factory1.build() + with self.assertRaises(UniquenessException): + Factory2.build() + + def test_faker_clear_unique_store(self): + class Foo: + def __init__(self, val): + self.val = val + + class Factory1(factory.Factory): + val = factory.Faker('pyint', min_value=1, max_value=1, unique=True) + + class Meta: + model = Foo + + Factory1.build() + Factory1.val.clear_unique_store() + Factory1.build()