-
-
Notifications
You must be signed in to change notification settings - Fork 78
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
Discussion: foo.NewFoo(foo.WithBar()...) design for v4 release #102
Comments
Hey @fornellas-udemy , Thanks for the detailed issue!
Not necessarily. Once a recorder has been created, it can be decorated/customized further, if needed. It's just a matter of calling out those options again, but with different values. I won't be able to go over each point now, but will try to get back to them soon. For now, I'll leave this code here, which is a rough translation of the provided code to // foo/foo.go
func TestFoo(t *testing.T) {
vcrRecorder := vcr.GetRecorder(t)
fooToken := SetupFooTest(t, vcrRecorder)
runTestWithTokenAndClient(fooToken, vcrRecorder.GetDefaultClient())
}
func SetupFooTest(t *testing.T, vcrRecorder *recorder.Recorder) (authToken string) {
// based on vcrRecorder.Mode():
// - If ModeReplayOnly, use a fixed mocked token value
// - If ModeRecordOnce, try fetching a valid token from env vars
authToken, replacement := getTestAuthToken(t, vcrRecorder.Mode())
recorder.WithHook(
vcr.GetMaskHeadersHookFn([]vcr.HeaderMaskRule{
{
Header: "Authorization",
Secret: authToken,
Replacement: replacement,
},
}),
recorder.AfterCaptureHook,
)(vcrRecorder)
recorder.WithRealTransport(vcr.NewReadOnlyRoundTripper(t, http.DefaultTransport))(vcrRecorder)
// if ModeReplayOnly, then remove rate limit
setTestRateLimit(t, vcrRecorder.Mode())
client = NewClient(authToken, vcrRecorder.GetDefaultClient())
require.NotNil(t, client)
return authToken
}
// vcr/helpers.go
// Gives a baseline common Recorder that behaves coherently across all tests.
func GetRecorder(t *testing.T) *recorder.Recorder {
cassetteName := fmt.Sprintf("fixtures/%s", t.Name())
cassettePath := fmt.Sprintf("%s.yaml", cassetteName)
mode := recorder.ModeReplayOnly
_, err := os.Stat(cassettePath)
switch {
case errors.Is(err, os.ErrNotExist):
// Cassette is missing, recording
mode = recorder.ModeRecordOnce
case errors.Is(err, nil):
// Cassette is present
if os.Getenv("VCR_UPDATE_FIXTURES=true") == "true" {
if err := os.Remove(cassettePath); err != nil {
require.NoError(t, err)
}
mode = recorder.ModeRecordOnce
}
default:
// Some other error occurred
require.NoError(t, err)
}
r, err := recorder.New(
recorder.WithCassette(cassetteName),
recorder.WithMode(mode),
recorder.WithSkipRequestLatency(true),
recorder.WithMatcher(cassette.NewDefaultMatcher(cassette.WithIgnoreUserAgent(true))),
)
require.NoError(t, err)
t.Cleanup(func() {
if !t.Failed() {
require.NoError(t, r.Stop())
}
})
return r
} I think the functional opts pattern offers a cleaner API overall, and also doesn't expose too much of the inner workings of the recorder itself. In Again, thanks for the detailed issue, and I'll try to get back to the rest of the points soon! |
OK, so because of
I feel like we're disagreeing on this point. The fact the previous suggestion wasn't obvious to me (and maybe others as well), is evidence of that this is not black and white. The argument about "cleaner API" is subjective, and often a function of our frame of reference, so... neither is view necessarily better / worse than the other. However, I think we can more objectively look at two different designs, and gauge complexity, easiness to understand, alignment with common Go idioms, volume of code required etc etc. In this light, I'm considering comparing this: type Recorder struct {
BlockUnsafeMethods bool
Cassette string
AfterCaptureHook []HookFunc
BeforeSaveHook []HookFunc
BeforeResponseReplayHook []HookFunc
OnRecorderStopHook []HookFunc
Matcher MatcherFunc
Mode Mode
PassthroughFunc PassthroughFunc
RealTransport http.RoundTripper
ReplayableInteractions bool
SkipRequestLatency bool
} to what we have now on v4:
Personally, I always gravitate towards the simplest solution: the best code is the one I don't have to write or maintain :-P @dnaeon thanks for taking the time to engage on the conversation. I suppose your suggestion above "unblocks" me from having to fight to refactor a lot of stuff, and just do some ad-hoc substitutions, so thanks for the suggestion. If you feel like keeping the discussion about the v4 design here, I'm happy to put time to help craft something concrete out of this. If not, it is totally fine, as there's some subjective element to this conversation, and no point in being opinionated. |
Nothing is set in stone, and I'm open to any suggestions and improvements for |
So... how about using the plain struct, WDYT? type Recorder struct {
BlockUnsafeMethods bool
Cassette string
AfterCaptureHook []HookFunc
BeforeSaveHook []HookFunc
BeforeResponseReplayHook []HookFunc
OnRecorderStopHook []HookFunc
Matcher MatcherFunc
Mode Mode
PassthroughFunc PassthroughFunc
RealTransport http.RoundTripper
ReplayableInteractions bool
SkipRequestLatency bool
} |
cc: @jsoriano, @calvinmclean |
I don't have a huge stake in the implementation of options/config. Either way will be fine for me, but here's a few of my opinions:
Overall, I have a preference for the new options pattern but don't have any compelling concrete reasons for it since it's mostly my personal preference. |
That's a good point. The cassette name is required, which I will take into account and fix that in
Exactly. And that is one of the nice things about this pattern. Here's an example in one of my other projects, where I think this plays out nicely, when configuring options externally. Another benefit to me about this pattern is that it helps with encapsulation and not exposing too much of the inner details of the recorder. It also helps with separation of concerns, because it allows each Without this, when using a single Another good read about functional options may be found in Uber Go Style Guide.
That's how I think about it as well -- having too many ways to create a recorder is not a good thing, in my opinion. Thanks for the feedback, @calvinmclean ! |
Related change: |
Thanks for sharing these points. I reckon there's a preference for this pattern here, so arguing about preference either way, when both would be functional, may not be fruitful at this point (especially because the effort to write the code already happened). How v4 is, is functional for me, regardless on what's my preference. I'm OK closing this issue if you are @dnaeon . |
On v3, I've been building tests with VRC using a pattern like so:
vcr.GetRecorder()
function that sets up common behavior for the Recorder.SetupFooTest()
, which decorates the Recorder behavior with specifics:On v4, the change to
foo.NewFoo(foo.WithBar()...)
requires refactoring all this logic:recorder.Option
functions from the "baseline", then for each used API, and then finally creating the object and setting up the cleanup.GetRecorder(t *testing.T) *recorder.Recorder
would need changing to something likeGetRecorderOptions(t *testing.T) []recorder.Option
, so it'd return a list of function pointers.SetupFooTest(t *testing.T, vcrRecorder *recorder.Recorder) (authToken string)
would need to be changed to something likeSetupFooTest(t *testing.T, mode recorder.Mode) (authToken string, []recorder.Option)
, which creates sort of a chicken and egg problem.GetRecorderOptions()
returns function pointers, and we can't know what's the recording mode there.Considering v3 + #99 + #100 as a baseline, if we called that v4, upgrading would be trivial (only the new stricter default matcher could break some pre-existing brittle / dubious code). With this v4 interface change, upgrades become a big endeavour.
To be clear, I'm 100% on breaking APIs and improving things, assuming the end result brings us somewhere that's so much better, that's worth the migration headache. In this case though:
WithSomeOption()
methods requires (a lot) more code to write and maintain than a simpletype Options struct
, or even simpler, a plaintype Recorder struct
, a lahttp.Server
from the standard library (no need forWithOptionFoo()
orfoo.SetOptionBar()
code).WithOption()
puts more cognitive load on users than other solutions.WithOption()
addresses, and why it is worth the migration trouble.@dnaeon, WDYT?
The text was updated successfully, but these errors were encountered: