FactoryBoy / factory_boy

A test fixtures replacement for Python
https://factoryboy.readthedocs.io/
MIT License
3.48k stars 392 forks source link

Sequence not working as expected #284

Open jcquarto opened 8 years ago

jcquarto commented 8 years ago

Is there a way to understand what conditions would cause Sequence not to work as expected? I tried this in a test: factories.py

class Event(mongoengine.Document):
    name = mongoengine.StringField(required=True)
class EventFactory(factory.mongoengine.MongoEngineFactory):
    class Meta:
        model = Event
    name = factory.Sequence(lambda n: 'event_%d' % n)

testcases_spec.py:

        it('should correctly generate sequences via Factory'):
            e0 = factory.build(dict, FACTORY_CLASS=EventFactory)
            e1 = factory.build(dict, FACTORY_CLASS=EventFactory)

            (e0['name']).should.equal('event_0')
            (e1['name']).should.equal('event_1')

the first 'should' passes (name is 'event_0', as expected), yet the second one fails (name is 'event_0', rather than the expected 'event_1')

any pointers? (python 2.7, factory_boy 2.6.x, uptodate MongoDB, though as you can see from the above that's not even touched in the test.

rbarrois commented 8 years ago

You should be using EventFactory.build(). When you call factory.build(dict, FACTORY_CLASS=EventFactory) twice, this is equivalent to doing:

class E0Factory(EventFactory):
    class Meta:
        model = dict
e0 = E0Factory.build()

class E1Factory(EventFactory):
    class Meta:
        model = dict
e1 = E1Factory.build()

Thus, both instances are actually built from different factory classes, each having its sequence starting back to zero.

jcquarto commented 8 years ago

Then I guess I'm having confusion understanding how to use both the MongoEngine section of https://factoryboy.readthedocs.org/en/latest/orms.html combined with the converting of a factory's output to a dict of http://factoryboy.readthedocs.org/en/latest/recipes.html

If I follow precisely what you wrote, then my code (from above) looks like: testcases_spec.py:

it('should correctly generate sequences via Factory'):
e0 = EventFactory.build()
e1 = EventFactory.build()

which results in: Failure/Error: spec/pychronicle_spec.py e0 = EventFactory.build() KeyError: '_cls'

(sounds like an error related to setting the Sequence?)

SO I understand when you describe the WHY behind different factory classes, each having its own sequence. But I'm not understanding HOW you're intending we should implement this to both build, and then later, convert to a dict

jcquarto commented 8 years ago

Continuing to try to isolate the problem. The following works, as expected and as documented:

class Cat:
    def __init__(self, eyes='green', toys=0):
        self._eyes = eyes
        self._toys = toys

    @property
    def eyes(self):
        return self._eyes

    @eyes.setter
    def eyes(self, e):
        self._eyes = e

    @property
    def toys(self):
        return self._toys

    @toys.setter
    def toys(self, t):
        self._toys = t

class CatFactory(factory.Factory):
    class Meta:
        model = Cat

    eyes = "blue"
    toys = factory.Sequence(lambda n: n)

thus:

from factories import *
cato = CatFactory()
print cato.eyes, cato.toys

gives blue 0

and

cato_green = CatFactory(eyes = 'green')
print cato_green.eyes, cato_green.toys

gives green 1

but when I use the format the docs suggest for working with ORM , it does not:

class Event(mongoengine.Document):
    name = mongoengine.StringField(required=True)
    event_id = mongoengine.StringField(required=True)

class EventFactory(factory.mongoengine.MongoEngineFactory):
    class Meta:
        model = Event

    name = factory.Sequence(lambda n: 'event_%d' % n)
    event_id = factory.Faker('uuid4')

and this:

event_0 = EventFactory()
print event_0.name

yields the error: KeyError: '_cls'

jeffwidman commented 8 years ago

I'm reopening, since @jcquarto is actively looking into this. I don't use the MongoEngine stuff myself, so unclear if this is a code bug, a docs bug, or works-as-intended. I suspect a docs bug myself, as the recipe for converting to a dict only recently got added.

@jcquarto - if you figure out what's going on, we'd really appreciate if you could submit a PR.

jcquarto commented 8 years ago

I'm currently at a loss. The answer that @rbarrois gave looked correct as per the docs, but doesn't work when I code it that way. I was hoping the error message mentioned above ("KeyError: '_cls' ") would spark some insight .

When I use the other technique from the documentation: e1 = factory.build(dict, FACTORY_CLASS=EventFactory) it at least the constructor works, although Sequence doesn't sequence

jcquarto commented 8 years ago

when I don't use mongoengine, the factory works as expected. however, dictdoes not

My solution is not to use mongoengine anymore, and stead use a snippet I found on StackOverflow for converting an object instance to a dict:

import inspect

def props(obj):
    pr = {}
    for name in dir(obj):
        value = getattr(obj, name)
        if not name.startswith('__') and not inspect.ismethod(value):
            pr[name] = value
    return pr

thus now I do:

e0 = EventFactory.build()
e1 = EventFactory.build()

which gets me the build() as per @rbarrois and then I do

q0 = props(e0)
q1 = props(e1)

to convert them into dicts. This bypasses the not-working-with-mongoengine approach of

q0 = factory.build(dict, FACTORY_CLASS=EventFactory)
q1 = factory.build(dict, FACTORY_CLASS=EventFactory)

I hope that's a useful enough of a hint that any readers can either use it or track down where in the code dict doesn't play nice with mongoengine

rbarrois commented 8 years ago

@jcquarto in your stack trace, where does the error appear (with _cls)? This could be a bug in factory_boy, so I'd like to fix it.

If you have a working factory for MongoEngine and want to extract it as a dict, you may also do the following:

class EventFactory(factory.mongoengine.MongoEngineFactory):
    # Your code here

class EventAsDictFactory(EventFactory):
    class Meta:
        model = dict

EventAsDictFactory.build()
jcquarto commented 8 years ago

here's the stack trace from the last time I ran the tests before I scooted around the problem by by-passing the use of Sequence with mongoengine.

  1) pychronicle: CRUD behavior: it should correctly generate sequences via Factory
     Failure/Error: spec/pychronicle_spec.py e0 = BaseEventFactory()
         KeyError: '_cls'

     File "/Users/john/anaconda/lib/python2.7/site-packages/mongoengine/fields.py", line 591, in to_python
         doc_cls = get_document(value['_cls'])
     File "/Users/john/anaconda/lib/python2.7/site-packages/mongoengine/base/document.py", line 118, in __init__
         value = field.to_python(value)
     File "/Users/john/anaconda/lib/python2.7/site-packages/factory/mongoengine.py", line 45, in _create
         instance = model_class(*args, **kwargs)
     File "/Users/john/anaconda/lib/python2.7/site-packages/factory/base.py", line 467, in _prepare
         return cls._create(model_class, *args, **kwargs)
     File "/Users/john/anaconda/lib/python2.7/site-packages/factory/base.py", line 492, in _generate
         obj = cls._prepare(create, **attrs)
     File "/Users/john/anaconda/lib/python2.7/site-packages/factory/base.py", line 567, in create
         return cls._generate(True, attrs)
     File "/Users/john/anaconda/lib/python2.7/site-packages/factory/base.py", line 81, in __call__
         return cls.create(**kwargs)
     File "spec/pychronicle_spec.py", line 105, in 00000007__it should correctly generate sequences via Factory
         e0 = BaseEventFactory()

the only difference from the very top of this thread is that the last thing I tried was to re-name the class name from Event to BaseEvent (ditto with the factory) in the nominal hope that "Event" was some sort of reserved word.