181 lines
4.9 KiB
Markdown
181 lines
4.9 KiB
Markdown
# Testing GraphQL in Go
|
|
|
|
## gqlgen — Client Harness
|
|
|
|
The `github.com/99designs/gqlgen/client` package drives the full stack (directives, middleware, resolvers) via an `http.Handler`:
|
|
|
|
```go
|
|
func TestCreateUser(t *testing.T) {
|
|
// Build the full handler with real dependencies (use a test DB)
|
|
srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{
|
|
Resolvers: &graph.Resolver{
|
|
DB: testDB,
|
|
},
|
|
}))
|
|
|
|
c := client.New(srv)
|
|
|
|
var resp struct {
|
|
CreateUser struct {
|
|
User struct {
|
|
ID string
|
|
Email string
|
|
}
|
|
Errors []struct{ Message string }
|
|
}
|
|
}
|
|
|
|
c.MustPost(`
|
|
mutation CreateUser($email: String!, $name: String!) {
|
|
createUser(input: {email: $email, name: $name}) {
|
|
user { id email }
|
|
errors { message }
|
|
}
|
|
}
|
|
`, &resp,
|
|
client.Var("email", "alice@example.com"),
|
|
client.Var("name", "Alice"),
|
|
client.AddHeader("Authorization", "Bearer test-token"),
|
|
)
|
|
|
|
require.Empty(t, resp.CreateUser.Errors)
|
|
require.Equal(t, "alice@example.com", resp.CreateUser.User.Email)
|
|
}
|
|
```
|
|
|
|
For unit testing individual resolvers, call resolver methods directly with a constructed `Resolver` and a real `context.Context` — no HTTP overhead.
|
|
|
|
## gqlgen — Testing with DataLoaders
|
|
|
|
Wrap the test server with the DataLoader middleware so resolver tests exercise the full batching path:
|
|
|
|
```go
|
|
srv := handler.NewDefaultServer(es)
|
|
h := dataloaders.Middleware(testDB, srv)
|
|
|
|
c := client.New(h)
|
|
```
|
|
|
|
## gqlgen — Testing Subscriptions
|
|
|
|
Use `client.Subscription` to test subscription resolvers:
|
|
|
|
```go
|
|
sub := c.Subscription(`subscription { messageAdded(room: "general") { content } }`)
|
|
defer sub.Close()
|
|
|
|
// Trigger an event
|
|
publishMessage("general", "hello")
|
|
|
|
var event struct{ MessageAdded struct{ Content string } }
|
|
err := sub.Next(&event)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "hello", event.MessageAdded.Content)
|
|
```
|
|
|
|
## graph-gophers — gqltesting
|
|
|
|
```go
|
|
func TestUser(t *testing.T) {
|
|
gqltesting.RunTests(t, []*gqltesting.Test{
|
|
{
|
|
Schema: schema,
|
|
Query: `{ user(id: "1") { name email } }`,
|
|
ExpectedResult: `{"user":{"name":"Alice","email":"alice@example.com"}}`,
|
|
},
|
|
{
|
|
Schema: schema,
|
|
Query: `{ user(id: "999") { name } }`,
|
|
ExpectedErrors: []*gqlerrors.QueryError{
|
|
{Message: "user not found", Extensions: map[string]any{"code": "NOT_FOUND"}},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
```
|
|
|
|
For HTTP-level tests:
|
|
|
|
```go
|
|
func TestRelayHandler(t *testing.T) {
|
|
body := `{"query":"{ user(id: \"1\") { name } }"}`
|
|
req := httptest.NewRequest(http.MethodPost, "/graphql", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
relay.Handler{Schema: schema}.ServeHTTP(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
require.Contains(t, w.Body.String(), `"Alice"`)
|
|
}
|
|
```
|
|
|
|
## Testing Error Handling
|
|
|
|
Verify error extensions reach the client:
|
|
|
|
```go
|
|
var resp struct {
|
|
Errors []struct {
|
|
Message string
|
|
Extensions struct{ Code string }
|
|
}
|
|
}
|
|
c.Post(`{ user(id: "999") { name } }`, &resp)
|
|
require.Equal(t, "NOT_FOUND", resp.Errors[0].Extensions.Code)
|
|
```
|
|
|
|
## Testing Auth Directives (gqlgen)
|
|
|
|
Test the directive function directly:
|
|
|
|
```go
|
|
func TestHasRoleDirective(t *testing.T) {
|
|
ctx := context.WithValue(context.Background(), userKey, &domain.User{Role: "USER"})
|
|
_, err := HasRole(ctx, nil, func(ctx context.Context) (any, error) {
|
|
return "ok", nil
|
|
}, model.RoleAdmin)
|
|
require.Error(t, err)
|
|
|
|
var gqlErr *gqlerror.Error
|
|
require.True(t, errors.As(err, &gqlErr))
|
|
require.Equal(t, "FORBIDDEN", gqlErr.Extensions["code"])
|
|
}
|
|
```
|
|
|
|
## Table-Driven Tests
|
|
|
|
```go
|
|
func TestUserQueries(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
query string
|
|
vars map[string]any
|
|
wantCode string
|
|
wantName string
|
|
}{
|
|
{"existing user", `query($id:ID!){user(id:$id){name}}`, map[string]any{"id": "1"}, "", "Alice"},
|
|
{"missing user", `query($id:ID!){user(id:$id){name}}`, map[string]any{"id": "999"}, "NOT_FOUND", ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var resp struct {
|
|
User *struct{ Name string }
|
|
Errors []struct {
|
|
Extensions struct{ Code string }
|
|
}
|
|
}
|
|
c.Post(tt.query, &resp, client.Var("id", tt.vars["id"]))
|
|
if tt.wantCode != "" {
|
|
require.Equal(t, tt.wantCode, resp.Errors[0].Extensions.Code)
|
|
} else {
|
|
require.Equal(t, tt.wantName, resp.User.Name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
For testing patterns across the codebase, see the `samber/cc-skills-golang@golang-testing` skill.
|