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:
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",
}
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:
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-test | Arrange | Assert |
|---|---|---|
| Happy case | publish returns nil | err == nil; status → QUEUED |
| Kafka publish error | publish returns an error | status.Error(codes.Internal, wrapped); status → QUEUED_FAILED |
| Publish and update fail | both return errors | multierr.Combine of both, wrapped |
| Publish ok, update fails | publish ok, update errors | no error returned — it's logged |
The error assertions are exact — they compare the whole status.Error:
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
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 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?
mock_commands.MockCreateEmailHandler into the service.
Q2. How does the test give the handler a tenant/identity?
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.
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{}.multierr case or the
observer setup in more detail.
1. In-repo: …/controller/grpc/email_modifier_send_email_test.go:35-185. zaptest/observer.