Lesson 11 · Testing the layers & running

Testing a gRPC handler

Mock the usecase, fake the auth context, and — at last — cover every error path, including one that only logs.

Your win: test a transport handler by mocking the layer below it (the usecase), faking the request's identity context, and writing the full error-path checklist — the coverage the usecase test skipped.

Mock the usecase — the layer below the transport

The pattern holds one more rung up: a gRPC handler's collaborator is the usecase, so you mock that (plus kafka/repos it touches directly) and inject into the service:

internal/spike/modules/email/controller/grpc/email_modifier_send_email_test.go:35-52
createEmailHandler := &mock_commands.MockCreateEmailHandler{}   // mock the USECASE
mockKafka := mocks.NewKafkaManagement(t)                        // mockery ctor (auto-cleanup)
mockDB    := new(mock_database.Ext)

svc := &EmailModifierService{
    EmailCommandHandler: createEmailHandler,      // ← the usecase, mocked
    KafkaMgmt: mockKafka, DB: mockDB, EmailRepo: &mockEmailRepo,
    Logger: logger, Env: "local",
}
Two mock construction styles, both here mocks.NewKafkaManagement(t) is a mockery-generated constructor — pass t and it auto-registers AssertExpectations on cleanup. &mock_commands.MockCreateEmailHandler{} is a reflection-generated struct you construct directly. Same testify underneath (Lesson 7); the constructor is just a mockery nicety.

Fake the auth context

The handler reads the tenant from the request context (the auth machinery — see the Auth course). In a unit test there's no real interceptor, so you fake the claims directly:

email_modifier_send_email_test.go:57-61
ctx := interceptors.ContextWithJWTClaims(context.Background(), &interceptors.CustomClaims{
    Manabie: &interceptors.ManabieClaims{ ResourcePath: "-2147483648" },   // the tenant
})

Now interceptors.ResourcePathFromContext(ctx) inside the handler returns your fake tenant — no JWT, no verification, just the value the code needs. (The same trick sets UserID/UserGroup for RBAC-dependent handlers.)

The error-path checklist, in full

This is the file that does what the usecase test skipped (Lesson 9). Four cases walk the handler's branches:

Sub-testArrangeAssert
Happy casepublish returns nilerr == nil; status → QUEUED
Kafka publish errorpublish returns an errorstatus.Error(codes.Internal, wrapped); status → QUEUED_FAILED
Publish and update failboth return errorsmultierr.Combine of both, wrapped
Publish ok, update failspublish ok, update errorsno error returned — it's logged

The error assertions are exact — they compare the whole status.Error:

email_modifier_send_email_test.go:126-129 (the kafka-error case)
expectedErr := errors.Wrap(fmt.Errorf("published failed"), "error when call svc.KafkaMgmt.PublishContext")
_, err := svc.SendEmail(ctx, reqGRPC)
assert.Equal(t, status.Error(codes.Internal, expectedErr.Error()), err)   // exact code + message
The log-only case needs a special tool The fourth case is subtle: when the publish succeeds but the status update fails, the handler doesn't return an error — it just logs and moves on. How do you test "it logged"? With zaptest/observer: you build the logger from an observer core (core, log := observer.New(zap.InfoLevel), :41-42), and after the call you inspect log.All() for the expected entry. It's the only way to assert on a side-effect that isn't a return value.
The layer pattern, complete You've now seen all three: repository → mock the driver; usecase → mock the repos; transport → mock the usecase. Each layer's test mocks exactly the layer beneath it, so every layer is tested in true isolation. That's the whole strategy in one sentence.
Read this next

The gRPC handler test

Read the four cases in full — the model for thorough error-path coverage.

→ in-repo …/controller/grpc/email_modifier_send_email_test.go
pkg.go.dev — zaptest/observer (asserting on logs) · grpc/codes (error codes)

Check yourself (from memory)

Q1. To test a gRPC handler, which collaborator do you mock?

Each layer mocks the one below: transport → usecase. Inject mock_commands.MockCreateEmailHandler into the service.

Q2. How does the test give the handler a tenant/identity?

Fake the claims directly, so ResourcePathFromContext returns your value — no JWT, no interceptor.

Q3. How do you test a path that logs an error instead of returning one?

observer.New gives a logger whose entries you can read with log.All() — the way to assert a logged side-effect.
Recall: how to test a gRPC handler + the layer pattern.
mock + fake ctx + the 4 cases + log tool, then reveal
Mock the usecase: inject mock_commands.MockCreateEmailHandler (+ kafka/db) into the service. Fake auth ctx: interceptors.ContextWithJWTClaims(ctx, &CustomClaims{Manabie: &ManabieClaims{ResourcePath: …}})ResourcePathFromContext returns it, no JWT. Error-path checklist (4 cases): happy (publish nil → QUEUED) · kafka error (status.Error(codes.Internal, wrapped) → QUEUED_FAILED) · publish+update both fail (multierr.Combine) · publish ok but update fails (logs only, no error). Assert errors by exact status.Error(code, msg). Log-only path: core, log := observer.New(zap.InfoLevel) → inspect log.All(). Layer pattern complete: repo→mock driver; usecase→mock repos; transport→mock usecase. Ctor styles: mockery NewX(t) (auto-cleanup) vs reflection &MockX{}.
🎯 Interview one-liner "How do you test a request handler thoroughly?" → "Mock the usecase it delegates to, fake the identity on the context, and cover every branch: the happy path, each downstream error mapped to the right gRPC status, combined failures, and the paths that only log — which I assert with a zap observer. Each layer's test mocks the layer beneath it, so everything's isolated."
Last lesson: running tests, reading coverage, and the daily workflow — plus the whole-course recap. Ask me to explain the multierr case or the observer setup in more detail.

1. In-repo: …/controller/grpc/email_modifier_send_email_test.go:35-185. zaptest/observer.