-
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
Add redisxtest #484
base: master
Are you sure you want to change the base?
Add redisxtest #484
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we apply this to existing test code in this repository while we are at it? I noticed that we use miniredis here too:
func NewMockRedisClient(ctx context.Context, timeout time.Duration) (client redisx.Syncx, teardown func(), err error) { | ||
redisCluster, err := miniredis.Run() | ||
if err != nil { | ||
return redisx.Syncx{}, func() {}, err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: in an error return, we would want to return nil
for non-error returns whenever possible. you cannot return nil
for the client
, but you certainly can return nil
for the teardown
(we don't want people using it to blindly defer teardown()
without checking for errors first).
|
||
// NewMockRedisClient sets up a mock redis cluster, client and sender | ||
// This should be called from TestMain since the miniredis instance created is locked | ||
func NewMockRedisClient(ctx context.Context, timeout time.Duration) (client redisx.Syncx, teardown func(), err error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if this is only intended to be used by unit tests, we do not really need to return teardown
and err
. just pass intb testing.TB
as an arg (so it can be used by both tests and benchmarks), then when an error happens we can just tb.Fatalf("Failed to ...: %v", err)
, and at the end, before returning, we can just call tb.Cleanup(teardown)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like the main use case is to set this up in testing.M
which sadly does not have those those methods available.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@konradreiche that is correct. We have been following the pattern of setup in TestMain
since places where the mock redis cluster is used tend to be places where it made sense to init there. This approach also gives the flexibility to use this func in both tests directly but also in TestMain
. However, if we feel strongly that supporting TestMain
doesn't have much value, then I am happy to change this.
return client, teardown, nil | ||
} | ||
|
||
func FlushRedis(ctx context.Context, client redisx.Syncx) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see this used by NewMockRedisClient
, and it's not explained by the PR description either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing. This was a func we were using locally that got dropped in here. Agree it is probably not necessary.
redis/cache/redispipebp/init_test.go
Outdated
@@ -53,7 +60,9 @@ func TestMain(m *testing.M) { | |||
client = redisx.BaseSync{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this value of client
is no longer used by anything?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed.
// NewMockRedisClient sets up a client and sender to a mock redis cluster | ||
func NewMockRedisClient( | ||
ctx context.Context, | ||
redisCluster MockRedisCluster, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this feels like an over-design to me.
if in most tests we only need a single, global, mocked cluster and client, then we don't really need separated NewMockRedisCluster
and NewMockRedisClient
, a single NewMockRedisClient
with its returned teardown
also tears down the cluster should be sufficient (the cluster does not need to be exposed to the caller at all).
but if that's not the case, then I think only the cluster itself should be global (that's handled inside TestMain
), the clients should be created per test, so my previous comment regarding passing in testing.TB
and no need to return error
nor teardown
applies.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and looking at this function again I don't see it doing anything special. all it does are pretty standard stuff about create a redis connection (you actually only need the address passed in, not the whole cluster object) and create a redis client out of this connection. the teardown
is also standard stuff about closing the client, so I don't really see much value of providing (and maintaining) this as part of the public API.
If we think a func NewClient(ctx, addr, opts) (redisx.BaseSync, error)
API is useful, then that should be part of redisx
, not redisxtest
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reason for the change to have separated NewMockRedisCluster
and NewMockRedisClient
was to support the cases like that in TestMain
in init_test
, where it looks like we need a global cluster. Perhaps an alternative solution to this would be to have the following API for redisxtest:
func NewMockRedisClient(tb, ctx, connOpts) (client, error)
func NewMockRedisClientWithCluster(tb, ctx, clusterAddr, connOpts) (client, error)
These functions would handle cleanup and client creation via tb as you have suggested.
I am really not opposed at all to going down the path updating the redisx API to have a func NewClient()
. This is actually our preferred route. We saw adding redisxtest
as being minimally invasive, which was the reason for going this path. If you are ok with us doing that, happy to open another PR for that change and close potentially close this one if we see no value in what I have proposed.
// Create connection | ||
conn, err := redisconn.Connect(ctx, redisCluster.Addr(), opts) | ||
if err != nil { | ||
redisCluster.Close() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a very unexpected side effect. if a client cannot connect to the server, it will also kill the server, causing other, clients to stop working.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed
} | ||
|
||
// Close shuts down the MockRedisCluster | ||
func (mrc *MockRedisCluster) Close() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: make this function to return an error, so it implements io.Closer
interface
since miniredis.Miniredis.Close
does not return an error, you can just always return nil
inside this function.
teardown = func() { | ||
mockRedisCluster.Close() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a very standard closing pattern in go (especially when you make MockRedisCluster
to implement io.Closer
), so I would say returning a teardown
is unnecessary, caller could just do:
cluster, err := NewMockRedisCluster()
if err != nil { ... }
defer cluster.Close()
// NewMockRedisClient sets up a client and sender to a mock redis cluster | ||
func NewMockRedisClient( | ||
ctx context.Context, | ||
redisCluster MockRedisCluster, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and looking at this function again I don't see it doing anything special. all it does are pretty standard stuff about create a redis connection (you actually only need the address passed in, not the whole cluster object) and create a redis client out of this connection. the teardown
is also standard stuff about closing the client, so I don't really see much value of providing (and maintaining) this as part of the public API.
If we think a func NewClient(ctx, addr, opts) (redisx.BaseSync, error)
API is useful, then that should be part of redisx
, not redisxtest
.
func NewMockRedisClient( | ||
ctx context.Context, | ||
address string, | ||
opts redisconn.Opts, | ||
) (client redisx.BaseSync, teardown func(), err error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would still recommend change this function signature into:
func NewMockRedisClient(ctx context.Context, tb testing.TB, address string, opts redisconn.Opts) redisx.BaseSync
it's OK for the mocked cluster to be a global singleton that's initialized in TestMain
and shared between tests, because a mocked cluster is probably using much more resource and stand it up and tear it down in every test case is probably too slow/wasteful, but for clients, the pattern we should promote should be that every unit test create it and clean it up, instead of doing it in TestMain
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we would still want to change the existing tests to use this, that includes:
- instead of store the client globally (
client redisx.Sync - in tests currently using the global client, use this function to get the client instead
- change
flushRedis
to take tb and client as args.
// Cleanup | ||
tb.Cleanup(func() { | ||
conn.Close() | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I would move this up to right after the err
check (to line 27).
// NewMockRedisClient sets up a redis client for use in testing | ||
func NewMockRedisClient( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
now there's nothing really "mocking" here (the mocking part is done via miniredis, which is no longer done here).
I would rename this function to NewTestRedisClient
, and document that:
- the
address
arg should be coming from miniredis (we should also provide an example on how to it, see https://pkg.go.dev/testing#hdr-Examples, we also have prior arts in baseplate.go, for example https://github.com/reddit/baseplate.go/blob/master/errorsbp/batch_size_example_test.go) - we should also document that the returned client will be auto released at the end of test and there's no need to do that explicitly (because the caller actually still have access to that via
client.SyncCtx.S.Close()
@@ -0,0 +1,4 @@ | |||
// Package redisxtest provides utilities for baseplate.go/redids/cache/redisx testing. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no need to spell out the full baseplate.go/redids/cache/redisx
, just say "... for redisx testing" is enough.
@@ -0,0 +1,4 @@ | |||
// Package redisxtest provides utilities for baseplate.go/redids/cache/redisx testing. | |||
// | |||
// This includes functions for mocking redis instances. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same comment here regarding "mocking". I think with the current state of this change the first paragraph is enough for the package doc and we can just remove this paragraph.
tb testing.TB, | ||
address string, | ||
opts redisconn.Opts, | ||
) (*redisx.BaseSync, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looking at how existing tests work, we do need the returned client to be of type redisx.BaseSync
instead of the pointer, for example
wClient := redispipebp.WrapErrorsSync{Sync: client} |
if err != nil { | ||
return nil, err | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh and this function no longer need to return an error. this can be just:
if err != nil { | |
return nil, err | |
} | |
if err != nil { | |
tb.Fatalf("Failed to create redis client: %v", err) | |
} |
// Create connection to redis | ||
conn, err := redisconn.Connect(ctx, address, opts) | ||
if err != nil { | ||
return nil, err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is a testing helper, in my opinion it's better to call t.Fatalf and document that it must be called directly in a test / subtest, rather than do what is inevitable: have every user of this make their own local helper that wraps this helper that does the transition. I prefix my t.Fatalf errors with "SETUP:" to make it clear that it's not a failure of the code under test, and I would name the function "Setup" not "New" to encourage that interpretation as well.
|
||
// Cleanup | ||
tb.Cleanup(func() { | ||
conn.Close() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
check the error and log it if it fails?
Summary
Adds redisxtest implementation which provides setup and teardown functions for setting up a mock redis cluster and client for testing purposes. One of the challenges of using this package is that testing clients requires writing setup and teardown functions for the mock redis instance. This leads to a lot of copy and paste across baseplate.go services, since the pattern is nearly identical. This change makes it possible to set up a mock redis cluster and client of that cluster with two simple functions.
What to look out for
Current implementation uses
miniredis
to mock the redis instance.miniredis
is the currently the predominately used library for standing up a local in-memory redis instance for testing purposes in go. However, there may be other options out there.