DEV Community

yanoandri
yanoandri

Posted on

Mock Database Unit Test with Mockery in Go

Introduction

Hi everyone, in this article, we will learn how to create unit test in Go, using mockery to testing a project's dependency. We often use mock testing as we unit test a function with another dependencies such as database, so we will learn on how to create it by using example that i'd wrote in here with the steps by steps on how to refactor, create unit test and using the mockery itself, let's get start into it!

Why Mockery?

Mockery provide us with the command to generate mock functions, that we can use for unit testing, so as a results we don't have to write our mock function from scratch, and it will be automatically provided by searching into our interface functions, inside our project's dependencies

To make this tutorial easier, we will prepare to refactor our code from previous article first, create a unit test, run the mockery and start using it in our unit test file in Go

Refactor

first thing to do, we're going to have a look into our old code, we going to define the interface in it, how do we do it? by creating a new root folder with name of repository, from here this is what we call when we use our database dependency.

- config
 |_ config.go
- models
 |_ payment.go
- repository
 |_ payment.go
- test.go
Enter fullscreen mode Exit fullscreen mode

we will start by defining the interface in payment.go inside our repository folder

type IPaymentRepository interface {
    UpdatePayment(id string, payment models.Payment) (models.Payment, error)
    DeletePayment(id string) (int64, error)
    SelectPaymentWIthId(id string) (models.Payment, error)
    CreatePayment(payment models.Payment) (int64, error)
}
Enter fullscreen mode Exit fullscreen mode

since we are going to separate the database dependency from the function, we're going to create a struct that act to hold our database dependency value

type Repository struct {
    Database *gorm.DB
}
Enter fullscreen mode Exit fullscreen mode

then we move all the functions except the main one, inside our payment.go, the complete repository will look like this

package repository

import (
    "errors"

    "github.com/yanoandri/simple-goorm/models"
    "gorm.io/gorm"
)

type IPaymentRepository interface {
    UpdatePayment(id string, payment models.Payment) (models.Payment, error)
    DeletePayment(id string) (int64, error)
    SelectPaymentWIthId(id string) (models.Payment, error)
    CreatePayment(payment models.Payment) (int64, error)
}

type Repository struct {
    Database *gorm.DB
}

func (repo Repository) UpdatePayment(id string, payment models.Payment) (models.Payment, error) {
    var updatePayment models.Payment
    result := repo.Database.Model(&updatePayment).Where("id = ?", id).Updates(payment)
    if result.RowsAffected == 0 {
        return models.Payment{}, errors.New("payment data not update")
    }
    return updatePayment, nil
}

func (repo Repository) DeletePayment(id string) (int64, error) {
    var deletedPayment models.Payment
    result := repo.Database.Where("id = ?", id).Delete(&deletedPayment)
    if result.RowsAffected == 0 {
        return 0, errors.New("payment data not update")
    }
    return result.RowsAffected, nil
}

func (repo Repository) SelectPaymentWIthId(id string) (models.Payment, error) {
    var payment models.Payment
    result := repo.Database.First(&payment, "id = ?", id)
    if result.RowsAffected == 0 {
        return models.Payment{}, errors.New("payment data not found")
    }
    return payment, nil
}

func (repo Repository) CreatePayment(payment models.Payment) (int64, error) {
    result := repo.Database.Create(&payment)
    if result.RowsAffected == 0 {
        return 0, errors.New("payment not created")
    }
    return result.RowsAffected, nil
}
Enter fullscreen mode Exit fullscreen mode

because we move all the functions into our repository folder, we will have to change the way to call our functions, we start to define which database value we want to hold in our struct inside our test.go at the main function

repo := repository.Repository{Database: db}
Enter fullscreen mode Exit fullscreen mode

and from there we can use our functions, without putting any external dependency inside our parameters, for example

// create a payment
payment := models.Payment{
    PaymentCode: "XXX-1",
    Name:        "Payment for item #1",
    Status:      "PENDING",
}

result, err := repo.CreatePayment(payment)
Enter fullscreen mode Exit fullscreen mode

Create a Unit Test File

Luckily if we use VSCode, we can generate the unit test automatically by pressing ctrl + p if you are in windows or command + p and you will find the option to generate the functions like the screenshot below

go generate functions in vs code

and for the example we will create the test function of CreatePayment by blocking the whole functions to generate it

package repository

import (
    "testing"

    "github.com/yanoandri/simple-goorm/models"
)

func TestRepository_CreatePayment(t *testing.T) {
    type args struct {
        payment models.Payment
    }
    tests := []struct {
        name    string
        repo    Repository
        args    args
        want    int64
        wantErr bool
    }{
        // TODO: Add test cases.
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := tt.repo.CreatePayment(tt.args.payment)
            if (err != nil) != tt.wantErr {
                t.Errorf("Repository.CreatePayment() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if got != tt.want {
                t.Errorf("Repository.CreatePayment() = %v, want %v", got, tt.want)
            }
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

Mockery

We will install mockery using our go command

go install github.com/vektra/mockery/v2@latest
Enter fullscreen mode Exit fullscreen mode

after it finishes, we will create our mock functions by using mockery features to generate all of function mocks, by typing this command

mockery --all --keeptree
Enter fullscreen mode Exit fullscreen mode

notice the mocks folder of our project has been installed, and all of the functions has been mocked
Mocks Folder

and this is one example of our CreatePayment mock functions

// CreatePayment provides a mock function with given fields: payment
func (_m *IPaymentRepository) CreatePayment(payment models.Payment) (int64, error) {
    ret := _m.Called(payment)

    var r0 int64
    if rf, ok := ret.Get(0).(func(models.Payment) int64); ok {
        r0 = rf(payment)
    } else {
        r0 = ret.Get(0).(int64)
    }

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

    return r0, r1
}
Enter fullscreen mode Exit fullscreen mode

Write our test functions to use mocks

Now let's include our mocked function to our unit test

package repository

import (
    "errors"
    "testing"

    mocks "github.com/yanoandri/simple-goorm/mocks/repository"
    "github.com/yanoandri/simple-goorm/models"
)

func TestRepository_CreatePayment(t *testing.T) {
    type args struct {
        payment models.Payment
    }
    tests := []struct {
        name    string
        args    args
        want    int64
        wantErr bool
    }{
        // TODO: Add test cases.
        {
            name: "success_create_payment",
            args: args{
                models.Payment{
                    PaymentCode: "payment-code-011",
                    Status:      "PENDING",
                },
            },
            want:    1,
            wantErr: false,
        },
        {
            name: "failed_create_payment",
            args: args{
                models.Payment{},
            },
            want:    0,
            wantErr: true,
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            repo := &mocks.IPaymentRepository{}
            if !tt.wantErr {
                repo.On("CreatePayment", tt.args.payment).Return(tt.want, nil)
            } else {
                repo.On("CreatePayment", tt.args.payment).Return(tt.want, errors.New("Failed to create payment"))
            }
            got, err := repo.CreatePayment(tt.args.payment)
            if (err != nil) != tt.wantErr {
                t.Errorf("Repository.CreatePayment() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if got != tt.want {
                t.Errorf("Repository.CreatePayment() = %v, want %v", got, tt.want)
            }
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

notice the repo is missing, it is because we didn't have to use the actual dependencies of database, and we want to mock it in every test session. And for now we will try to run our test by using command

go test ./... -v
Enter fullscreen mode Exit fullscreen mode

Tested Results

Conclusions

This is just one function as an example to use mocks functions in unit test, you can continue to make test for UpdatePayment, DeletePayment and also SelectPaymentWIthId. Mockery has provide us with easier way to generate our functions and as a results we can test our dependencies without creating the same functions twice from scratch, Hope this tutorial helps, and keep exploring! See ya!

Source:

Latest comments (0)