tutorial

123 Testing Resolver gqlgen dengan httptest dan Mock

123 Testing Resolver gqlgen dengan httptest dan Mock

GraphQL makin populer di ekosistem backend modern. Salah satu alasan utamanya adalah fleksibilitas dan efisiensi query-nya. Di Golang, gqlgen menawarkan tools powerful untuk mengelola GraphQL server, salah satunya meng-generate resolver dari schema. Tapi, tanpa testing yang bagus, kita riskan membuat bug dan behaviour yang tidak diharapkan. Nah, bagaimana sih cara efektif mengetes resolver GraphQL di project gqlgen? Di artikel ini, saya akan berbagi praktik 123 testing resolver gqlgen: dari httptest sampai mock dependency.

Kenapa Testing Resolver itu Penting?

Sebelum diving ke teknis, mari kita bahas mengapa testing resolver itu esensial.

AlasanPenjelasan Singkat
Validasi Logika BisnisMemastikan semua business logic berjalan sesuai requirement, terutama jika terjadi perubahan di resolver.
Refactoring AmanMenjamin perubahan kode (refactor/optimasi) tidak mengubah behaviour yang diinginkan dari resolver.
Error Handling TerujiMemastikan error scenarios, misal dependency error atau data tidak ditemukan, sudah di-handle dengan benar.
Dokumentasi HidupTest case berguna sebagai living documentation cara kerja resolver, terutama untuk engineer baru di proyek.

Skema Arsitektur GraphQL dengan gqlgen

Mari kita refresh sederhana arsitekturnya:

flowchart TD
    Client -->|GraphQL Query| HTTPHandler
    HTTPHandler -->|GraphQL Request| Resolver
    Resolver -->|Access| Service
    Service -->|Access| DataSource[Database / API / etc]

1. Client menggunakan HTTP mengirim GraphQL Query

2. gqlgen HTTP Handler menerima dan men-parse request

3. Resolver resolve data dengan memanggil Service

4. Service mengambil data dari DataSource

Studi Kasus: Resolver Query books

Bayangkan kita punya schema berikut:

type Book {
  id: ID!
  title: String!
  author: String!
}

type Query {
  books: [Book!]!
}

Dan resolvernya mengambil data dari service:

type BookService interface {
    GetBooks(ctx context.Context) ([]*Book, error)
}

Implementasi resolvernya:

func (r *queryResolver) Books(ctx context.Context) ([]*model.Book, error) {
    return r.BookService.GetBooks(ctx)
}

Tantangan Testing

  • Resolver tergantung service (BookService)
  • Ingin testing tanpa akses DB (isolasi unit test)
  • Ingin testing end-to-end (integrasi antar komponen)

123 Testing gqlgen Resolver di Golang

Kita akan bahas 3 level test paling efektif:

  1. Unit Test Resolver dengan Mock (Isolasi Service)
  2. Integration Test Handler pakai httptest
  3. End-to-End Test (optional, keluar scope, tapi akan saya mention)

1. Unit Test Resolver dengan Mock

Fokus ke business logic resolver dan isolasi dependency.

Simulasi: Mock Service

Kita bikin mock BookService pakai stretchr/testify/mock.

// book_service_mock.go
type BookServiceMock struct {
    mock.Mock
}

func (m *BookServiceMock) GetBooks(ctx context.Context) ([]*model.Book, error) {
    args := m.Called(ctx)
    return args.Get(0).([]*model.Book), args.Error(1)
}

Unit Test Resolver

func TestBooksResolver(t *testing.T) {
    mockSvc := new(BookServiceMock)
    dummyBooks := []*model.Book{
        {ID: "1", Title: "Clean Code", Author: "Robert Martin"},
        {ID: "2", Title: "The Pragmatic Programmer", Author: "Andy Hunt"},
    }
    mockSvc.On("GetBooks", mock.Anything).Return(dummyBooks, nil)

    resolver := &Resolver{BookService: mockSvc}

    ctx := context.Background()
    result, err := resolver.Query().Books(ctx)
    
    assert.NoError(t, err)
    assert.Equal(t, dummyBooks, result)
    mockSvc.AssertExpectations(t)
}

Kelebihan:

  • Sangat cepat, tidak bergantung infra.
  • Isolasi logic resolver.

Kekurangan:

  • Tidak mengetes serialization (GraphQL handler).
  • Tidak mengetes behaviour interaksi antar handler.

2. Integration Test GraphQL Handler dengan httptest

Fokus ke seluruh pipeline: HTTP handler, parsing, serialization, resolver, dan service.

func TestGraphQLBooksQuery(t *testing.T) {
    // Setup mock service
    mockSvc := new(BookServiceMock)
    expectedBooks := []*model.Book{
        {ID: "1", Title: "Domain-Driven Design", Author: "Eric Evans"},
        {ID: "2", Title: "Go in Action", Author: "William Kennedy"},
    }
    mockSvc.On("GetBooks", mock.Anything).Return(expectedBooks, nil)

    // Inject mock ke resolver
    resolver := &graph.Resolver{BookService: mockSvc}

    // Setup server GraphQL pakai gqlgen handler
    srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: resolver}))

    // Compose query
    query := `{"query": "{ books { id title author } }"}`
    req := httptest.NewRequest(http.MethodPost, "/query", strings.NewReader(query))
    req.Header.Set("Content-Type", "application/json")
    w := httptest.NewRecorder()

    srv.ServeHTTP(w, req)

    resp := w.Result()
    body, _ := io.ReadAll(resp.Body)
    assert.Equal(t, http.StatusOK, resp.StatusCode)

    // Parse response JSON
    var gqlResp struct {
        Data struct {
            Books []model.Book `json:"books"`
        } `json:"data"`
    }
    json.Unmarshal(body, &gqlResp)

    assert.Equal(t, len(expectedBooks), len(gqlResp.Data.Books))
    for i, b := range gqlResp.Data.Books {
        assert.Equal(t, expectedBooks[i].ID, b.ID)
        assert.Equal(t, expectedBooks[i].Title, b.Title)
        assert.Equal(t, expectedBooks[i].Author, b.Author)
    }
    mockSvc.AssertExpectations(t)
}

Gambaran Flow dengan mermaid

sequenceDiagram
    participant TestClient as Test (httptest)
    participant HTTPHandler
    participant Resolver
    participant BookServiceMock

    TestClient->>HTTPHandler: POST /query (GraphQL)
    HTTPHandler->>Resolver: resolve Query.books
    Resolver->>BookServiceMock: GetBooks(ctx)
    BookServiceMock-->>Resolver: Dummy Data
    Resolver-->>HTTPHandler: Data
    HTTPHandler-->>TestClient: JSON response

Kelebihan:

  • Menguji seluruh stack GraphQL handler.
  • Validasi marshalling/unmarshalling JSON.
  • Bisa test scenarios lebih realistis (error case, misal GetBooks return error).

Kekurangan:

  • Lebih lambat dari unit test.
  • Mock perlu di-maintain jika interface berubah.

3. End-to-End Test (Opsional)

Jika kita mau test integrasi dengan sistem lain (misal DB asli, Redis, dsb), end-to-end test bisa dilakukan. Biasanya pakai tool tambahan seperti docker-compose untuk spin up seluruh stack. Tapi, section ini di luar scope artikel kali ini.


Tabel Komparasi Cepat

Unit Test ResolverIntegration Test (httptest)
SpeedSangat cepatSedang
IsolasiYaPartial (Dependency mock)
CoverageLogic resolverEnd-to-end pipeline handler
Test DataDummy/mockDummy/mock
RealisticMediumHigh

Best Practice & Tips

  • Mock all external dependencies: Gunakan mock untuk service agar test deterministic.
  • Gunakan table-driven tests: Untuk test banyak scenario di 1 file.
  • Test error case: Pastikan ada test jika dependency error, return error.
  • Jangan gabungkan unit dan integration test: Pisahkan untuk maintainability.
  • Gunakan coverage tool: Cek coverage untuk kualitas tests.

Kesimpulan

Testing di gqlgen bukan lagi mimpi buruk. Dengan kombinasi httptest dan mock, kita bisa dengan mudah nge-test resolver secara isolated maupun end-to-end tanpa pusing setup environment berat. Mulailah dari unit test resolver, naik ke integration test pakai handler. Pastikan dependency di-mock, gunakan table-driven, dan selalu test happy maupun error case!

Happy testing! 🚀


Referensi:

Semoga artikel ini membantu memulai journey kamu menulis test di project gqlgen. Jangan lupa share jika bermanfaat!

comments powered by Disqus