惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Engineering at Meta
Engineering at Meta
人人都是产品经理
人人都是产品经理
大猫的无限游戏
大猫的无限游戏
博客园 - 三生石上(FineUI控件)
量子位
腾讯CDC
The Cloudflare Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
云风的 BLOG
云风的 BLOG
Vercel News
Vercel News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
L
LangChain Blog
aimingoo的专栏
aimingoo的专栏
The Hacker News
The Hacker News
T
The Exploit Database - CXSecurity.com
B
Blog
S
SegmentFault 最新的问题
P
Privacy & Cybersecurity Law Blog
T
Threatpost
博客园 - 聂微东
T
Tailwind CSS Blog
The Last Watchdog
The Last Watchdog
C
Check Point Blog
N
Netflix TechBlog - Medium
D
DataBreaches.Net
爱范儿
爱范儿
IT之家
IT之家
S
Secure Thoughts
M
MIT News - Artificial intelligence
NISL@THU
NISL@THU
C
Cisco Blogs
TaoSecurity Blog
TaoSecurity Blog
有赞技术团队
有赞技术团队
A
Arctic Wolf
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
P
Proofpoint News Feed
Spread Privacy
Spread Privacy
Schneier on Security
Schneier on Security
Simon Willison's Weblog
Simon Willison's Weblog
G
GRAHAM CLULEY
雷峰网
雷峰网
Project Zero
Project Zero
博客园 - Franky
H
Heimdal Security Blog
A
About on SuperTechFans
Security Latest
Security Latest
Webroot Blog
Webroot Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Hugging Face - Blog
Hugging Face - Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More

kmcd.dev

gRPC-Web Should Have Fixed gRPC Making Dynamic Protobuf Fast in Go Proxy, Record, and Mock gRPC APIs with FauxRPC Exploring Protocol Buffers Interactively Introducing ProtoDocs Ghost in the Shell: The Manga Behind the Anime The Hidden Cost of google.protobuf.Value Why Networking Built Its Own Data Modeling Language Zero-Friction Demos with WASM Let's Learn About BGP ConnectRPC: Where is it now? Building APIs with Contracts The Case for Greppable Code Unknown Fields in Protobuf IRC Log: Reactionary Faking protobuf data in Go Y'all are Sleeping on Mise-en-Place IRC Log: Standup 2 HTTP/2 From Scratch: Part 4 IRC Log: rm -rf /var/opt/gitlab/postgresql/data HTTP/2 From Scratch: Part 3 Building a Live BGP Map HTTP/2 From Scratch: Part 2 IRC Log: The Cloud Scale Incident Visualizing the Internet (2026) Shell Log: Namaste HTTP/2 From Scratch: Part 1 IRC Log: Standup HTTP/1.1 From Scratch WHOIS is dead, long live RDAP Months Considered Harmful Encryption vs. Compression On Creating My Own Cover Art Traceroute Tool from Scratch in Go My Favorite Interview Question From JSON to Protobuf Breaking gRPC Morse Code Can You Hack a Phone with Your Voice? Visualizing the Internet (2025) HTTP QUERY and Go I made a daily word game Protovalidate: Can Input Validation Be This Easy? Behold! The Barcode Scanner Mixing CEL and Protobuf for Fun FauxRPC and Protovalidate The Call of the Monolithic Codebase FauxRPC + Test Containers Self-Documenting Connect Services gRPC Over HTTP/3: Followup JSON to Protobuf Conversion gRPC: The Ugly Parts Working with Protobuf in 2024 Introducing FauxRPC HTTP/1.0 From Scratch Y'all are sleeping on HTTP/3 HTTP/0.9 From Scratch What version of HTTP are you using? Texans in Denmark gRPC Over HTTP/3 gRPC: The Good Parts Leaving Texas for Greener Pastures gRPC: The Bad Parts Daily Prompts Adding chart.js to Hugo Why I'm Rebranding Benchmarking gRPC (golang) Blog Update gRPC From Scratch: Part 3 - Protobuf Encoding Tracking the Wins Visualizing the Internet (2024) Dropping Unknown Fields in ConnectRPC RESTless: Web APIs After REST Introducing unknownconnect-go Making gRPC more approachable with ConnectRPC Inspecting Protobuf Messages Introducing protoc-gen-connect-openapi gRPC From Scratch: Part 2 - Server gRPC From Scratch: Part 1 - Client Why you should use gNMI over SNMP in 2026 The Rollercoaster of Productivity in Side Projects Lessons from a Decades-Long Project How I learned to code Economists with (virtual) Guns Visualizing the Internet (2023) softlayer-python: language bindings/CLI for a cloud company SwFTP: SFTP/FTP Server For Openstack Swift Video: Morning Copenhagen Commute Goodbye Evepraisal Visualizing the spectrum of the sun (Part 2) Visualizing the Internet (2022) Evepraisal: A price estimation tool for Eve Online Visualizing the spectrum of the sun
Unit Testing ConnectRPC Servers
2024-06-11 · via kmcd.dev

If you’ve embarked on the journey of building efficient and scalable RPC systems with ConnectRPC, you might be pondering the best way to ensure the reliability and correctness of your services. Unit testing is the obvious tool for this, providing a safety net that catches bugs early and empowers you to refactor code fearlessly. In the ConnectRPC world, unit testing can be daunting due to its integration with Protocol Buffers and the client-server architecture. In this guide, we’ll unravel the mysteries of unit testing ConnectRPC services, while arming you with practical examples and advanced techniques to fortify your codebase.

First off, the full source code can be found on github. If it helps, feel free to download, run, and modify as you see fit!

Why Unit Test?

Before we dive in, let’s address the “why.” Unit testing your ConnectRPC servers brings a multitude of benefits:

  • Isolation: Focus on testing individual components in isolation, making it easier to pinpoint and fix issues.
  • Speed: Unit tests execute quickly, providing fast feedback during development.
  • Refactoring Confidence: When you have solid unit tests, you can refactor your code with confidence, knowing that the tests will catch any unintended consequences.
  • Documentation: Well-written unit tests can serve as living documentation, illustrating how your code is meant to be used.
  • Bug Prevention: A good suite of unit tests can help you catch bugs early on, before they become harder and more expensive to fix.

Testing Strategies with ConnectRPC

ConnectRPC, built upon the Protocol Buffers ecosystem, offers a couple of primary approaches to unit testing:

  1. Direct Service Testing: This is ideal for unit testing but it’s not always possible. You directly call the methods of your service implementation (typically a struct in Go), bypassing any client and server networking.
  2. Server Testing: This approach creates an actual ConnectRPC server with net/http/httptest. It’s helpful when you want to test the interactions between your client and server code but is usually “overkill” unless you’re wanting to test interceptors or HTTP middleware.

Hands-On: Our example service

Here is the protobuf file that we’re using for our example:

syntax = "proto3";

package greet.v1;

option go_package = "example/gen/greet/v1;greetv1";

message GreetRequest {
  string name = 1;
}

message GreetResponse {
  string greeting = 1;
}

service GreetService {
  rpc Greet(GreetRequest) returns (GreetResponse) {}
}

And here is the resulting server implementation:

type greeterService struct{}

var _ greetv1connect.GreetServiceHandler = (*greeterService)(nil)

func (g *greeterService) Greet(ctx context.Context, req *connect.Request[greetv1.GreetRequest]) (*connect.Response[greetv1.GreetResponse], error) {
	if req.Msg.Name == "" {
		return nil, errors.New("missing name")
	}

	// Simulate some network call that takes 10ms
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	case <-time.After(10 * time.Millisecond):
	}
	return connect.NewResponse(&greetv1.GreetResponse{Greeting: "Hello, " + req.Msg.Name}), nil
}

Here we defined our greetv1connect.GreetServiceHandler implementation. It implements the Greet method defined in the protobuf file alove. Since this is for demonstration purposes, all we do is check to see if the given name is empty, sleep for 10 milliseconds to simulate a network call and returns the greeting as "Hello, {name}".

A keep observer might notice that this file contains our first “test”. The line var _ greetv1connect.GreetServiceHandler = (*greeterService)(nil) is a way of doing a type assertion in Go. It ensures that your greeterService struct correctly implements the GreeterService interface defined by the protobuf file above. This relies on a trick of the Go syntax that will try to bind a variable _ using the greetv1connect.GreetServiceHandler type. If the given greeterService pointer doesn’t implement the interface then the compiler should complain about what specific methods are missing and which method signatures don’t match.

Hands-On: Direct Service Testing Example

Let’s write some unit tests for a simple ConnectRPC service:

func TestGreet(t *testing.T) {
	service := &greeterService{}
	response, err := service.Greet(context.Background(), connect.NewRequest(&greetv1.GreetRequest{Name: "Alice"}))
	if err != nil {
		t.Fatalf("Greet failed: %v", err)
	}
	if response.Msg.Greeting != "Hello, Alice" {
		t.Errorf("Unexpected greeting: got %q, want %q", response.Msg.Greeting, "Hello, Alice")
	}
}

Explanation: The TestGreet function creates an instance of your greeterService and directly calls its Greet method. We then assert that the response matches our expectations. This is, by far, the simplest method for testing a ConnectRPC service.

Hands-On: Table-Driven Tests with Testify

Now that we wrote a single unit test, the next example will show you how to utilize table tests in order to easily write more test cases. You will see code that looks like this in well-tested Go repositories.

func TestGreetTable(t *testing.T) {
	service := &greeterService{}
	cancelledCtx, cancel := context.WithCancel(context.Background())
	cancel()

	testCases := []struct {
		name    string
		ctx     context.Context
		req     *connect.Request[greetv1.GreetRequest]
		want    *connect.Response[greetv1.GreetResponse]
		wantErr string
	}{
		{
			name:    "Success",
			req:     connect.NewRequest(&greetv1.GreetRequest{Name: "Bob"}),
			want:    connect.NewResponse(&greetv1.GreetResponse{Greeting: "Hello, Bob"}),
			wantErr: "",
		},
		{
			name:    "Empty Name",
			req:     connect.NewRequest(&greetv1.GreetRequest{}),
			want:    nil, // Expecting an error
			wantErr: "missing name",
		},
		{
			name:    "Context Cancelled",
			ctx:     cancelledCtx,
			req:     connect.NewRequest(&greetv1.GreetRequest{Name: "Alice"}),
			want:    nil,
			wantErr: "context canceled",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			ctx := tc.ctx
			if ctx == nil {
				ctx = context.Background()
			}

			got, err := service.Greet(ctx, tc.req)
			if tc.wantErr != "" {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tc.wantErr)
			} else {
				require.NoError(t, err)
				assert.Equal(t, tc.want, got)
			}
		})
	}
}

Explanation:

  1. Table Setup: A testCases slice defines scenarios with varying inputs (req), expected outputs (want), and potential errors (wantErr).
  2. Context Cancellation: The test case “Context Cancelled” simulates a cancelled context by creating a context with context.WithCancel and immediately calling cancel().
  3. Testify Assertions: The require package is used for assertions that should stop the test if they fail (e.g., requiring an error). The assert package is used for assertions that are not critical for continuing the test. Typically, errors during test setup use require and assertions on the results of the test use assert.

Hands-On: Server Testing Example

Here’s how you can test the same service using net/http/httptest server:

func TestGreetWithServer(t *testing.T) {
	mux := http.NewServeMux()
	mux.Handle(greetv1connect.NewGreetServiceHandler(&greeterService{}))

	server := httptest.NewServer(mux)
	t.Cleanup(func() { server.Close() })

	client := greetv1connect.NewGreetServiceClient(http.DefaultClient, server.URL)

	response, err := client.Greet(context.Background(), connect.NewRequest(&greetv1.GreetRequest{Name: "Alice"}))
	if err != nil {
		t.Fatalf("Greet failed: %v", err)
	}
	if response.Msg.Greeting != "Hello, Alice" {
		t.Errorf("Unexpected greeting: got %q, want %q", response.Msg.Greeting, "Hello, Alice")
	}
}
  • Server Setup: We create a ConnectRPC handler and start it with httptest.NewServer(mux).
  • Client Setup: We create a ConnectRPC client that connects to the server that we just created.
  • Test Interaction: We use the client to call the Greet method and assert the response, just like in the direct service testing example.

Conclusion: Test with Confidence

In this guide, we’ve explored the “why” and “how” of unit testing your ConnectRPC services. By embracing unit testing as a core part of your development workflow, you’ll create more robust, reliable, and maintainable RPC systems. Remember, effective testing isn’t just about fixing bugs – it’s about building confidence in your codebase and enabling you to iterate and evolve your services with ease.

The full source code can be found on github.

Next Steps:

  • Go Beyond the Basics: Explore more advanced testing techniques, such as mocking dependencies for more complex scenarios.
  • Integrate with Your CI/CD: Automate your unit tests to run as part of your continuous integration and continuous delivery (CI/CD) pipeline for immediate feedback on code changes.
  • Share Your Knowledge: Help the ConnectRPC community grow by sharing your own testing strategies and experiences!

Ready to put your newfound knowledge into action? Start writing those unit tests and watch your ConnectRPC projects thrive!