πŸ§ͺ GOLANG TESTING WITH STRETCHR/TESTIFY AND MOCKERY

Truong Phung - Oct 16 - - Dev Community

Let's go through a comprehensive example that covers common features of the stretchr/testify library and mockery for mocking in Golang. This example will include testing with assertions, using the require package for strict assertions, testing HTTP handlers, and mocking dependencies using mockery.

Scenario

Imagine we have a service that fetches user information from an external API. We want to test:

  • The service's functionality.
  • Its integration with an external client.
  • Mocking the external client.

Project Structure

/project
β”‚
β”œβ”€β”€ main.go
β”œβ”€β”€ service.go
β”œβ”€β”€ service_test.go
β”œβ”€β”€ user_client.go
β”œβ”€β”€ mocks/
β”‚   └── UserClient.go (generated by mockery)
└── go.mod

Enter fullscreen mode Exit fullscreen mode

Code Overview

  1. user_client.go

    This file defines an interface for interacting with an external user API.

    package project
    
    type User struct {
        ID   int
        Name string
    }
    
    type UserClient interface {
        GetUserByID(id int) (*User, error)
    }
    
  2. service.go

    This file contains a service that uses the UserClient to fetch user details.

    package project
    
    import "fmt"
    
    type UserService struct {
        client UserClient
    }
    
    func NewUserService(client UserClient) *UserService {
        return &UserService{client: client}
    }
    
    func (s *UserService) GetUserDetails(id int) (string, error) {
        user, err := s.client.GetUserByID(id)
        if err != nil {
            return "", fmt.Errorf("failed to get user: %w", err)
        }
    
        return fmt.Sprintf("User: %s (ID: %d)", user.Name, user.ID), nil
    }
    
  3. Generating Mocks with mockery

    You can generate mocks for the UserClient using mockery:

    mockery --name=UserClient --output=./mocks
    

    This will generate a mock in mocks/UserClient.go.

  4. service_test.go

    Now, let's write a test for the UserService using testify assertions and the mockery-generated mock.

        package project_test
    
        import (
            "errors"
            "testing"
    
            "github.com/stretchr/testify/assert"
            "github.com/stretchr/testify/require"
            "github.com/stretchr/testify/mock"
            "project"
            "project/mocks"
        )
    
        func TestUserService_GetUserDetails_Success(t *testing.T) {
            // Create a new mock client
            mockClient := new(mocks.UserClient)
    
            // Define what the mock should return when `GetUserByID` is called
            mockClient.On("GetUserByID", 1).Return(&project.User{
                ID:   1,
                Name: "John Doe",
            }, nil)
    
            // Create the UserService with the mock client
            service := project.NewUserService(mockClient)
    
            // Test the GetUserDetails method
            result, err := service.GetUserDetails(1)
    
            // Use `require` for error checks
            require.NoError(t, err)
            require.NotEmpty(t, result)
    
            // Use `assert` for value checks
            assert.Equal(t, "User: John Doe (ID: 1)", result)
    
            // Ensure that the `GetUserByID` method was called exactly once
            mockClient.AssertExpectations(t)
        }
    
        func TestUserService_GetUserDetails_Error(t *testing.T) {
            // Create a new mock client
            mockClient := new(mocks.UserClient)
    
            // Define what the mock should return when `GetUserByID` is called with an error
            mockClient.On("GetUserByID", 2).Return(nil, errors.New("user not found"))
    
            // Create the UserService with the mock client
            service := project.NewUserService(mockClient)
    
            // Test the GetUserDetails method
            result, err := service.GetUserDetails(2)
    
            // Use `require` for error checks
            require.Error(t, err)
            assert.Contains(t, err.Error(), "user not found")
    
            // Ensure that the result is empty
            assert.Empty(t, result)
    
            // Ensure that the `GetUserByID` method was called exactly once
            mockClient.AssertExpectations(t)
        }
    
    

Key Points of This Example

  1. Assertions with testify:
    • assert and require packages are used for different types of checks.
    • require is used for checks that should fail the test immediately if they fail (e.g., checking for nil errors).
    • assert is used for checks that can continue even if they fail (e.g., comparing values).
  2. Mocking with mockery:
    • mockery generates a mock of the UserClient interface.
    • In the test, the mock is configured with.On() to specify expected inputs and .Return() to specify the outputs.
    • AssertExpectations verifies that the mocked method was called with the expected inputs.
  3. Testing Error Handling:
    • One test checks the successful scenario, while the other tests how the service handles an error from the UserClient.

This setup covers the basic functionality of stretchr/testify for assertions and mocking with mockery, providing a structured and maintainable approach to unit testing in Golang.

If you found this helpful, let me know by leaving a πŸ‘ or a comment!, or if you think this post could help someone, feel free to share it! Thank you very much! πŸ˜ƒ

. . . . . . . . . . . . . . . . . . . . . . . . . . .