tutorial

  1. Studi Kasus: Testing Layanan CRUD dengan gRPC

86. Studi Kasus: Testing Layanan CRUD dengan gRPC

Dewasa ini, pengembangan aplikasi skala besar cenderung mengadopsi arsitektur microservices serta protokol komunikasi yang efisien, seperti gRPC. Banyak perusahaan menjadikan gRPC pilihan utama karena performa tinggi dan schema-driven API menggunakan Protocol Buffers. Namun, tantangan terbesar yang kerap muncul adalah bagaimana melakukan pengujian (testing) dengan efektif, khususnya untuk layanan CRUD (Create, Read, Update, Delete).

Pada artikel ini, saya ingin mengajak Anda menyelami sebuah studi kasus nyata mengenai testing layanan CRUD berbasis gRPC. Saya akan membahas struktur layanan, strategi pengujian unit, integration testing, hingga automatisasi pengujian. Simulasi kode akan menggunakan bahasa Go (Golang) karena popularitasnya dalam ekosistem gRPC.


Latar Belakang Masalah

Bayangkan sebuah tim mengembangkan service BookService yang menawarkan operasi CRUD pada entitas Book. Layanan ini harus cepat, andal, dan mudah untuk diuji. Pengujian otomatis diperlukan demi memastikan perubahan kode tidak menimbulkan regresi. Tes juga harus mudah dijalankan, baik di lingkungan lokal maupun CI/CD pipeline.

Diagram Alur CRUD di BookService

flowchart TD
    C(Client) -->|CreateRequest| S(BookService)
    C(Client) -->|ReadRequest| S(BookService)
    C(Client) -->|UpdateRequest| S(BookService)
    C(Client) -->|DeleteRequest| S(BookService)
    subgraph S
      direction TB
      DB[(Database)]
    end
    S --> DB

Definisi Protobuf

Langkah pertama dalam pengembangan layanan gRPC adalah mendeskripsikan API dengan protobuf:

syntax = "proto3";

package book;

service BookService {
    rpc CreateBook (Book) returns (BookId) {}
    rpc GetBook (BookId) returns (Book) {}
    rpc UpdateBook (Book) returns (Book) {}
    rpc DeleteBook (BookId) returns (Empty) {}
}

message Book {
    string id = 1;
    string title = 2;
    string author = 3;
}

message BookId {
    string id = 1;
}

message Empty {}

Dengan skema ini, struktur API jelas, mudah didokumentasi dan dikonsumsi oleh klien lintas bahasa.


Struktur Proyek Go

Mari simulasikan struktur direktori sederhana untuk BookService.

bookservice/
  ├── proto/
  │   └── book.proto
  ├── main.go
  ├── server.go
  ├── repository.go
  └── repository_test.go

Implementasi Server Sederhana

Bagian server akan menghandle request CRUD. Repository di-abstract agar memudahkan testing:

// repository.go
type Book struct {
    ID     string
    Title  string
    Author string
}

type BookRepository interface {
    Create(book Book) (string, error)
    Get(id string) (Book, error)
    Update(book Book) error
    Delete(id string) error
}

type InMemoryBookRepo struct {
    books map[string]Book
}

func (r *InMemoryBookRepo) Create(book Book) (string, error) {
    r.books[book.ID] = book
    return book.ID, nil
}

func (r *InMemoryBookRepo) Get(id string) (Book, error) {
    if book, ok := r.books[id]; ok {
        return book, nil
    }
    return Book{}, errors.New("not found")
}

func (r *InMemoryBookRepo) Update(book Book) error {
    if _, ok := r.books[book.ID]; ok {
        r.books[book.ID] = book
        return nil
    }
    return errors.New("not found")
}

func (r *InMemoryBookRepo) Delete(id string) error {
    if _, ok := r.books[id]; ok {
        delete(r.books, id)
        return nil
    }
    return errors.New("not found")
}

Pengujian Unit: Fokus pada Repository

Sebelum menguji service penuh, pengujian unit pada repository penting dilakukan. Tujuannya untuk memastikan logika dasar CRUD sudah benar, independen dari gRPC. Berikut contoh test case menggunakan testing package di Go:

// repository_test.go
func TestInMemoryBookRepo_CRUD(t *testing.T) {
    repo := &InMemoryBookRepo{books: map[string]Book{}}
    book := Book{ID: "1", Title: "Go in Action", Author: "John Doe"}
    
    // Test Create
    id, err := repo.Create(book)
    if err != nil || id != "1" {
        t.Errorf("Create failed, got id=%v, err=%v", id, err)
    }

    // Test Read
    b, err := repo.Get("1")
    if err != nil || b.Title != "Go in Action" {
        t.Errorf("Get failed, got book=%v, err=%v", b, err)
    }

    // Test Update
    book.Title = "Go Lang in Action"
    err = repo.Update(book)
    if err != nil {
        t.Errorf("Update failed: %v", err)
    }

    // Test Delete
    err = repo.Delete("1")
    if err != nil {
        t.Errorf("Delete failed: %v", err)
    }
}

Manfaat Pengujian Unit

  • Isolasi: Memastikan komponen repository berjalan seperti yang diharapkan, terlepas dari protokol komunikasi.
  • Kecepatan: Test berjalan sangat cepat.

Integration Testing dengan gRPC

Selanjutnya, kita perlu menguji apakah service server gRPC benar-benar meng-translate request menjadi operasi CRUD yang benar.

Setup Integration Test

  1. Spin up server gRPC dengan repository in-memory.
  2. Kirim request ke server menggunakan client gRPC secara langsung.
// integration_test.go (pseudo)
func TestBookServiceIntegration(t *testing.T) {
    // 1. Jalankan server (tanpa perlu listen di port production)
    lis := bufconn.Listen(1024 * 1024)
    s := grpc.NewServer()
    repo := &InMemoryBookRepo{books: map[string]Book{}}
    RegisterBookServiceServer(s, &BookServiceServer{repo: repo})
    go s.Serve(lis)
    defer s.Stop()

    // 2. Set client dengan custom dial
    ctx := context.Background()
    conn, _ := grpc.DialContext(
        ctx, "bufnet",
        grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
            return lis.Dial()
        }),
        grpc.WithInsecure(),
    )
    defer conn.Close()
    client := NewBookServiceClient(conn)

    // 3. Proses uji integrasi end-to-end
    resp, err := client.CreateBook(ctx, &Book{Id: "123", Title: "gRPC for Dummies", Author: "Jane"})
    // 4. Assert validations
    if err != nil || resp.Id != "123" {
        t.Fatalf("Expected Id=123, got %v, err=%v", resp.Id, err)
    }
}

Tabel Hasil Pengujian Skenario CRUD

OperasiInputOutput / EkspetasiStatus
Createid=123, title=“gRPC for Dummies”Success, id=123Pass
Readid=123Book ditemukanPass
Updateid=123, title=“gRPC 101”Book diperbaruiPass
Deleteid=123Book dihapusPass
Readid=999 (belum ada)Error: not foundPass

Automasi Testing di CI/CD Pipeline

Testing service gRPC di atas bisa langsung diotomasi dalam pipeline CI-CD (misal dengan Github Actions atau Gitlab CI). Kunci utama keberhasilan: tidak ada dependency ke infrastruktur eksternal, servis bisa spin-up dan testing sendiri dengan dependency in-memory.

Tips Otomasi Testing:

  • Gunakan test double untuk resource eksternal (e.g. in-memory or mock).
  • Pastikan coverage test minimal untuk seluruh fitur CRUD.
  • Pisahkan test unit vs integration (misal dengan suffix _test.go).

Key Takeaways

  • gRPC sangat powerful untuk layanan high performance dan schema-first.
  • Testing CRUD wajib dilakukan di dua level: unit (logic) dan integration (antarmuka gRPC).
  • Strategy dengan in-memory repository memudahkan isolasi dan otomasi test di pipeline.
  • Test coverage tidak boleh hanya di operation “bahagia” (happy case), tapi juga kasus limit & error.

Kesimpulan

Testing layanan CRUD berbasis gRPC bisa dibuat sederhana dan powerful dengan strategi modularisasi komponen, penulisan test unit & integration yang sistematis, serta mengandalkan in-memory dependency. Dengan pendekatan ini error lebih cepat ditemukan, development velocity meningkat, serta codebase lebih tangguh menghadapi perubahan.

Secara praktis, model seperti ini mudah direplikasi untuk layanan microservice lain, dan membangun fondasi engineering excellence yang kuat di tim pengembang Anda.


Referensi:


Semoga studi kasus ini menambah insight Anda dalam membangun dan menguji layanan CRUD gRPC yang scalable dan reliable!

comments powered by Disqus