[teamai] Push 87 resource(s) from XingfenD
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
# Protobuf & Code Generation Reference
|
||||
|
||||
## Directory Layout
|
||||
|
||||
Organize proto files by domain with versioned directories. Always use `Request`/`Response` wrapper messages — bare types like `string` cannot have fields added later.
|
||||
|
||||
```
|
||||
proto/
|
||||
├── user/v1/
|
||||
│ ├── user.proto # Messages
|
||||
│ └── user_service.proto # Service RPCs
|
||||
├── order/v1/
|
||||
│ ├── order.proto
|
||||
│ └── order_service.proto
|
||||
└── shared/v1/
|
||||
└── common.proto # Pagination, timestamps, shared enums
|
||||
```
|
||||
|
||||
## Proto File Conventions
|
||||
|
||||
```protobuf
|
||||
syntax = "proto3";
|
||||
package mycompany.user.v1;
|
||||
option go_package = "github.com/mycompany/myservice/gen/user/v1;userv1";
|
||||
|
||||
service UserService {
|
||||
rpc GetUser(GetUserRequest) returns (GetUserResponse);
|
||||
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
|
||||
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
|
||||
rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse);
|
||||
rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);
|
||||
}
|
||||
|
||||
message GetUserRequest {
|
||||
string user_id = 1;
|
||||
}
|
||||
|
||||
message GetUserResponse {
|
||||
User user = 1;
|
||||
}
|
||||
|
||||
message ListUsersRequest {
|
||||
int32 page_size = 1;
|
||||
string page_token = 2;
|
||||
}
|
||||
|
||||
message ListUsersResponse {
|
||||
repeated User users = 1;
|
||||
string next_page_token = 2;
|
||||
}
|
||||
```
|
||||
|
||||
### `go_package` conventions
|
||||
|
||||
- Format: `"import/path;alias"` — the alias becomes the Go package name
|
||||
- Convention: lowercase version suffix (e.g. `userv1`, `orderv1`)
|
||||
- Generate into a `gen/` directory to keep generated code separate from hand-written code
|
||||
|
||||
## Code Generation with `protoc`
|
||||
|
||||
```bash
|
||||
# Basic generation
|
||||
protoc --go_out=gen --go_opt=paths=source_relative \
|
||||
--go-grpc_out=gen --go-grpc_opt=paths=source_relative \
|
||||
proto/user/v1/*.proto
|
||||
|
||||
# With validation (using buf-validate)
|
||||
protoc --go_out=gen --go_opt=paths=source_relative \
|
||||
--go-grpc_out=gen --go-grpc_opt=paths=source_relative \
|
||||
--validate_out="lang=go:gen" \
|
||||
proto/user/v1/*.proto
|
||||
|
||||
# Include external imports
|
||||
protoc -I proto -I third_party \
|
||||
--go_out=gen --go_opt=paths=source_relative \
|
||||
--go-grpc_out=gen --go-grpc_opt=paths=source_relative \
|
||||
proto/user/v1/*.proto
|
||||
```
|
||||
|
||||
### Common `protoc` flags
|
||||
|
||||
| Flag | Purpose |
|
||||
| -------------------------------- | --------------------------------------- |
|
||||
| `--go_out=DIR` | Output directory for message types |
|
||||
| `--go-grpc_out=DIR` | Output directory for service stubs |
|
||||
| `--go_opt=paths=source_relative` | Place output relative to proto source |
|
||||
| `-I DIR` | Add import path for proto dependencies |
|
||||
| `--descriptor_set_out=FILE` | Emit binary descriptor (for reflection) |
|
||||
|
||||
## Code Generation with `buf`
|
||||
|
||||
`buf` is the recommended modern alternative to raw `protoc`. It manages dependencies, lints protos, and generates code from a single config.
|
||||
|
||||
### `buf.gen.yaml`
|
||||
|
||||
```yaml
|
||||
version: v2
|
||||
plugins:
|
||||
- remote: buf.build/protocolbuffers/go
|
||||
out: gen
|
||||
opt: paths=source_relative
|
||||
- remote: buf.build/grpc/go
|
||||
out: gen
|
||||
opt: paths=source_relative
|
||||
```
|
||||
|
||||
### `buf.yaml`
|
||||
|
||||
```yaml
|
||||
version: v2
|
||||
modules:
|
||||
- path: proto
|
||||
lint:
|
||||
use:
|
||||
- STANDARD
|
||||
breaking:
|
||||
use:
|
||||
- FILE
|
||||
```
|
||||
|
||||
### Common `buf` commands
|
||||
|
||||
```bash
|
||||
buf generate # Generate code from buf.gen.yaml
|
||||
buf lint # Lint proto files
|
||||
buf breaking --against '.git#branch=main' # Check backward compatibility
|
||||
buf dep update # Update dependencies
|
||||
buf build # Validate proto files compile
|
||||
```
|
||||
|
||||
## Generated Code Patterns
|
||||
|
||||
After generation, import and use the generated code:
|
||||
|
||||
```go
|
||||
import (
|
||||
userv1 "github.com/mycompany/myservice/gen/user/v1"
|
||||
)
|
||||
|
||||
// Server: implement the interface
|
||||
type userServer struct {
|
||||
userv1.UnimplementedUserServiceServer
|
||||
}
|
||||
|
||||
// Client: use the generated client
|
||||
client := userv1.NewUserServiceClient(conn)
|
||||
resp, err := client.GetUser(ctx, &userv1.GetUserRequest{UserId: "123"})
|
||||
```
|
||||
|
||||
Always embed `Unimplemented*Server` (not `Unsafe*Server`) — it provides forward compatibility when new RPCs are added to the proto definition.
|
||||
@@ -0,0 +1,270 @@
|
||||
# gRPC Testing Reference
|
||||
|
||||
## Testing with `bufconn`
|
||||
|
||||
`bufconn` creates in-memory connections that exercise the full gRPC stack (serialization, interceptors, metadata) without network overhead. This is the standard approach for gRPC unit and integration tests.
|
||||
|
||||
### Basic Setup
|
||||
|
||||
```go
|
||||
func setupTest(t *testing.T) pb.UserServiceClient {
|
||||
t.Helper()
|
||||
lis := bufconn.Listen(1024 * 1024)
|
||||
t.Cleanup(func() { lis.Close() })
|
||||
|
||||
srv := grpc.NewServer()
|
||||
pb.RegisterUserServiceServer(srv, newTestService())
|
||||
t.Cleanup(func() { srv.Stop() })
|
||||
go srv.Serve(lis)
|
||||
|
||||
conn, err := grpc.NewClient("passthrough:///bufconn",
|
||||
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
|
||||
return lis.DialContext(ctx)
|
||||
}),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("dial: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
return pb.NewUserServiceClient(conn)
|
||||
}
|
||||
```
|
||||
|
||||
### Setup with Interceptors
|
||||
|
||||
Test your interceptors by including them in the test server:
|
||||
|
||||
```go
|
||||
func setupTestWithInterceptors(t *testing.T) pb.UserServiceClient {
|
||||
t.Helper()
|
||||
lis := bufconn.Listen(1024 * 1024)
|
||||
t.Cleanup(func() { lis.Close() })
|
||||
|
||||
srv := grpc.NewServer(
|
||||
grpc.ChainUnaryInterceptor(
|
||||
loggingInterceptor,
|
||||
authInterceptor,
|
||||
recoveryInterceptor,
|
||||
),
|
||||
)
|
||||
pb.RegisterUserServiceServer(srv, newTestService())
|
||||
t.Cleanup(func() { srv.Stop() })
|
||||
go srv.Serve(lis)
|
||||
|
||||
conn, err := grpc.NewClient("passthrough:///bufconn",
|
||||
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
|
||||
return lis.DialContext(ctx)
|
||||
}),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("dial: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
return pb.NewUserServiceClient(conn)
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Error Codes
|
||||
|
||||
Always verify that RPCs return the expected gRPC status codes — clients rely on codes for retry and error-handling logic.
|
||||
|
||||
```go
|
||||
func TestGetUser_NotFound(t *testing.T) {
|
||||
client := setupTest(t)
|
||||
_, err := client.GetUser(context.Background(), &pb.GetUserRequest{UserId: "nonexistent"})
|
||||
|
||||
st, ok := status.FromError(err)
|
||||
if !ok {
|
||||
t.Fatalf("expected gRPC status error, got: %v", err)
|
||||
}
|
||||
if st.Code() != codes.NotFound {
|
||||
t.Errorf("expected NotFound, got %s: %s", st.Code(), st.Message())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Table-Driven Error Code Tests
|
||||
|
||||
```go
|
||||
func TestGetUser_Errors(t *testing.T) {
|
||||
client := setupTest(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req *pb.GetUserRequest
|
||||
wantCode codes.Code
|
||||
}{
|
||||
{
|
||||
name: "empty user ID",
|
||||
req: &pb.GetUserRequest{UserId: ""},
|
||||
wantCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "user not found",
|
||||
req: &pb.GetUserRequest{UserId: "nonexistent"},
|
||||
wantCode: codes.NotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := client.GetUser(context.Background(), tt.req)
|
||||
st, ok := status.FromError(err)
|
||||
if !ok {
|
||||
t.Fatalf("expected gRPC status error, got: %v", err)
|
||||
}
|
||||
if st.Code() != tt.wantCode {
|
||||
t.Errorf("code = %s, want %s; message: %s", st.Code(), tt.wantCode, st.Message())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Streaming RPCs
|
||||
|
||||
### Server Streaming
|
||||
|
||||
```go
|
||||
func TestListUsers_Stream(t *testing.T) {
|
||||
client := setupTest(t)
|
||||
stream, err := client.ListUsers(context.Background(), &pb.ListUsersRequest{})
|
||||
if err != nil {
|
||||
t.Fatalf("ListUsers: %v", err)
|
||||
}
|
||||
|
||||
var users []*pb.User
|
||||
for {
|
||||
user, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Recv: %v", err)
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
if len(users) != 3 {
|
||||
t.Errorf("got %d users, want 3", len(users))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Client Streaming
|
||||
|
||||
```go
|
||||
func TestBatchCreate_ClientStream(t *testing.T) {
|
||||
client := setupTest(t)
|
||||
stream, err := client.BatchCreateUsers(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("BatchCreateUsers: %v", err)
|
||||
}
|
||||
|
||||
for _, u := range testUsers {
|
||||
if err := stream.Send(u); err != nil {
|
||||
t.Fatalf("Send: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := stream.CloseAndRecv()
|
||||
if err != nil {
|
||||
t.Fatalf("CloseAndRecv: %v", err)
|
||||
}
|
||||
if resp.Created != int32(len(testUsers)) {
|
||||
t.Errorf("created = %d, want %d", resp.Created, len(testUsers))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Metadata
|
||||
|
||||
Verify that interceptors correctly read/write metadata:
|
||||
|
||||
```go
|
||||
func TestAuth_Metadata(t *testing.T) {
|
||||
client := setupTestWithInterceptors(t)
|
||||
|
||||
// Without auth token → Unauthenticated
|
||||
_, err := client.GetUser(context.Background(), &pb.GetUserRequest{UserId: "1"})
|
||||
if st, _ := status.FromError(err); st.Code() != codes.Unauthenticated {
|
||||
t.Errorf("expected Unauthenticated without token, got %s", st.Code())
|
||||
}
|
||||
|
||||
// With valid token → success
|
||||
md := metadata.Pairs("authorization", "Bearer valid-token")
|
||||
ctx := metadata.NewOutgoingContext(context.Background(), md)
|
||||
resp, err := client.GetUser(ctx, &pb.GetUserRequest{UserId: "1"})
|
||||
if err != nil {
|
||||
t.Fatalf("expected success with valid token: %v", err)
|
||||
}
|
||||
if resp.User == nil {
|
||||
t.Error("expected user in response")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Deadlines
|
||||
|
||||
```go
|
||||
func TestGetUser_DeadlineExceeded(t *testing.T) {
|
||||
client := setupTest(t) // server handler sleeps for 5s
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
_, err := client.GetUser(ctx, &pb.GetUserRequest{UserId: "slow"})
|
||||
st, _ := status.FromError(err)
|
||||
if st.Code() != codes.DeadlineExceeded {
|
||||
t.Errorf("expected DeadlineExceeded, got %s", st.Code())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Integration Test Patterns
|
||||
|
||||
For tests that hit real dependencies (database, external services), use build tags to separate them:
|
||||
|
||||
```go
|
||||
//go:build integration
|
||||
|
||||
func TestUserService_Integration(t *testing.T) {
|
||||
// Connect to real gRPC server
|
||||
conn, err := grpc.NewClient("localhost:50051",
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("dial: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := pb.NewUserServiceClient(conn)
|
||||
|
||||
// Test full roundtrip
|
||||
created, err := client.CreateUser(context.Background(), &pb.CreateUserRequest{
|
||||
Name: "integration-test",
|
||||
Email: "test@example.com",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateUser: %v", err)
|
||||
}
|
||||
|
||||
got, err := client.GetUser(context.Background(), &pb.GetUserRequest{
|
||||
UserId: created.User.Id,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GetUser: %v", err)
|
||||
}
|
||||
if got.User.Name != "integration-test" {
|
||||
t.Errorf("name = %q, want %q", got.User.Name, "integration-test")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Run integration tests separately:
|
||||
|
||||
```bash
|
||||
go test -tags=integration ./...
|
||||
```
|
||||
Reference in New Issue
Block a user