Skip to content

Commit

Permalink
Add rivertest.WorkContext for use testing JobArgs.Work implementa…
Browse files Browse the repository at this point in the history
…tions

Here, add a new test helper called `rivertest.WorkContext`. Its purpose
is to generate a realistic looking context for testing `JobArgs.Work`
implementation, particularly by adding a client to context that makes
`river.ClientFromContext` available in tests, but may also be used to
add any other context-related features that may be added in the future.

We've talked about new test helpers for running `Work` implementations
and may still do that, but this is a primitive that makes testing a
little better without having to add anything heavyweight.
  • Loading branch information
brandur committed Aug 11, 2024
1 parent 40e1bfd commit 9af3b2c
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 7 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `rivertest.WorkContext`, a test function that can be used to initialize a context to test a `JobArgs.Work` implementation that will have a client set to context for use with `river.ClientFromContext`. [PR #514](https://github.com/riverqueue/river/pull/514).

## [0.11.2] - 2024-08-08

### Fixed
Expand Down
16 changes: 9 additions & 7 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,14 @@ package river
import (
"context"
"errors"
)

type ctxKey int

const (
ctxKeyClient ctxKey = iota
"github.com/riverqueue/river/internal/rivercommon"
)

var errClientNotInContext = errors.New("river: client not found in context, can only be used in a Worker")

func withClient[TTx any](ctx context.Context, client *Client[TTx]) context.Context {
return context.WithValue(ctx, ctxKeyClient, client)
return context.WithValue(ctx, rivercommon.ContextKeyClient{}, client)
}

// ClientFromContext returns the Client from the context. This function can
Expand All @@ -23,6 +19,9 @@ func withClient[TTx any](ctx context.Context, client *Client[TTx]) context.Conte
//
// It panics if the context does not contain a Client, which will never happen
// from the context provided to a Worker's Work() method.
//
// When testing JobArgs.Work implementations, it might be useful to use
// rivertest.WorkContext to initialize a context that has an available client.
func ClientFromContext[TTx any](ctx context.Context) *Client[TTx] {
client, err := ClientFromContextSafely[TTx](ctx)
if err != nil {
Expand All @@ -37,8 +36,11 @@ func ClientFromContext[TTx any](ctx context.Context) *Client[TTx] {
//
// It returns an error if the context does not contain a Client, which will
// never happen from the context provided to a Worker's Work() method.
//
// When testing JobArgs.Work implementations, it might be useful to use
// rivertest.WorkContext to initialize a context that has an available client.
func ClientFromContextSafely[TTx any](ctx context.Context) (*Client[TTx], error) {
client, exists := ctx.Value(ctxKeyClient).(*Client[TTx])
client, exists := ctx.Value(rivercommon.ContextKeyClient{}).(*Client[TTx])
if !exists || client == nil {
return nil, errClientNotInContext
}
Expand Down
2 changes: 2 additions & 0 deletions internal/rivercommon/river_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const (
QueueDefault = "default"
)

type ContextKeyClient struct{}

// ErrShutdown is a special error injected by the client into its fetch and work
// CancelCauseFuncs when it's stopping. It may be used by components for such
// cases like avoiding logging an error during a normal shutdown procedure. This
Expand Down
10 changes: 10 additions & 0 deletions rivertest/rivertest.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"time"

"github.com/riverqueue/river"
"github.com/riverqueue/river/internal/rivercommon"
"github.com/riverqueue/river/riverdriver"
"github.com/riverqueue/river/rivershared/util/sliceutil"
"github.com/riverqueue/river/rivertype"
Expand Down Expand Up @@ -535,3 +536,12 @@ func failure(t testingT, format string, a ...any) {
func failureString(format string, a ...any) string {
return "\n River assertion failure:\n " + fmt.Sprintf(format, a...) + "\n"
}

// WorkContext returns a realistic context that can be used to test JobArgs.Work
// implementations.
//
// In particual, adds a client to the context so that river.ClientFromContext is
// usable in the test suite.
func WorkContext[TTx any](ctx context.Context, client *river.Client[TTx]) context.Context {
return context.WithValue(ctx, rivercommon.ContextKeyClient{}, client)
}
26 changes: 26 additions & 0 deletions rivertest/rivertest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,32 @@ func TestRequireManyInsertedTx(t *testing.T) {
})
}

func TestWorkContext(t *testing.T) {
t.Parallel()

ctx := context.Background()

type testBundle struct{}

setup := func(ctx context.Context, t *testing.T) (context.Context, *testBundle) {
t.Helper()

client, err := river.NewClient(riverpgxv5.New(nil), &river.Config{})
require.NoError(t, err)

return WorkContext(ctx, client), &testBundle{}
}

t.Run("ClientFromContext", func(t *testing.T) {
t.Parallel()

ctx, _ := setup(ctx, t)

client := river.ClientFromContext[pgx.Tx](ctx)
require.NotNil(t, client)
})
}

// MockT mocks testingT (or *testing.T). It's used to let us verify our test
// helpers.
type MockT struct {
Expand Down

0 comments on commit 9af3b2c

Please sign in to comment.