tutorial

42 Membuat Unit Test Query di graphql-go

42 Membuat Unit Test Query di graphql-go

Sebagai pengembang aplikasi backend modern, GraphQL bukanlah hal baru buat kita. Flexibilitas query, efisiensi data, dan self-documentation jadi alasan banyak tim beralih menggunakan GraphQL. Di ekosistem Go, library graphql-go menjadi pilihan populer. Namun, satu hal sering terlewatkan dalam penerapan GraphQL di Go: Unit Testing.
Unit test bukan sekedar formalitas. Ia jadi insurance policy terhadap bug di masa depan, terutama pada query yang jadi jembatan utama antara client dan server. Artikel ini akan membedah langkah demi langkah cara membuat unit test pada query di graphql-go, lengkap dengan contoh kode, simulasi mock data, dan best practice.


Mengapa Query di Unit Test?

Table : Manfaat Unit Test pada Query GraphQL

ManfaatPenjelasan
Menjamin CorrectnessPastikan struktur response dan resolusi data benar
Mendukung RefactoringMenambah/mengubah resolver aman karena tester siap menangkap regression
Dokumentasi HidupKonsumen backend tahu use case dan responsenya dengan membaca test case
Testing Error HandlingSimulasi skenario error (contoh: unauthorized, data kosong) mudah dilakukan

GraphQL-Go: Instalasi dan Skema Dasar

Sebelum masuk ke unit test, mari setup skema sederhana GraphQL di Go. Supaya fokus, kita buat query getUser(id: ID!): User.

type User struct {
    ID   string
    Name string
}

type Resolver struct{}

func (r *Resolver) GetUser(ctx context.Context, args struct{ ID string }) (*User, error) {
    // Skenario: Ambil user by ID atau error jika tidak ditemukan
    if args.ID == "42" {
        return &User{ID: "42", Name: "Arthur Dent"}, nil
    }
    return nil, errors.New("user not found")
}

// schema.graphql
/*
type User {
    id: ID!
    name: String!
}

type Query {
    getUser(id: ID!): User
}
*/

Integrasi ke server biasanya seperti ini:

schema := graphql.MustParseSchema(schemaString, &Resolver{})
http.Handle("/query", &relay.Handler{Schema: schema})

Dasar Unit Testing Query di graphql-go

Unit test pada graphql-go tidak mengakses endpoint HTTP. Ia langsung invoke resolver melalui schema, passing query dan variables. Keuntungannya, unit test bisa berjalan cepat dan isolated dari middleware/auth/network.

Dependensi

Tambahkan di go.mod:

require (
    github.com/graph-gophers/graphql-go v1.7.0
    github.com/stretchr/testify v1.8.0
)

Membuat Unit Test Sederhana

Mari buat file test: resolver_test.go.
Ingat, unit test Go wajib pakai function TestXxx(t *testing.T) agar dikenali oleh go test.

package main

import (
    "context"
    "testing"

    "github.com/graph-gophers/graphql-go"
    "github.com/stretchr/testify/assert"
)

// Sederhanakan skema untuk test
const testSchema = `
type User {
    id: ID!
    name: String!
}

type Query {
    getUser(id: ID!): User
}
`

// Implementasi resolver tiruan untuk test
type testResolver struct{}

func (r *testResolver) GetUser(ctx context.Context, args struct{ ID string }) (*User, error) {
    if args.ID == "42" {
        return &User{ID: "42", Name: "Tester"}, nil
    }
    return nil, nil
}

func TestGetUserQuery(t *testing.T) {
    schema := graphql.MustParseSchema(testSchema, &testResolver{})

    query := `
        query ($id: ID!) {
            getUser(id: $id) {
                id
                name
            }
        }
    `
    vars := map[string]interface{}{
        "id": "42",
    }

    // Eksekusi query langsung ke skema (bukan HTTP)
    response := schema.Exec(context.Background(), query, "GetUser", vars)

    // status code selalu 200, cek bagian data & errornya
    expected := `{"getUser":{"id":"42","name":"Tester"}}`
    assert.JSONEq(t, expected, string(response.Data))
    assert.Nil(t, response.Errors)
}

Simulasi Unit Test dengan Data Berbeda

Tes harus mencakup:

  • Query data sukses
  • Query data tidak ditemukan (should return nil)
  • Query invalid argumen

Skema Simulasi

Berikut simulasi case branching yang diharapkan:

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]

Mulai Tulis Table Test

Definisikan test table agar maintainable.

func TestGetUserQuery_TableDriven(t *testing.T) {
    schema := graphql.MustParseSchema(testSchema, &testResolver{})
    tests := []struct {
        name     string
        inputID  string
        expData  string
        hasError bool
    }{
        {"ID ada", "42", `{"getUser":{"id":"42","name":"Tester"}}`, false},
        {"ID tidak ada", "99", `{"getUser":null}`, false},
    }

    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            query := `
                query ($id: ID!) {
                    getUser(id: $id) {
                        id
                        name
                    }
                }
            `
            vars := map[string]interface{}{
                "id": tc.inputID,
            }
            resp := schema.Exec(context.Background(), query, "GetUser", vars)
            assert.JSONEq(t, tc.expData, string(resp.Data))
        })
    }
}

Menguji Error & Edge Case

Bagaimana jika argumen tidak dikirim?
Eksekusi query dengan missing argumen akan mengembalikan error. Simulasikan juga di test.

func TestGetUserQuery_MissingArg(t *testing.T) {
    schema := graphql.MustParseSchema(testSchema, &testResolver{})
    query := `
        query {
            getUser { id name }
        }
    `
    resp := schema.Exec(context.Background(), query, "", nil)
    assert.NotNil(t, resp.Errors)
    assert.Contains(t, resp.Errors[0].Message, "missing required argument")
}

Mocking Dependency

Resolver production biasanya ambil data via database/repository/service lain. Di unit test, hindari dependency pada resource eksternal!
Gunakan pattern dependency injection: resolver menerima interface service, dan di test, inject mock implementation.

type UserService interface {
    FindByID(ctx context.Context, id string) (*User, error)
}

type ResolverWithDep struct {
    Svc UserService
}

func (r *ResolverWithDep) GetUser(ctx context.Context, args struct{ ID string }) (*User, error) {
    return r.Svc.FindByID(ctx, args.ID)
}

// Mock teknologi: bisa pakai gomock/mockery, atau cukup dummy struct
type mockUserSvc struct{}

func (m *mockUserSvc) FindByID(ctx context.Context, id string) (*User, error) {
    if id == "42" {
        return &User{ID: "42", Name: "Arthur"}, nil
    }
    return nil, nil
}

// Dalam test:
schema := graphql.MustParseSchema(testSchema, &ResolverWithDep{Svc: &mockUserSvc{}})

Unit testing dengan mock memastikan resolver hanya diuji pada logic, tidak tergantung eksternal service. Praktik ini penting dalam real world microservice.


Penutup: Checklist Unit Test Query GraphQL

Berikut checklist agar testing kita solid di project nyata:

  • Query sukses
  • Query data tidak ditemukan/null
  • invalid/kurang argumen
  • Simulasi error (misal dependency failure)
  • Table test untuk maintainability
  • Isolasi dependency via mock
  • Cek data dan errors pada response

Kesimpulan

Membuat unit test untuk query di graphql-go tidak sulit tapi sangat krusial. Testing langsung ke skema tanpa HTTP menghemat waktu, memudahkan refactor, dan meningkatkan confidence. Dengan table driven test, diagram flow, serta mock dependency, kita siap membangun backend GraphQL yang kokoh dan scalable.

Jangan pernah meluncurkan query GraphQL tanpa ter-cover oleh unit test.
Dengan coverage solid, kita bisa tidur nyenyak — tanpa takut bug diam-diam merusak produksi!


Referensi:

Sampai jumpa di workshop selanjutnya. 🚀

comments powered by Disqus

Topik Terhangat

programming
263
tutorial
130
tips-and-trick
43
jaringan
28
hardware
11
linux
4
kubernetes
1