flyingmutant / rapid

Rapid is a modern Go property-based testing library
https://pkg.go.dev/pgregory.net/rapid
Mozilla Public License 2.0
579 stars 25 forks source link

Common interface for rapid.T and testing.T #26

Closed vfaronov closed 3 years ago

vfaronov commented 3 years ago

Thanks for this fine library.

I have the same problem as in #6: utility functions that I would like to call from both regular and property-based tests. They can’t take *rapid.T because there is none in regular tests. They could take *testing.T — the same one that I pass to rapid.Check — and this is how #6 was settled — but, firstly, this makes testing functions slightly less readable (can’t just pass the same t everywhere, need separate variables), and secondly, is it even correct? don’t the *rapid.T methods do anything rapid-specific above and beyond the underlying *testing.T?

It would be most convenient to make these utility functions take an interface — defined and exported from rapid, e.g. rapid.CommonT or something — that both *rapid.T and *testing.T implement.

But I’m not sure this is the right approach. Feel free to close this issue if you don’t like the idea.

vfaronov commented 3 years ago

On second thought, looks like this won’t work, because the first thing a utility function needs is Helper(), and there’s no way for *rapid.T to implement it.

My other idea would be to export a T *testing.T field on rapid.T, but it’s not clear how that would be better than just having two variables.

Still, please do comment: Is it OK to call the underlying (*testing.T).Fatalf directly instead of the (*rapid.T).Fatalf, etc.?

flyingmutant commented 3 years ago

(*testing.T).Fatalf is a bit magical: it calls FailNow, which does runtime.Goexit(). So rapid has no chance to do e.g. shrinking. I've never tested this case.

Do you have the same use case of writing tests that run on both hand-picked examples, and rapid-generated ones? It is an important one, and I've planned to add direct support for it (it is in my before-1.0 ideas list).

vfaronov commented 3 years ago

Do you have the same use case of writing tests that run on both hand-picked examples, and rapid-generated ones?

No, just small utility functions, such as program-specific assertions.

vfaronov commented 3 years ago

Well, this might still be convenient:

func mustBaz(t rapid.CommonT, tt *testing.T) {
    tt.Helper()
    if err := baz(); err != nil {
        t.Fatalf("failed to baz: %v", err)
    }
}

and then call mustBaz(t, t) from regular tests and mustBaz(t, tt) from rapid tests. Which is to say, a rapid.CommonT might still be useful.

flyingmutant commented 3 years ago

Hm, can you try making type tb interface in engine.go public, and doing

func mustBaz(t rapid.TB) {
    t.Helper()
    if err := baz(); err != nil {
        t.Fatalf("failed to baz: %v", err)
    }
}

? I think it should route Helper() properly both for *rapid.T and *testing.T.

flyingmutant commented 3 years ago

@vfaronov did you have a chance to try my suggestion?

vfaronov commented 3 years ago

Sorry for the delay. You’re right, it seems to work! I didn’t think of embedding.

vfaronov commented 3 years ago

@flyingmutant Thank you!