Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inject fixtures into given or composite? #1658

Closed
chebee7i opened this issue Oct 26, 2018 · 3 comments
Closed

Inject fixtures into given or composite? #1658

chebee7i opened this issue Oct 26, 2018 · 3 comments
Labels
question not sure it's a bug? questions welcome

Comments

@chebee7i
Copy link

I'm running various integration tests (that interact with a database), and so I need some global state (like an ID generator) to persist across tests. Normally I use fixtures for this in pytest. So for example:

def my_test(some_fixture):
     x = build_using_fixture(some_fixture)
     assert something(x)

With hypothesis, I've switched to using @given and specifying the things I normally built inside the test, but I'm not sure how to accomplish this in the same way with a fixture. I've read: https://hypothesis.works/articles/hypothesis-pytest-fixtures/, but it doesn't work if I would have needed the fixture to build the input data. So I was looking for something like (which doesn't work):

@given(x_builder(some_fixture))
def my_test():
    assert something(x)

If I used @composite, then I can get it done...

@composite
def my_test(draw, some_fixture):
    x = build_using_fixture(draw, fixture)
    assert something(x)

but now I'm back to not being able to cleanly see what the data by inspecting only the decorator. Might something like this be possible?

@composite
def builder(draw, some_fixture):
    x = build_using_fixture(draw, some_fixture)
    return x

@given(builder())
def my_test(x):
    assert something(x)
@Zac-HD Zac-HD added the question not sure it's a bug? questions welcome label Oct 27, 2018
@Zac-HD
Copy link
Member

Zac-HD commented Oct 27, 2018

If I used @composite, then I can get it done...

Uh. @composite is for defining strategies, not tests - your example here won't execute the test body at all, even if Pytest collects and runs the callable it returns. It would be nice have some way to detect this and warn about it, but everything I can think of is also prone to false-positives 😭

[Can a strategy defined with @composite accept fixtures as arguments]

No. The differing execution models and scoping rules make this impossible.

What to do instead

Use a factory pattern! Just like fixtures can return factories, you can also define a strategy that generates a customised factory for each test case:

@composite
def builder(draw, ...):
    value = draw(...)
    return functools.partial(build_using_fixture, non_fixture_arg=value)

@given(build=builder())
def test(some_fixture, build):
	# Each test case will get a `build` function with different
    # pre-filled arguments, which will be shrunk as usual.
    x = build(some_fixture)
    hypothesis.note(f'x = {repr(x)}')  # extra reporting
    assert something(x)

Hope that helps - feel free to ask any follow-up questions.

@Zac-HD Zac-HD closed this as completed Oct 27, 2018
@chebee7i
Copy link
Author

Opps on my usage of @composite. That's what I get for writing hurriedly as I run out to drink beers. :)

That's a good example thanks! Since the number of things I need to draw is conditional on the output of prior draws and the in-progress object that I'm trying to build, I'm actually passing draw to build_using_fixture right now. It seems to be working alright. Are there any concerns with the approach with respect to shrinking etc?

@Zac-HD
Copy link
Member

Zac-HD commented Oct 29, 2018

Passing draw to a function should be fine! (and in fact if some draws depend on the object you get from the fixture my earlier proposal won't help)

Shrinking can be tricky, but the baseline advice is that Hypothesis is really, really good at shrinking. Three bits of advice:

  • Pay attention to how shrinks compose. For example, make sure that drawing smaller integers or shorter lists corresponds to a natural idea of shrinking for your custom objects. If you want to shrink e.g. numbers towards 100, you could use integers().map(lambda x: x + 100) so that as the underlying strategy shrinks so does the resulting object.
  • For complicated @composite strategies, allow each component of the strategy to shrink as independently as possible - this makes shrinking much more efficient. In practice this is basically black magic and very difficult even for us, so I wouldn't bother.
  • The zero-effort baseline is so good that you don't need to think about shrinking until you either have an observed problem, or are building a library for external users who will not tell you when they have a problem.

TLDR: pay attention to composition, and otherwise ignore shrinking.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question not sure it's a bug? questions welcome
Projects
None yet
Development

No branches or pull requests

2 participants