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:
| Kelebihan | Kekurangan |
|---|---|
| Cepat – tidak ada latency jaringan | Tidak coverage code di level transport (TCP) |
| Tidak terganggu race pada port | Tidak catch bug config TLS/real network |
| Aman untuk paralel test | Perlu 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: