Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
02 Sep 2025 · 6 min read ·Article 86 / 110
Go

86. Case Study: Testing a CRUD Service with gRPC

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

86. Case Study: Testing a CRUD Service with gRPC

These days, large-scale application development tends to adopt microservices architectures along with efficient communication protocols such as gRPC. Many companies make gRPC their primary choice because of its high performance and schema-driven API powered by Protocol Buffers. However, the biggest challenge that frequently arises is how to test these services effectively, especially for CRUD (Create, Read, Update, Delete) services.

In this article, I want to walk you through a real-world case study on testing a gRPC-based CRUD service. I’ll cover the service structure, unit testing strategies, integration testing, and even test automation. The code samples will use Go (Golang) because of its popularity within the gRPC ecosystem.


Background of the Problem

Imagine a team building a BookService that offers CRUD operations on a Book entity. This service has to be fast, reliable, and easy to test. Automated testing is needed to make sure that code changes don’t introduce regressions. The tests must also be easy to run, both in a local environment and in a CI/CD pipeline.

CRUD Flow Diagram in BookService

MERMAID
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

Protobuf Definition

The first step in developing a gRPC service is describing the API with protobuf:

protobuf
 1syntax = "proto3";
 2
 3package book;
 4
 5service BookService {
 6    rpc CreateBook (Book) returns (BookId) {}
 7    rpc GetBook (BookId) returns (Book) {}
 8    rpc UpdateBook (Book) returns (Book) {}
 9    rpc DeleteBook (BookId) returns (Empty) {}
10}
11
12message Book {
13    string id = 1;
14    string title = 2;
15    string author = 3;
16}
17
18message BookId {
19    string id = 1;
20}
21
22message Empty {}

With this schema, the API structure is clear, easy to document, and easy to consume by clients across different languages.


Go Project Structure

Let’s simulate a simple directory structure for BookService.

shell
1bookservice/
2  ├── proto/
3  │   └── book.proto
4  ├── main.go
5  ├── server.go
6  ├── repository.go
7  └── repository_test.go

A Simple Server Implementation

The server side handles the CRUD requests. The repository is abstracted away to make testing easier:

go
 1// repository.go
 2type Book struct {
 3    ID     string
 4    Title  string
 5    Author string
 6}
 7
 8type BookRepository interface {
 9    Create(book Book) (string, error)
10    Get(id string) (Book, error)
11    Update(book Book) error
12    Delete(id string) error
13}
14
15type InMemoryBookRepo struct {
16    books map[string]Book
17}
18
19func (r *InMemoryBookRepo) Create(book Book) (string, error) {
20    r.books[book.ID] = book
21    return book.ID, nil
22}
23
24func (r *InMemoryBookRepo) Get(id string) (Book, error) {
25    if book, ok := r.books[id]; ok {
26        return book, nil
27    }
28    return Book{}, errors.New("not found")
29}
30
31func (r *InMemoryBookRepo) Update(book Book) error {
32    if _, ok := r.books[book.ID]; ok {
33        r.books[book.ID] = book
34        return nil
35    }
36    return errors.New("not found")
37}
38
39func (r *InMemoryBookRepo) Delete(id string) error {
40    if _, ok := r.books[id]; ok {
41        delete(r.books, id)
42        return nil
43    }
44    return errors.New("not found")
45}

Unit Testing: Focusing on the Repository

Before testing the full service, it’s important to write unit tests for the repository. The goal is to make sure the core CRUD logic is correct, independent of gRPC. Here’s an example test case using Go’s testing package:

go
 1// repository_test.go
 2func TestInMemoryBookRepo_CRUD(t *testing.T) {
 3    repo := &InMemoryBookRepo{books: map[string]Book{}}
 4    book := Book{ID: "1", Title: "Go in Action", Author: "John Doe"}
 5    
 6    // Test Create
 7    id, err := repo.Create(book)
 8    if err != nil || id != "1" {
 9        t.Errorf("Create failed, got id=%v, err=%v", id, err)
10    }
11
12    // Test Read
13    b, err := repo.Get("1")
14    if err != nil || b.Title != "Go in Action" {
15        t.Errorf("Get failed, got book=%v, err=%v", b, err)
16    }
17
18    // Test Update
19    book.Title = "Go Lang in Action"
20    err = repo.Update(book)
21    if err != nil {
22        t.Errorf("Update failed: %v", err)
23    }
24
25    // Test Delete
26    err = repo.Delete("1")
27    if err != nil {
28        t.Errorf("Delete failed: %v", err)
29    }
30}

Benefits of Unit Testing

  • Isolation: Ensures the repository component behaves as expected, regardless of the communication protocol.
  • Speed: The tests run extremely fast.

Integration Testing with gRPC

Next, we need to test whether the gRPC server actually translates requests into the correct CRUD operations.

Setting Up the Integration Test

  1. Spin up a gRPC server backed by the in-memory repository.
  2. Send requests to the server directly using a gRPC client.
go
 1// integration_test.go (pseudo)
 2func TestBookServiceIntegration(t *testing.T) {
 3    // 1. Run the server (no need to listen on a production port)
 4    lis := bufconn.Listen(1024 * 1024)
 5    s := grpc.NewServer()
 6    repo := &InMemoryBookRepo{books: map[string]Book{}}
 7    RegisterBookServiceServer(s, &BookServiceServer{repo: repo})
 8    go s.Serve(lis)
 9    defer s.Stop()
10
11    // 2. Set up the client with a custom dialer
12    ctx := context.Background()
13    conn, _ := grpc.DialContext(
14        ctx, "bufnet",
15        grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
16            return lis.Dial()
17        }),
18        grpc.WithInsecure(),
19    )
20    defer conn.Close()
21    client := NewBookServiceClient(conn)
22
23    // 3. Run the end-to-end integration test
24    resp, err := client.CreateBook(ctx, &Book{Id: "123", Title: "gRPC for Dummies", Author: "Jane"})
25    // 4. Assert validations
26    if err != nil || resp.Id != "123" {
27        t.Fatalf("Expected Id=123, got %v, err=%v", resp.Id, err)
28    }
29}

CRUD Scenario Test Results Table

OperationInputOutput / ExpectationStatus
Createid=123, title=“gRPC for Dummies”Success, id=123Pass
Readid=123Book foundPass
Updateid=123, title=“gRPC 101”Book updatedPass
Deleteid=123Book deletedPass
Readid=999 (does not exist)Error: not foundPass

Test Automation in a CI/CD Pipeline

The gRPC service tests above can be automated directly within a CI/CD pipeline (for example, with GitHub Actions or GitLab CI). The key to success: no dependency on external infrastructure, so the service can spin up and test itself using in-memory dependencies.

Test Automation Tips:

  • Use test doubles for external resources (e.g. in-memory or mock).
  • Make sure you have at least minimal test coverage for every CRUD feature.
  • Separate unit tests from integration tests (for example, using the _test.go suffix).

Key Takeaways

  • gRPC is extremely powerful for high-performance, schema-first services.
  • Testing CRUD must be done at two levels: unit (logic) and integration (the gRPC interface).
  • Using an in-memory repository makes it easy to isolate components and automate tests in the pipeline.
  • Test coverage shouldn’t be limited to the “happy path” only; it must also cover edge cases and error conditions.

Conclusion

Testing a gRPC-based CRUD service can be made both simple and powerful by modularizing components, writing unit and integration tests systematically, and relying on in-memory dependencies. With this approach, errors are caught faster, development velocity improves, and the codebase becomes more resilient to change.

In practice, a model like this is easy to replicate for other microservices, and it builds a strong foundation of engineering excellence within your development team.


References:


I hope this case study deepens your insight into building and testing scalable, reliable gRPC CRUD services!

Related Articles

💬 Comments