Home > Software engineering >  How to create mockable code without shadowing usages (preferably with interfaces)
How to create mockable code without shadowing usages (preferably with interfaces)

Time:09-24

I've designed my system, so a layer receives an interface for a lower layer. This seems to be the best practice way to create mockable code in golang. The higher level layer can accept any struct that implements the interface, so you can call the higher layer with a real lower layer or a mocked lower layer. The problem is that the lower layers usages are lost. Because of the abstraction, the compiler can't see where the lower layer is used. This visibility is especially important when refactoring, so the programmer can see everywhere a function is used--without relying on control-f. I've included a minimized version of the current architecture, if you were to copy the code into an ide, you could see the issue by attempting to find all usages of Get() > Repository > repository.go

How can I make this pattern work, using interfaces, without shadowing the usages of lower layers?

Package - main

File - main.go

package main

import (
    "awesomeProject1/internal"
    "fmt"
)

func main() {
    realRepo := &internal.Repository{}
    realService := internal.Service{Repo: realRepo}
    fmt.Println(realService.FindById(1))
}

Package - Internal

File - service.go

package internal

type Service struct {
    Repo IRepository
}

type IRepository interface {
    Get(id uint64) string
}

func (service *Service) FindById(id uint64) string {
    return service.Repo.Get(id)
}

File - repository.go

package internal

type Repository struct {
}

func (repo *Repository) Get(id uint64) string {
    return "a real value from db"
}

Package - tests

File - service_test.go

package tests

import (
    "awesomeProject1/internal"
    "fmt"
    "testing"
)

func TestService(t *testing.T) {
    mockRepo := &MockRepository{}
    realService := internal.Service{Repo: mockRepo}
    fmt.Println(realService.FindById(1))
}

File - mock_repository.go

package tests

type MockRepository struct {
}

func (repo *MockRepository) Get(id uint64) string {
    return "a fake value for testing"
}

CodePudding user response:

You can do by using the mockery tool in golang

 1. First install the mockery tool  (link: https://github.com/vektra/mockery)
    PS C:\GolandProjects\Stackoverflow\internal>  go install github.com/vektra/mockery/v2@latest  

 2. Go to the folder location in terminal for which interface u want to generate the mock file and generate the mock                 
    PS C:\GolandProjects\Stackoverflow\internal> mockery --name IRepository
    22 Sep 22 13:45 IST INF Starting mockery dry-run=false version=v2.14.0
    22 Sep 22 13:45 IST INF Walking dry-run=false version=v2.14.0
    22 Sep 22 13:45 IST INF Generating mock dry-run=false interface=IRepository qualified-    
              name=awesomeProject1/internal version=v2.14.0
3. You will see one folder called mocks and the mock file with interface name       

4. Mock IRepository.go               
    
    // Code generated by mockery v2.14.0. DO NOT EDIT.
    
    package mocks
    
    import mock "github.com/stretchr/testify/mock"
    
    // IRepository is an autogenerated mock type for the IRepository type
    type IRepository struct {
        mock.Mock
    }
    
    // Get provides a mock function with given fields: id
    func (_m *IRepository) Get(id uint64) string {
        ret := _m.Called(id)
    
        var r0 string
        if rf, ok := ret.Get(0).(func(uint64) string); ok {
            r0 = rf(id)
        } else {
            r0 = ret.Get(0).(string)
        }
    
        return r0
    }
    
    type mockConstructorTestingTNewIRepository interface {
        mock.TestingT
        Cleanup(func())
    }
    
    // NewIRepository creates a new instance of IRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
    func NewIRepository(t mockConstructorTestingTNewIRepository) *IRepository {
        mock := &IRepository{}
        mock.Mock.Test(t)
    
        t.Cleanup(func() { mock.AssertExpectations(t) })
    
        return mock
    }

5. In the below file you need to create object for IRepository      

        package tests

        import(
        "awesomeProject1/internal""fmt""testing"
        )

        funcTestService(t *testing.T){

        mockRepo := mocks.IRepository{}
        mockRepo.On("Get", mock.Anything).Return("some string")

        realService := internal.Service{Repo: &mockRepo}
        fmt.Println(realService.FindById(1))
    }
  • Related