tutorial

  1. Test dengan bufconn tanpa Network

82. Test dengan bufconn tanpa Network

Saat membangun aplikasi berbasis gRPC, salah satu tantangan terbesar adalah melakukan testing secara reliable tanpa harus berurusan dengan kompleksitas jaringan yang nyata. Biasanya, test gRPC dilakukan dengan mensimulasikan server berjalan di port tertentu, lalu client mengirim request ke sana. Namun cara ini memiliki sejumlah keterbatasan, antara lain:

  • Lambat, karena harus benar-benar membuka socket network
  • Rentan terhadap race condition port, terutama jika dijalankan secara parallel
  • Mengganggu jika ada network firewall, resource limit, dsb

Di dunia Go, ada sebuah solusi elegan untuk masalah ini, yaitu: bufconn, singkatan dari buffered connection. Dengan bufconn, kita bisa membuat client dan server gRPC saling berkomunikasi lewat memory buffer (mirip pipe), tanpa benar-benar membuka koneksi jaringan. Artikel ini membahas konsep, praktik, kelebihan, kekurangan, hingga simulasi penggunaan bufconn untuk testing gRPC di Go.


Apa itu bufconn?

Secara sederhana, bufconn adalah package yang menyediakan implementasi net.Listener, namun transport-nya menggunakan memory buffer alih-alih jaringan. Ini memungkinkan dependency injection pada saat testing, cukup dengan mengganti listener server menjadi bufconn.Listen ketimbang net.Listen.

Ilustrasinya kira-kira seperti berikut:

flowchart LR
    A[gRPC Client] --(memory buffer)--> B[gRPC Server]
    subgraph Normal Network
        style A fill:#f9f,stroke:#333,stroke-width:2px
        style B fill:#bbf,stroke:#333,stroke-width:2px
    end
    A2[gRPC Client] --TCP socket--> B2[gRPC Server]
    subgraph Bufconn
        style A2 fill:#f6f,stroke:#333,stroke-width:2px,stroke-dasharray: 5 5
        style B2 fill:#bdf,stroke:#333,stroke-width:2px,stroke-dasharray: 5 5
    end

Pada diagram di atas, bagian kiri memperlihatkan test via memory buffer (bufconn), sedangkan kanan dengan network asli (TCP socket).


Kenapa Menggunakan bufconn?

Ada beberapa alasan mengapa sebaiknya mulai melirik bufconn di workflow testing Anda:

KelebihanKekurangan
Cepat – tidak ada latency jaringanTidak coverage code di level transport (TCP)
Tidak terganggu race pada portTidak catch bug config TLS/real network
Aman untuk paralel testPerlu sedikit setup kode tambahan
Mudah diintegrasikan dalam test Go (testing package)Tidak ideal untuk integration test yang butuh real env

Contoh Skenario: Testing gRPC Service Calculator

Sebagai ilustrasi, misalkan kita punya service gRPC sederhana bernama Calculator dengan satu method Add.

1. Definisi Service (proto)

// calculator.proto
syntax = "proto3";
package calculator;

service Calculator {
  rpc Add (AddRequest) returns (AddReply);
}

message AddRequest {
  int32 a = 1;
  int32 b = 2;
}

message AddReply {
  int32 result = 1;
}

Generate kode Go-nya (pastikan anda sudah install protoc-gen-go-grpc dan protoc-gen-go):

protoc --go_out=. --go-grpc_out=. calculator.proto

2. Implementasi Server Go

// calculator_server.go
package main

import (
  pb "path/to/calculatorpb"
  "context"
)

type calculatorServer struct {
  pb.UnimplementedCalculatorServer
}

func (s *calculatorServer) Add(ctx context.Context, req *pb.AddRequest) (*pb.AddReply, error) {
  sum := req.A + req.B
  return &pb.AddReply{Result: sum}, nil
}

3. Setup Testing dengan Bufconn

Mari kita menulis test integration tanpa jaringan:

// calculator_test.go
package main

import (
  "context"
  "log"
  "net"
  "testing"
  "google.golang.org/grpc"
  "google.golang.org/grpc/test/bufconn"

  pb "path/to/calculatorpb"
)

const bufSize = 1024 * 1024

var lis *bufconn.Listener

func bufDialer(context.Context, string) (net.Conn, error) {
  return lis.Dial()
}

func startTestGrpcServer() {
  lis = bufconn.Listen(bufSize)
  s := grpc.NewServer()
  pb.RegisterCalculatorServer(s, &calculatorServer{})
  go func() {
    if err := s.Serve(lis); err != nil {
      log.Fatalf("Server exited with: %v", err)
    }
  }()
}

func TestAdd_WithBufconn(t *testing.T) {
  startTestGrpcServer()

  ctx := context.Background()
  conn, err := grpc.DialContext(
    ctx,
    "bufnet",
    grpc.WithContextDialer(bufDialer),
    grpc.WithInsecure(),
  )
  if err != nil {
    t.Fatalf("Failed to dial bufnet: %v", err)
  }
  defer conn.Close()

  client := pb.NewCalculatorClient(conn)
  resp, err := client.Add(ctx, &pb.AddRequest{A: 10, B: 30})
  if err != nil {
    t.Fatalf("Add failed: %v", err)
  }
  if resp.Result != 40 {
    t.Fatalf("Expected 40, got %d", resp.Result)
  }
}

4. Simulasi Eksekusi Test

Mari lihat flow internal ketika test dijalankan:

sequenceDiagram
    participant T as Test Runner (Go `testing`)
    participant C as gRPC Client
    participant S as gRPC Server (di memory buffer)

    T->>S: Jalankan gRPC server pada bufconn.Listener
    T->>C: Buat koneksi gRPC client via bufDialer
    C->>S: Kirim request Add(10, 30) (via buffer memory)
    S->>S: Proses & balas AddReply(40)
    S->>C: Balas reply ke client (via buffer)
    C->>T: Hasil dikembalikan ke test runner

Keuntungan terbesar: Test berjalan full di memory, cepat, tanpa butuh port, bebas gangguan race/OS.


Tips Production Use

  • Gunakan bufconn hanya untuk unit/integration test, bukan real production!
  • Cocok untuk testing service yang sangat banyak dependensi network atau sulit mockup.
  • Untuk test dengan interaksi sungguhan (TLS, firewall), tetap butuh e2e test via real socket.

Kesimpulan

Testing dengan bufconn membuka cara baru untuk speed up dan merapikan ekosistem test gRPC. Kita tak lagi harus woro-woro soal port bentrok atau race condition pada integration test. Cukup injeksi bufconn.Listener, dan seluruh tes cukup berjalan di memory tanpa satu byte pun dilepas ke jaringan asli.

Summary benefit menggunakan bufconn pada testing gRPC:

  • Eliminasi dependency jaringan
  • Meningkatkan determinasi dan kecepatan test
  • Aman untuk paralel test dan CI/CD pipeline

Ingat: Gunakan bufconn untuk level integration/unit test, tapi jangan lupakan e2e test pada env sungguhan untuk coverage penuh.

Selamat mencoba bufconn di workflow gRPC testing Anda! 🚀


Referensi Lanjut:

comments powered by Disqus