Kevin's Blog

Test Command Handler With Mock

This post demonstrates how to test command and query handlers in a Command Query Separation based project.

CQS Structure

Testing Query and Command handlers involves similar techniques, this post will demonstrate testing Command handlers. The principles can be easily applied to Query handlers.

The CQS structure defines a command handler interface, which receives a C generic command object and return nothing but an error in non-happy path.

The CreateArticleHandler is a specific interface which handle the CreateArticle command.

type CommandHandler[C any] interface {
	Handle(ctx context.Context, cmd C) error
}

type CreateArticleHandler = common.CommandHandler[CreateArticle]

type createArticleHandler struct {
    repo ArticleRepository
}

func (c createArticleHandler) Handle(ctx context.Context, cmd CreateArticle) error {
    // TODO
}

Given the command handler is following this structure, we need to test the command handler and the higher component, e.g the controller.

We need to deal with the command handler’s dependency first, which is the ArticleRepository. It would be easier if the ArticleRepository is an interface. If there is no interface for the repository, please consider to have define the interface and use the interface for the repo dependency.

With the interface setup, we can create a dummy implementation to test the ArticleRepository. mockery or gomock can generate mock implementations for interfaces, saving manual effort. We can automate the creation of mock implementations by adding a go:generate comment to the interface.

// file location: internal/domain/article/repository.go

//go:generate mockgen -destination mocks/mock_article_repository.go -package mocks . ArticleRepository
type ArticleRepository interface {
	CreateArticle(
		ctx context.Context,
		article *Article,
	) error
}

The mocked article repository is extremely useful in simulating different scenarios, here is an example where the repository returning an error when creating new article

ctrl := gomock.NewController(t)

repo.EXPECT().CreateArticle(
    gomock.Any(),
    gomock.AssignableToTypeOf(&domain.Article{}),
).Return(errors.New("invalid article category"))

The full testcase written with Ginkgo (this is optional). There is no expensive setup with actual DB connection, the syntax is very descriptive.

It("failed to create article", func() {
    id := uuid.New()

    repo.EXPECT().CreateArticle(
        gomock.Any(),
        gomock.AssignableToTypeOf(&domain.Article{}),
    ).Return(errors.New("invalid article category"))

    cmd := command.NewCreateArticleHandler(
        repo,
        logger,
    )

    err := cmd.Handle(ctx, command.CreateArticle{
        Article: &domain.Article{
            ArticleID: id,
        },
    })

    Expect(err).To(HaveOccurred())
})

After complete both successful and failed scenarios for the command handlers, we can move on to test the higher component (e.g the ArticlesController), which uses the command handler as a dependency.

The same mocking strategy can be applied, add a single line of go:generate command to the command handler interface:


//go:generate mockgen -destination mocks/mock_create_article.go -package mocks . CreateArticleHandler
type CreateArticleHandler = common.CommandHandler[CreateArticle]

By adding this comment, the go generate command will generates the mock implementation for the command handler, which allows to setup different scenarios to test the higher component.

It("returns 500 response when creating article command returning an error", func() {
    cmdError := errors.New("create article error")
    cmd := mocks.NewMockCreateArticleHandler(mockCtrl)

    controller := ports.NewArticlesController(cmd)

    cmd.
        EXPECT().
        Handle(
            gomock.Any(),
            gomock.AssignableToTypeOf(command.CreateArticle{}),
        ).
        Return(cmdError)

    resp, err := controller.CreateArticle(ctx, generateRequest())

    Expect(err).To(Equal(cmdError))
    Expect(resp).To(BeAssignableToTypeOf(ports.CreateArticle500Response{}))
})

To test the happy scenario, modify the mock expectation setup by setting the Return parameter to nil.

Conclusion

By enforcing a clean separation of concerns, CQS makes it easier to write targeted tests for individual components. This increased testability leads to higher quality software, as bugs can be identified and fixed more efficiently.

#architecture #golang #domain driven development

Reply to this post by email ↪