Membuat mock file pada go dengan mockery

Apa itu mocking

Pada testing sering kali kita harus membuat test pada fungsi yang akan memanggil fungsi lainya. Namun hal ini melanggar prinsip test isolation. Oleh karena itu, kita dapat menggunakan mock.

Mock bekerja dengan menggantikan fungsi yang akan dipanggil oleh fungsi yang akan kita test dengan unit test dengan masukan dan keluaran dummy. Dengan begitu hasil dari fungsi yang akan diuji coba tidak bergantung keldalam hasil fungsi yang ada didalamnya.

Membuat file mock dengan mockery

Pada golang, terdapat modul mockery yang dapat membuat mock file yang akan membuat mock dari interface yang ada.

Berikut adalah contoh interface user service:

//IUserService is the interface for auth service
type IUserService interface {
    UpdateUser(user models.User) (err error)
    CreateUser(user models.User) (err error)
    GetByID(id uint) (user models.User, err error)
    List(invoker int, user models.User, page int, limit int) (rows int64, users []models.User, err error)
}

dengan menjalankan command mockery --all --keeptree maka akan terbentuk file yang bernama IUserService.go yang berisi:

// Code generated by mockery 2.7.4. DO NOT EDIT.

package mocks

import (
    mock "github.com/stretchr/testify/mock"
    models "gitlab.cs.ui.ac.id/ppl-fasilkom-ui/2021/BB/peradilan-tanpa-kertas/paperless-backend/models"
)

// IUserService is an autogenerated mock type for the IUserService type
type IUserService struct {
    mock.Mock
}

// CreateUser provides a mock function with given fields: user
func (_m *IUserService) CreateUser(user models.User) error {
    ret := _m.Called(user)

    var r0 error
    if rf, ok := ret.Get(0).(func(models.User) error); ok {
        r0 = rf(user)
    } else {
        r0 = ret.Error(0)
    }

    return r0
}

// GetByID provides a mock function with given fields: id
func (_m *IUserService) GetByID(id uint) (models.User, error) {
    ret := _m.Called(id)

    var r0 models.User
    if rf, ok := ret.Get(0).(func(uint) models.User); ok {
        r0 = rf(id)
    } else {
        r0 = ret.Get(0).(models.User)
    }

    var r1 error
    if rf, ok := ret.Get(1).(func(uint) error); ok {
        r1 = rf(id)
    } else {
        r1 = ret.Error(1)
    }

    return r0, r1
}

// List provides a mock function with given fields: invoker, user, page, limit
func (_m *IUserService) List(invoker int, user models.User, page int, limit int) (int64, []models.User, error) {
    ret := _m.Called(invoker, user, page, limit)

    var r0 int64
    if rf, ok := ret.Get(0).(func(int, models.User, int, int) int64); ok {
        r0 = rf(invoker, user, page, limit)
    } else {
        r0 = ret.Get(0).(int64)
    }

    var r1 []models.User
    if rf, ok := ret.Get(1).(func(int, models.User, int, int) []models.User); ok {
        r1 = rf(invoker, user, page, limit)
    } else {
        if ret.Get(1) != nil {
            r1 = ret.Get(1).([]models.User)
        }
    }

    var r2 error
    if rf, ok := ret.Get(2).(func(int, models.User, int, int) error); ok {
        r2 = rf(invoker, user, page, limit)
    } else {
        r2 = ret.Error(2)
    }

    return r0, r1, r2
}

// UpdateUser provides a mock function with given fields: user
func (_m *IUserService) UpdateUser(user models.User) error {
    ret := _m.Called(user)

    var r0 error
    if rf, ok := ret.Get(0).(func(models.User) error); ok {
        r0 = rf(user)
    } else {
        r0 = ret.Error(0)
    }

    return r0
}

Dengan file mock yang telah digenerate, kita dapat melakukan test isolation.

Menggunakan mock yang sudah dibuat oleh mockery

Perhatikan contoh fungsi list pada user controller:

// List is
func (u UserController) List(res http.ResponseWriter, req *http.Request) {
    role, err := strconv.Atoi(req.Header.Get("Role"))
    if err != nil {
        helpers.ResponseBadRequest(res, http.StatusBadRequest, err)
        return
    }

    if !(helpers.IsAdminKepalaSeksi(strconv.Itoa(role)) || helpers.IsAdminPanitera(strconv.Itoa(role))) {
        helpers.ResponseBadRequest(res, http.StatusUnauthorized, errors.New(unauthorizedMessage))
        return
    }

    page, err := strconv.Atoi(req.URL.Query().Get("page"))
    if err != nil {
        helpers.ResponseBadRequest(res, http.StatusBadRequest, err)
        return
    }

    limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
    if err != nil {
        helpers.ResponseBadRequest(res, http.StatusBadRequest, err)
        return
    }

    // req.URL.Query().Get("limit")

    var dataBody models.User
    dataBody.Name = req.URL.Query().Get("name")
    dataBody.Role, _ = strconv.Atoi(req.URL.Query().Get("role"))
    dataBody.Email = req.URL.Query().Get("email")
    dataBody.Status, _ = strconv.Atoi(req.URL.Query().Get("status"))

    rows, users, err := u.UserService.List(role, dataBody, page, limit)
    if err != nil {
        helpers.ResponseBadRequest(res, http.StatusBadRequest, err)
        return
    }
    helpers.Response(res, http.StatusOK, map[string]interface{}{
        "TotalData": rows,
        "Users":     users,
    })
}

Kita ingin mengetest ketika service didalamnya memberikan error bad query limit maka akan disimulasikan bahwa service menerima input yang memiliki query limit yang salah:

func TestListBadQueryPageAndLimit(t *testing.T) {
    userService := new(mocks.IUserService)
    controller := InitUserController(userService)
    router := getUSRouter(controller)

    req := createBadRequest("GET", "/pengguna?page=0&limit=51")
    req.Header.Add("Role", "4")
    req.Header.Add("UID", "2")

    res := httptest.NewRecorder()
    userService.On("List", 4, models.User{}, 0, 51).Return(int64(0), []models.User{}, errors.New("invalid param"))
    router.ServeHTTP(res, req)
    assert.Equal(t, 400, res.Code, "response should be 400")
}

pada kasus ini kita akan menggunakan mock pada userService.On("List", 4, models.User{}, 0, 51).Return(int64(0), []models.User{}, errors.New("invalid param")) untuk menerima bad query dengan page 0 danlimit 51 dan mengembalikan error “invalid param”.

Kita juga dapat mensimulasikan pemanggilan fungsi sukses:

func TestListSuccess(t *testing.T) {
    userService := new(mocks.IUserService)
    controller := InitUserController(userService)
    router := getUSRouter(controller)

    req := createBadRequest("GET", "/pengguna?page=1&limit=10")
    req.Header.Add("Role", "4")
    req.Header.Add("UID", "2")

    res := httptest.NewRecorder()
    userService.On("List", 4, models.User{}, 1, 10).Return(int64(0), []models.User{}, nil)
    router.ServeHTTP(res, req)
    assert.Equal(t, 200, res.Code, "response should be 200")
}

pada kasus ini service akan melakukan mocking untuk menerima data yang valid dan memberikan keluaran yang valid juga.

Kesimpulan

Dengan adanya mocking kita dapat mengisolasi test case menjadi satu kasus yang tidak dependen kepada pemanggilan fungsi yang ada didalamnya. Hal ini akan mempermudah proses pembuatan testcase.