How to Write Automated Tests for a Golang and GraphQL API

Photo by Chris Ried on Unsplash

How to Write Automated Tests for a Golang and GraphQL API

Automated testing is a critical aspect of software development, ensuring that your application behaves as expected and changes to the codebase do not introduce new bugs. In this article, we’ll explore how to write automated tests for a Golang application that integrates with a GraphQL API. We’ll cover unit tests, integration tests, and testing strategies tailored to GraphQL and Go’s idiomatic approach.


Setting Up Your Testing Environment

Before writing automated tests, ensure your Golang project is well-structured, with modular components that make testing easier. Common prerequisites include:

  1. Testing Libraries:

  2. GraphQL Client Library:

  3. Test Server:

    • Use httptest to simulate HTTP requests and responses without requiring an actual server instance.

Unit Testing in Golang

Unit tests focus on individual functions and ensure each one works correctly in isolation.

Example: Testing a GraphQL Query Handler

Here’s a simple test for a function that handles GraphQL queries.

package main

import (
    "bytes"
    "net/http"
    "net/http/httptest"
    "testing"
)

// Mock GraphQL handler
func mockGraphQLHandler(w http.ResponseWriter, r *http.Request) {
    response := `{"data": {"message": "Hello, World!"}}`
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(response))
}

func TestGraphQLQueryHandler(t *testing.T) {
    reqBody := `{"query": "query { message }"}`
    req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBufferString(reqBody))
    req.Header.Set("Content-Type", "application/json")

    // Use httptest.ResponseRecorder to capture the response
    rec := httptest.NewRecorder()
    handler := http.HandlerFunc(mockGraphQLHandler)
    handler.ServeHTTP(rec, req)

    // Check status code
    if status := rec.Code; status != http.StatusOK {
        t.Fatalf("Expected status code %d, but got %d", http.StatusOK, status)
    }

    // Check response body
    expected := `{"data": {"message": "Hello, World!"}}`
    if rec.Body.String() != expected {
        t.Fatalf("Expected body %s, but got %s", expected, rec.Body.String())
    }
}

Integration Testing

Integration tests validate how different components of your application work together. For a GraphQL API, this might include ensuring schema resolvers interact correctly with data sources.

Example: Testing a GraphQL Resolver

Suppose you have a resolver that fetches data from a database.

Schema:

type Query {
    user(id: ID!): User
}

type User {
    id: ID!
    name: String!
}

Resolver:

func userResolver(params graphql.ResolveParams) (interface{}, error) {
    id := params.Args["id"].(string)
    return fetchUserByID(id) // fetchUserByID fetches data from a mock DB
}

Integration Test:

func TestUserResolver(t *testing.T) {
    query := `{"query": "query { user(id: \"1\") { id, name } }"}`
    req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBufferString(query))
    req.Header.Set("Content-Type", "application/json")

    rec := httptest.NewRecorder()
    gqlHandler := graphql.NewHandler(graphql.HandlerConfig{
        Schema: graphql.MustParseSchema(schema, &Resolver{}),
    })
    gqlHandler.ServeHTTP(rec, req)

    if status := rec.Code; status != http.StatusOK {
        t.Fatalf("Expected status code %d, but got %d", http.StatusOK, status)
    }

    expected := `{"data": {"user": {"id": "1", "name": "Alice"}}}`
    if rec.Body.String() != expected {
        t.Fatalf("Expected response %s, but got %s", expected, rec.Body.String())
    }
}

Mocking GraphQL Responses

For scenarios where the actual GraphQL server or database isn’t available, use mocking techniques. Libraries like github.com/99designs/gqlgen make it easy to mock resolvers.

Example:

type mockResolver struct{}

func (m *mockResolver) Query_user(ctx context.Context, id string) (*User, error) {
    return &User{ID: id, Name: "Mock User"}, nil
}

// Test using mock resolver
func TestMockResolver(t *testing.T) {
    // Similar setup to previous integration tests
}

Testing Strategies

  1. Test Coverage:

    • Prioritize testing schema validation, query/mutation execution, and error handling.
  2. Use a Test Database:

    • For integration tests involving data, use a separate test database to isolate test data.
  3. Mock External Services:

    • Replace external dependencies with mocks or stubs to ensure tests are not flaky.
  4. Run Tests in CI/CD:

    • Automate tests with tools like GitHub Actions, CircleCI, or Jenkins.

Conclusion

Automated testing for Golang applications with GraphQL APIs ensures reliability and confidence in your codebase. By combining unit, integration, and mocking strategies, you can cover a wide range of scenarios and edge cases. Start with small, focused tests and gradually expand to cover complex workflows. This approach not only prevents regressions but also accelerates development by catching issues early in the process.