42 Writing Unit Tests for Queries in graphql-go
42 Writing Unit Tests for Queries in graphql-go
As modern backend application developers, GraphQL is nothing new to us. Query flexibility, data efficiency, and self-documentation are the reasons many teams have switched to GraphQL. In the Go ecosystem, the graphql-go
library has become a popular choice. However, one thing is often overlooked when adopting GraphQL in Go: Unit Testing.
Unit tests are not just a formality. They serve as an insurance policy against future bugs, especially for queries that act as the main bridge between client and server. This article breaks down, step by step, how to write unit tests for queries in graphql-go, complete with code examples, mock data simulations, and best practices.
Why Unit Test Queries?
Table: Benefits of Unit Testing GraphQL Queries
| Benefit | Explanation |
|---|---|
| Ensures Correctness | Make sure the response structure and data resolution are correct |
| Supports Refactoring | Adding/modifying resolvers is safe because tests are ready to catch regressions |
| Living Documentation | Backend consumers learn the use cases and responses by reading the test cases |
| Tests Error Handling | Simulating error scenarios (e.g., unauthorized, empty data) becomes easy |
GraphQL-Go: Installation and Basic Schema
Before diving into unit tests, let’s set up a simple GraphQL schema in Go. To stay focused, we’ll create the query getUser(id: ID!): User.
1type User struct {
2 ID string
3 Name string
4}
5
6type Resolver struct{}
7
8func (r *Resolver) GetUser(ctx context.Context, args struct{ ID string }) (*User, error) {
9 // Scenario: Fetch a user by ID, or return an error if not found
10 if args.ID == "42" {
11 return &User{ID: "42", Name: "Arthur Dent"}, nil
12 }
13 return nil, errors.New("user not found")
14}
15
16// schema.graphql
17/*
18type User {
19 id: ID!
20 name: String!
21}
22
23type Query {
24 getUser(id: ID!): User
25}
26*/The integration into the server usually looks like this:
1schema := graphql.MustParseSchema(schemaString, &Resolver{})
2http.Handle("/query", &relay.Handler{Schema: schema})Fundamentals of Unit Testing Queries in graphql-go
Unit tests in graphql-go do not hit the HTTP endpoint. They invoke the resolver directly through the schema, passing the query and variables. The advantage is that unit tests can run quickly and remain isolated from middleware/auth/network.
Dependencies
Add these to go.mod:
1require (
2 github.com/graph-gophers/graphql-go v1.7.0
3 github.com/stretchr/testify v1.8.0
4)Writing a Simple Unit Test
Let’s create the test file: resolver_test.go.
Remember, a Go unit test must use a function with the signature TestXxx(t *testing.T) so that go test recognizes it.
1package main
2
3import (
4 "context"
5 "testing"
6
7 "github.com/graph-gophers/graphql-go"
8 "github.com/stretchr/testify/assert"
9)
10
11// Simplify the schema for testing
12const testSchema = `
13type User {
14 id: ID!
15 name: String!
16}
17
18type Query {
19 getUser(id: ID!): User
20}
21`
22
23// Mock resolver implementation for testing
24type testResolver struct{}
25
26func (r *testResolver) GetUser(ctx context.Context, args struct{ ID string }) (*User, error) {
27 if args.ID == "42" {
28 return &User{ID: "42", Name: "Tester"}, nil
29 }
30 return nil, nil
31}
32
33func TestGetUserQuery(t *testing.T) {
34 schema := graphql.MustParseSchema(testSchema, &testResolver{})
35
36 query := `
37 query ($id: ID!) {
38 getUser(id: $id) {
39 id
40 name
41 }
42 }
43 `
44 vars := map[string]interface{}{
45 "id": "42",
46 }
47
48 // Execute the query directly against the schema (not over HTTP)
49 response := schema.Exec(context.Background(), query, "GetUser", vars)
50
51 // The status code is always 200, so check the data and errors instead
52 expected := `{"getUser":{"id":"42","name":"Tester"}}`
53 assert.JSONEq(t, expected, string(response.Data))
54 assert.Nil(t, response.Errors)
55}Simulating Unit Tests with Different Data
The tests should cover:
- A successful data query
- A query for data that is not found (should return nil)
- A query with an invalid argument
Simulation Schema
Here is a simulation of the expected branching cases:
flowchart TD
A[Start Test] --> B{Input ID?}
B -- "42" --> C[Data Ada]
C --> F[Sukses response]
B -- "99" --> D[Data Tidak Ada]
D --> G[Response Null]
B -- "" --> E[Invalid Argumen]
E --> H[Error Handling]
Start Writing the Table Test
Define a test table to keep things maintainable.
1func TestGetUserQuery_TableDriven(t *testing.T) {
2 schema := graphql.MustParseSchema(testSchema, &testResolver{})
3 tests := []struct {
4 name string
5 inputID string
6 expData string
7 hasError bool
8 }{
9 {"ID exists", "42", `{"getUser":{"id":"42","name":"Tester"}}`, false},
10 {"ID not found", "99", `{"getUser":null}`, false},
11 }
12
13 for _, tc := range tests {
14 t.Run(tc.name, func(t *testing.T) {
15 query := `
16 query ($id: ID!) {
17 getUser(id: $id) {
18 id
19 name
20 }
21 }
22 `
23 vars := map[string]interface{}{
24 "id": tc.inputID,
25 }
26 resp := schema.Exec(context.Background(), query, "GetUser", vars)
27 assert.JSONEq(t, tc.expData, string(resp.Data))
28 })
29 }
30}Testing Errors & Edge Cases
What happens if an argument isn’t sent?
Executing a query with a missing argument will return an error. Let’s simulate this in a test as well.
1func TestGetUserQuery_MissingArg(t *testing.T) {
2 schema := graphql.MustParseSchema(testSchema, &testResolver{})
3 query := `
4 query {
5 getUser { id name }
6 }
7 `
8 resp := schema.Exec(context.Background(), query, "", nil)
9 assert.NotNil(t, resp.Errors)
10 assert.Contains(t, resp.Errors[0].Message, "missing required argument")
11}Mocking Dependencies
A production resolver usually fetches data via a database/repository/another service. In unit tests, avoid depending on external resources!
Use the dependency injection pattern: the resolver accepts a service interface, and in the test, you inject a mock implementation.
1type UserService interface {
2 FindByID(ctx context.Context, id string) (*User, error)
3}
4
5type ResolverWithDep struct {
6 Svc UserService
7}
8
9func (r *ResolverWithDep) GetUser(ctx context.Context, args struct{ ID string }) (*User, error) {
10 return r.Svc.FindByID(ctx, args.ID)
11}
12
13// Mocking approach: you can use gomock/mockery, or just a simple dummy struct
14type mockUserSvc struct{}
15
16func (m *mockUserSvc) FindByID(ctx context.Context, id string) (*User, error) {
17 if id == "42" {
18 return &User{ID: "42", Name: "Arthur"}, nil
19 }
20 return nil, nil
21}
22
23// In the test:
24schema := graphql.MustParseSchema(testSchema, &ResolverWithDep{Svc: &mockUserSvc{}})Unit testing with mocks ensures the resolver is tested only on its logic, without depending on external services. This practice is important in a real world microservice.
Wrap-up: GraphQL Query Unit Test Checklist
Here is a checklist to make our testing solid in real projects:
- Successful query
- Query with data not found/null
- Invalid/missing argument
- Error simulation (e.g., dependency failure)
- Table test for maintainability
- Dependency isolation via mocks
- Check both data and errors in the response
Conclusion
Writing unit tests for queries in graphql-go is not difficult, but it is extremely crucial. Testing directly against the schema without HTTP saves time, makes refactoring easier, and increases confidence. With table driven tests, flow diagrams, and mocked dependencies, we’re ready to build robust and scalable GraphQL backends.
Never ship a GraphQL query without unit test coverage.
With solid coverage, we can sleep soundly — without fear of bugs quietly breaking production!
References:
See you in the next workshop. 🚀