pytest-factoryboy icon indicating copy to clipboard operation
pytest-factoryboy copied to clipboard

Factory.build fixtures

Open stevelacey opened this issue 7 years ago • 8 comments

Might be worth adding fixtures for creating models with build instead of create

I know this exists because I've used FactoryBot in Rails and I knew to look for it, but others might not, and it's probably worth nudging people towards writing non-db tests when they can

If I've understood correctly, currently the only way to build is to use the factory fixture and call build?

def test(asset_factory):
    asset = asset_factory.build()

Automatically registering fixtures like asset_build or similar might be a way to go. Thoughts?

stevelacey avatar Jan 20 '18 15:01 stevelacey

@olegpidsadnyi any thoughts on this? A sensible defacto way of retrieving build/unsaved models would replace a bunch of stuff in the suite I'm currently working on – and I'd rather not write a fixture for each model – is there a way to do this already or would adding a pre/suffixed fixture name be the way to go here?

stevelacey avatar Jan 23 '18 11:01 stevelacey

A few further thoughts

  • A decorator would be passable... but I'd rather make the default not saving than making people go to more /effort/ to write db-free tests
  • Automatically toggling this based on whether the db fixture (or /a/ db fixture) is included would be neat but kind of crazy, and I have no idea how you'd do that... that'd negate the need for pre/suffixing though which would be cool
  • An alternative to the above could be a nodb fixture or something but again crazy no idea how

A suffix or a decorator to adjust how the factory works for the test in question seems like the way to go but I am wondering if we can do better

stevelacey avatar Jan 23 '18 11:01 stevelacey

@stevelacey it is more like what kind of base factory are you using, right? What kind of ORM is it in your case. I guess there's a difference between build and create, but if you have a non-db model, what kind of difference does it make?

Could you please describe your setup?

olegpidsadnyi avatar Jan 23 '18 13:01 olegpidsadnyi

I don’t have anything irregular, we’re talking totally normal Django here

I am just trying to write faster tests that don’t touch the database when they don’t need to

In FactoryBot (Ruby) the way you do this is using a build or build_stubbed call, instead of create

Obviously, that call is implicit here via the model fixtures, but FactoryBoy does mirror that build and create distinction on factories – these tests can be written by injecting factory fixtures instead of model fixtures everywhere, and explicitly calling build – but I am wondering if we can do better

stevelacey avatar Jan 23 '18 14:01 stevelacey

@olegpidsadnyi ☝️

stevelacey avatar Feb 04 '18 02:02 stevelacey

This is very good idea. I try another case, byt this is not work. Normally in FactoryBoy we have strategy in class Meta. If I set BUILD_STRATEGY and usage fixture_factory and call to them, this fixture connect with my db. This is not intuitive..

register(MyModel)

def test_something(my_model_factory):
    my_model_factory()   # <- this call db
    my_model_factory.build().   # <- this not call db

witold-gren avatar Mar 29 '18 22:03 witold-gren

I did a lot more work on top of pytest-factoryboy since I suggested this - but I still don’t have an elegant solution to this

I think a great implementation would be an extension of the factory_boy’s use_strategy as a decorator that would be applied to a test definition and apply use_factory to the factory fixture as well as the factory instance used to generate the model fixture before it is passed into the test

If that worked, it would basically mean you can use the regular model fixtures but they wouldn’t do db in that test only

You’d be able to use the stub strategy this way too, which might be handy to some

The feasibility of doing that? Very difficult, and maybe infeasible, was the conclusion I made last time I looked into this On Fri, 30 Mar 2018 at 05:29, witold [email protected] wrote:

This is very good idea. I try another case, byt this is not work. Normally in FactoryBoy we have strategy in class Meta. If I set BUILD_STRATEGY and usage fixture_factory and call to them, this fixture connect with my db. This is not intuitive..

def test_something(my_model_factory): my_model_factory() # <- this call db my_model_factory.build(). # <- this not call db

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/pytest-dev/pytest-factoryboy/issues/56#issuecomment-377392076, or mute the thread https://github.com/notifications/unsubscribe-auth/AARq-2C5LgUIHwjd4r4gxyNwa5yZU5vkks5tjWBRgaJpZM4Rlenv .

stevelacey avatar Mar 30 '18 09:03 stevelacey

Wouldn't the following work? Generate these fixtures:

@pytest.fixture
def derive_factory_strategy():  # or call it factory_strategy_strategy ;)
    def derive(factory_class):
        return factory_class._meta.strategy
    return derive

@pytest.fixture
def {factory_name}__strategy(derive_factory_strategy):
    strategy = derive_factory_strategy(factory_class)
    ...

@pytest.yield_fixture
def {factory_name}({factory_name}__strategy):
    # - Setting TheFactory._meta.strategy is basically all use_strategy does.
    #   I'm not using it as I have to get the _meta.strategy so that
    #   I can reset it later and there's no public function for that that I know of.
    # - You could also use the pytest builtin monkeypatch.setattr to temporarily
    #    change the strategy.
    #   (It's necessary to change it back as the factory is the actual factory
    #   class, which isn't isolated across tests)
    default_strategy = foo_factory._meta.strategy
    foo_factory._meta.strategy = {factory_name}__strategy
    yield foo_factory
    # yield_fixture catches exceptions for us, no need for try: finally:
    foo_factory._meta.strategy = default_strategy

The user can now globally override the desired strategy, e.g. to derive from presence of a db fixture the user could write:

@pytest.fixture
def derive_factory_strategy(request):
    def derive(factory_class):
        if 'db' in request.fixturenames:
            return factory.CREATE_STRATEGY
        else:
            return factory.BUILD_STRATEGY

If a user wants to override a single FooFactory's strategy, they can override the foo_factory__strategy fixture.

hannahdiels avatar Dec 10 '18 13:12 hannahdiels