At this stage we will need the following dependencies
brew install mockery
go get github.com/stretchr/testify
go mod tidy
go mod vendor
go get github.com/DATA-DOG/go-sqlmock
In this project we will try to create unit tests using a mocking library with the name mockery
. This library is widely used by golang developers because of its ease of use and has the advantage of features that can cover all the necessary unit tests. If you have used unit tests in golang, you may already know this library but if you want to get more references, Santekno has also provided previous posts including:
- How to Create Unit Tests on Golang
- How to Make a Unit Benchmark on Golang
- How to Create Unit Tests Using Moq Library On Golang
- Mocking Unit Test Techniques in Golang
- How to Create Integration Tests on Golang
- Or a collection of articles about unit tests
Unit Test Repository
The first time we will create a unit test at the repository level. This repository has several functions so that later we will create unit tests one by one to be more detailed. Then before making a unit test we need to make a mocking to be used later at the top level unit test, for example the repository will need at the usecase level then we need to generate and add the code below to the interface
that has been defined.
Open the reposito.go
file and add the code as below.
``go
//go:generate mockery –name=ArticleRepository –filename=repository_mock.go –inpackage
type ArticleRepository interface { GetAll(ctx context.Context) ([]*models.Article, error) GetByID(ctx context.Context, id int64) (*models.Article, error) Update(ctx context.Context, article *models.Article) (*models.Article, error) Store(ctx context.Context, article *models.Article) (int64, error) Delete(ctx context.Context, id int64) (bool, error) }
Then place the terminal pointer at the folder position and execute this command.
```bash
go generate ./...
If successful there will be a repository_mock.go
file that has automatically generated itself.
Create initialitation unit test
Here is the unit test for repository initiation.
func TestNew(t *testing.T) {
type args struct {
conn *sql.DB
}
tests := []struct {
name string
args args
want *ArticleStore
}{
{
name: "success",
args: args{},
want: &ArticleStore{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := New(tt.args.conn); !reflect.DeepEqual(got, tt.want) {
t.Errorf("New() = %v, want %v", got, tt.want)
}
})
}
}
Unit test for GetAll function
func TestArticleStore_GetAll(t *testing.T) {
ctx := context.Background()
mockDB, mock, err := sqlmock.New()
if err != nil {
t.Errorf("error")
}
// expect query
const expetcqueryGetAll = `SELECT id, title, content, create_at, update_at FROM articles`
var column = []string{"id", "title", "content", "create_at", "update_at"}
var timeNow = time.Now()
var expectResult []*models.Article
type fields struct {
db *sql.DB
}
type args struct {
ctx context.Context
}
tests := []struct {
name string
fields fields
args args
want []*models.Article
wantErr bool
mock func()
}{
{
name: "success get article",
fields: fields{
db: mockDB,
},
mock: func() {
mock.ExpectQuery(expetcqueryGetAll).WillReturnRows(
sqlmock.NewRows(column).AddRow(
1, "test", "test content", timeNow, timeNow,
),
)
},
want: []*models.Article{
{
ID: 1,
Title: "test",
Content: "test content",
CreateAt: timeNow,
UpdateAt: timeNow,
},
},
args: args{
ctx: ctx,
},
wantErr: false,
},
{
name: "failed when query error",
fields: fields{
db: mockDB,
},
mock: func() {
mock.ExpectQuery(expetcqueryGetAll).WillReturnError(sql.ErrNoRows)
},
want: expectResult,
args: args{
ctx: ctx,
},
wantErr: true,
},
{
name: "failed rows next",
fields: fields{
db: mockDB,
},
mock: func() {
mock.ExpectQuery(expetcqueryGetAll).WillReturnRows(
sqlmock.NewRows(column).AddRow(
1, "test", "test content", "test", "test",
),
)
},
want: expectResult,
args: args{
ctx: ctx,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &ArticleStore{
db: tt.fields.db,
}
tt.mock()
got, err := r.GetAll(tt.args.ctx)
if (err != nil) != tt.wantErr {
t.Errorf("ArticleStore.GetAll() error = %v, wantErr %v", err, tt.wantErr)
return
}
assert.EqualValues(t, got, tt.want)
})
}
}
Create unit test for GetByID
func TestArticleStore_GetByID(t *testing.T) {
ctx := context.Background()
mockDB, mock, err := sqlmock.New()
if err != nil {
t.Errorf("error")
}
// expect query
const expectQueryGetById = `SELECT id, title, content, create_at, update_at FROM articles WHERE id=(.*)`
var column = []string{"id", "title", "content", "create_at", "update_at"}
var timeNow = time.Now()
type fields struct {
db *sql.DB
}
type args struct {
ctx context.Context
id int64
}
tests := []struct {
name string
fields fields
args args
want *models.Article
wantErr bool
mock func()
}{
{
name: "success get article",
fields: fields{
db: mockDB,
},
mock: func() {
mock.ExpectQuery(expectQueryGetById).WillReturnRows(
sqlmock.NewRows(column).AddRow(
1, "test", "test content", timeNow, timeNow,
),
)
},
want: &models.Article{
ID: 1,
Title: "test",
Content: "test content",
CreateAt: timeNow,
UpdateAt: timeNow,
},
args: args{
ctx: ctx,
},
wantErr: false,
},
{
name: "failed when query error",
fields: fields{
db: mockDB,
},
mock: func() {
mock.ExpectQuery(expectQueryGetById).WillReturnError(sql.ErrNoRows)
},
want: &models.Article{},
args: args{
ctx: ctx,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &ArticleStore{
db: tt.fields.db,
}
tt.mock()
got, err := r.GetByID(tt.args.ctx, tt.args.id)
if (err != nil) != tt.wantErr {
t.Errorf("ArticleStore.GetByID() error = %v, wantErr %v", err, tt.wantErr)
return
}
assert.EqualExportedValues(t, got, tt.want)
})
}
}
Create unit test for Update function
func TestArticleStore_Update(t *testing.T) {
ctx := context.Background()
mockDB, mock, err := sqlmock.New()
if err != nil {
t.Errorf("error")
}
// expect query
const expectQueryUpdate = `UPDATE articles SET title=(.*), content=(.*), update_at=(.*) WHERE id=(.*)`
var timeNow = time.Now()
type fields struct {
db *sql.DB
}
type args struct {
ctx context.Context
article *models.Article
}
tests := []struct {
name string
fields fields
args args
want *models.Article
wantErr bool
mock func()
}{
{
name: "success update article",
fields: fields{
db: mockDB,
},
mock: func() {
mock.ExpectExec(expectQueryUpdate).WillReturnResult(
sqlmock.NewResult(1, 1),
)
},
want: &models.Article{
ID: 1,
Title: "test",
Content: "test content",
CreateAt: timeNow,
UpdateAt: timeNow,
},
args: args{
ctx: ctx,
article: &models.Article{
ID: 1,
Title: "test",
Content: "test content",
CreateAt: timeNow,
UpdateAt: timeNow,
},
},
wantErr: false,
},
{
name: "failed when query error",
fields: fields{
db: mockDB,
},
mock: func() {
mock.ExpectExec(expectQueryUpdate).WillReturnError(errors.New("got error"))
},
want: nil,
args: args{
ctx: ctx,
article: &models.Article{
ID: 1,
Title: "test",
Content: "test content",
CreateAt: timeNow,
UpdateAt: timeNow,
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &ArticleStore{
db: tt.fields.db,
}
tt.mock()
got, err := r.Update(tt.args.ctx, tt.args.article)
if (err != nil) != tt.wantErr {
t.Errorf("ArticleStore.Update() error = %v, wantErr %v", err, tt.wantErr)
return
}
assert.EqualExportedValues(t, got, tt.want)
})
}
}
Create Unit test for Store function
func TestArticleStore_Store(t *testing.T) {
ctx := context.Background()
mockDB, mock, err := sqlmock.New()
if err != nil {
t.Errorf("error")
}
// expect query
const expectQueryInsert = `INSERT INTO articles\(title, content, create_at, update_at\) VALUES\((.*),(.*),(.*),(.*)\)`
var timeNow = time.Now()
type fields struct {
db *sql.DB
}
type args struct {
ctx context.Context
article *models.Article
}
tests := []struct {
name string
fields fields
args args
want int64
wantErr bool
mock func()
}{
{
name: "success create article",
fields: fields{
db: mockDB,
},
mock: func() {
mock.ExpectExec(expectQueryInsert).WillReturnResult(
sqlmock.NewResult(1, 1),
)
},
want: 1,
args: args{
ctx: ctx,
article: &models.Article{
ID: 1,
Title: "test",
Content: "test content",
CreateAt: timeNow,
UpdateAt: timeNow,
},
},
wantErr: false,
},
{
name: "failed when query error",
fields: fields{
db: mockDB,
},
mock: func() {
mock.ExpectExec(expectQueryInsert).WillReturnError(errors.New("got error"))
},
want: 0,
args: args{
ctx: ctx,
article: &models.Article{
ID: 1,
Title: "test",
Content: "test content",
CreateAt: timeNow,
UpdateAt: timeNow,
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &ArticleStore{
db: tt.fields.db,
}
tt.mock()
got, err := r.Store(tt.args.ctx, tt.args.article)
if (err != nil) != tt.wantErr {
t.Errorf("ArticleStore.Store() error = %v, wantErr %v", err, tt.wantErr)
return
}
assert.Equal(t, got, tt.want)
})
}
}
Create unit test for Delete function
func TestArticleStore_Delete(t *testing.T) {
ctx := context.Background()
mockDB, mock, err := sqlmock.New()
if err != nil {
t.Errorf("error")
}
// expect query
const expectQueryDelete = `DELETE FROM articles WHERE id=(.*)`
type fields struct {
db *sql.DB
}
type args struct {
ctx context.Context
id int64
}
tests := []struct {
name string
fields fields
args args
want bool
wantErr bool
mock func()
}{
{
name: "success delete article",
fields: fields{
db: mockDB,
},
mock: func() {
mock.ExpectExec(expectQueryDelete).WillReturnResult(
sqlmock.NewResult(1, 1),
)
},
want: true,
args: args{
ctx: ctx,
id: 1,
},
wantErr: false,
},
{
name: "failed when query error",
fields: fields{
db: mockDB,
},
mock: func() {
mock.ExpectExec(expectQueryDelete).WillReturnError(errors.New("got error"))
},
want: false,
args: args{
ctx: ctx,
id: 1,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &ArticleStore{
db: tt.fields.db,
}
tt.mock()
got, err := r.Delete(tt.args.ctx, tt.args.id)
if (err != nil) != tt.wantErr {
t.Errorf("ArticleStore.Delete() error = %v, wantErr %v", err, tt.wantErr)
return
}
assert.Equal(t, got, tt.want)
})
}
}
Creating a Usecase level unit test
In making this Usecase level unit test, we also need to add a mock so that the unit test can be done at the upper level, namely the handler.
//go:generate mockery --name=ArticleUsecase --filename=usecase_mock.go --inpackage
type ArticleUsecase interface {
GetAll(ctx context.Context) ([]models.ArticleResponse, error)
GetByID(ctx context.Context, id int64) (models.ArticleResponse, error)
Update(ctx context.Context, article models.ArticleUpdateRequest) (models.ArticleResponse, error)
Store(ctx context.Context, article models.ArticleCreateRequest) (models.ArticleResponse, error)
Delete(ctx context.Context, id int64) (bool, error)
}
Do the same as at the repository level
go generate ./...
then a file called usecase_mock.go
will be created.
First create a unit test on initialization as below.
func TestNew(t *testing.T) {
type args struct {
repo repository.ArticleRepository
}
tests := []struct {
name string
args args
want *Usecase
}{
{
name: "success new",
want: &Usecase{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := New(tt.args.repo); !reflect.DeepEqual(got, tt.want) {
t.Errorf("New() = %v, want %v", got, tt.want)
}
})
}
}
Create unit test for GetAll
func TestUsecase_GetAll(t *testing.T) {
var ctx = context.Background()
var timeNow = time.Now()
mockRepository := new(repository.MockArticleRepository)
type fields struct {
articleRepository repository.ArticleRepository
}
type args struct {
ctx context.Context
}
tests := []struct {
name string
fields fields
args args
want []models.ArticleResponse
wantErr bool
mock func(args args)
}{
{
name: "success",
args: args{
ctx: ctx,
},
fields: fields{
articleRepository: mockRepository,
},
mock: func(args args) {
var res = []*models.Article{
{
ID: 1,
Title: "test",
Content: "test content",
CreateAt: timeNow,
UpdateAt: timeNow,
},
}
mockRepository.On("GetAll", args.ctx).Return(res, nil).Once()
},
want: []models.ArticleResponse{
{
ID: 1,
Title: "test",
Content: "test content",
CreateAt: timeNow,
UpdateAt: timeNow,
},
},
wantErr: false,
},
{
name: "failed get all data",
args: args{
ctx: ctx,
},
fields: fields{
articleRepository: mockRepository,
},
mock: func(args args) {
var res = []*models.Article{}
mockRepository.On("GetAll", args.ctx).Return(res, errors.New("got error")).Once()
},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mock(tt.args)
u := &Usecase{
articleRepository: tt.fields.articleRepository,
}
got, err := u.GetAll(tt.args.ctx)
if (err != nil) != tt.wantErr {
t.Errorf("Usecase.GetAll() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Usecase.GetAll() = %v, want %v", got, tt.want)
}
})
}
}
Create Unit test for GetByID
func TestUsecase_GetByID(t *testing.T) {
var ctx = context.Background()
var timeNow = time.Now()
mockRepository := new(repository.MockArticleRepository)
type fields struct {
articleRepository repository.ArticleRepository
}
type args struct {
ctx context.Context
id int64
}
tests := []struct {
name string
fields fields
args args
want models.ArticleResponse
wantErr bool
mock func(args args)
}{
{
name: "success",
args: args{
ctx: ctx,
id: 1,
},
fields: fields{
articleRepository: mockRepository,
},
mock: func(args args) {
var res = &models.Article{
ID: 1,
Title: "test",
Content: "test content",
CreateAt: timeNow,
UpdateAt: timeNow,
}
mockRepository.On("GetByID", args.ctx, args.id).Return(res, nil).Once()
},
want: models.ArticleResponse{
ID: 1,
Title: "test",
Content: "test content",
CreateAt: timeNow,
UpdateAt: timeNow,
},
wantErr: false,
},
{
name: "failed get by id",
args: args{
ctx: ctx,
},
fields: fields{
articleRepository: mockRepository,
},
mock: func(args args) {
var res = &models.Article{}
mockRepository.On("GetByID", args.ctx, args.id).Return(res, errors.New("got error")).Once()
},
want: models.ArticleResponse{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mock(tt.args)
u := &Usecase{
articleRepository: tt.fields.articleRepository,
}
got, err := u.GetByID(tt.args.ctx, tt.args.id)
if (err != nil) != tt.wantErr {
t.Errorf("Usecase.GetByID() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Usecase.GetByID() = %v, want %v", got, tt.want)
}
})
}
}
Create unit test for Update
func TestUsecase_Update(t *testing.T) {
var ctx = context.Background()
var timeNow = time.Now()
mockRepository := new(repository.MockArticleRepository)
type fields struct {
articleRepository repository.ArticleRepository
}
type args struct {
ctx context.Context
request models.ArticleUpdateRequest
}
tests := []struct {
name string
fields fields
args args
want models.ArticleResponse
wantErr bool
mock func(args args)
}{
{
name: "failed when request id was zero",
fields: fields{
articleRepository: mockRepository,
},
args: args{
ctx: ctx,
request: models.ArticleUpdateRequest{},
},
mock: func(args args) {},
want: models.ArticleResponse{},
wantErr: true,
},
{
name: "failed when get by repo",
fields: fields{
articleRepository: mockRepository,
},
args: args{
ctx: ctx,
request: models.ArticleUpdateRequest{
ID: 1,
},
},
mock: func(args args) {
var article *models.Article
mockRepository.On("GetByID", args.ctx, args.request.ID).Return(article, errors.New("got error")).Once()
},
want: models.ArticleResponse{},
wantErr: true,
},
{
name: "failed when get by id not found",
fields: fields{
articleRepository: mockRepository,
},
args: args{
ctx: ctx,
request: models.ArticleUpdateRequest{
ID: 1,
},
},
mock: func(args args) {
var article = &models.Article{}
mockRepository.On("GetByID", args.ctx, args.request.ID).Return(article, nil).Once()
},
want: models.ArticleResponse{},
wantErr: true,
},
{
name: "failed when update repo",
fields: fields{
articleRepository: mockRepository,
},
args: args{
ctx: ctx,
request: models.ArticleUpdateRequest{
ID: 1,
Title: "test update",
Content: "test update",
UpdateAt: timeNow,
},
},
mock: func(args args) {
var article = &models.Article{
ID: 1,
Title: "test",
Content: "test",
CreateAt: timeNow,
UpdateAt: timeNow,
}
mockRepository.On("GetByID", args.ctx, args.request.ID).Return(article, nil).Once()
article.FromUpdateRequest(args.request)
mockRepository.On("Update", args.ctx, article).Return(&models.Article{}, errors.New("got error"))
},
want: models.ArticleResponse{},
wantErr: true,
},
{
name: "success update",
fields: fields{
articleRepository: mockRepository,
},
args: args{
ctx: ctx,
request: models.ArticleUpdateRequest{
ID: 1,
Title: "test update",
Content: "test update",
UpdateAt: timeNow,
},
},
mock: func(args args) {
var article = &models.Article{
ID: 1,
Title: "test",
Content: "test",
CreateAt: timeNow,
UpdateAt: timeNow,
}
mockRepository.On("GetByID", args.ctx, args.request.ID).Return(article, nil).Once()
article.FromUpdateRequest(args.request)
mockRepository.On("Update", args.ctx, article).Return(article, nil).Once()
},
want: models.ArticleResponse{
ID: 1,
Title: "test update",
Content: "test update",
UpdateAt: timeNow,
CreateAt: timeNow,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mock(tt.args)
u := &Usecase{
articleRepository: tt.fields.articleRepository,
}
got, err := u.Update(tt.args.ctx, tt.args.request)
if (err != nil) != tt.wantErr {
t.Errorf("Usecase.Update() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got.ID, tt.want.ID) {
t.Errorf("Usecase.Update() = %v, want %v", got, tt.want)
}
if !reflect.DeepEqual(got.Title, tt.want.Title) {
t.Errorf("Usecase.Update() = %v, want %v", got, tt.want)
}
if !reflect.DeepEqual(got.Content, tt.want.Content) {
t.Errorf("Usecase.Update() = %v, want %v", got, tt.want)
}
if !reflect.DeepEqual(got.CreateAt, tt.want.CreateAt) {
t.Errorf("Usecase.Update() = %v, want %v", got, tt.want)
}
})
}
}
Create unit test for Store
func TestUsecase_Store(t *testing.T) {
var ctx = context.Background()
var timeNow = time.Now()
mockRepository := new(repository.MockArticleRepository)
type fields struct {
articleRepository repository.ArticleRepository
}
type args struct {
ctx context.Context
request models.ArticleCreateRequest
}
tests := []struct {
name string
fields fields
args args
want models.ArticleResponse
wantErr bool
mock func(args args)
}{
{
name: "failed store repository",
fields: fields{
articleRepository: mockRepository,
},
args: args{
ctx: ctx,
request: models.ArticleCreateRequest{
Title: "test",
Content: "test content",
CreateAt: timeNow,
},
},
mock: func(args args) {
var result int64
mockRepository.On("Store", args.ctx, mock.Anything).Return(result, errors.New("got error")).Once()
},
want: models.ArticleResponse{},
wantErr: true,
},
{
name: "success store repository",
fields: fields{
articleRepository: mockRepository,
},
args: args{
ctx: ctx,
request: models.ArticleCreateRequest{
Title: "test",
Content: "test content",
CreateAt: timeNow,
},
},
mock: func(args args) {
var result int64 = 1
mockRepository.On("Store", args.ctx, mock.Anything).Return(result, nil).Once()
},
want: models.ArticleResponse{
ID: 1,
Title: "test",
Content: "test content",
CreateAt: timeNow,
UpdateAt: timeNow,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mock(tt.args)
u := &Usecase{
articleRepository: tt.fields.articleRepository,
}
got, err := u.Store(tt.args.ctx, tt.args.request)
if (err != nil) != tt.wantErr {
t.Errorf("Usecase.Store() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got.ID, tt.want.ID) {
t.Errorf("Usecase.Store() = %v, want %v", got, tt.want)
}
if !reflect.DeepEqual(got.Title, tt.want.Title) {
t.Errorf("Usecase.Store() = %v, want %v", got, tt.want)
}
if !reflect.DeepEqual(got.Content, tt.want.Content) {
t.Errorf("Usecase.Store() = %v, want %v", got, tt.want)
}
})
}
}
Create unit test for Delete
func TestUsecase_Store(t *testing.T) {
var ctx = context.Background()
var timeNow = time.Now()
mockRepository := new(repository.MockArticleRepository)
type fields struct {
articleRepository repository.ArticleRepository
}
type args struct {
ctx context.Context
request models.ArticleCreateRequest
}
tests := []struct {
name string
fields fields
args args
want models.ArticleResponse
wantErr bool
mock func(args args)
}{
{
name: "failed store repository",
fields: fields{
articleRepository: mockRepository,
},
args: args{
ctx: ctx,
request: models.ArticleCreateRequest{
Title: "test",
Content: "test content",
CreateAt: timeNow,
},
},
mock: func(args args) {
var result int64
mockRepository.On("Store", args.ctx, mock.Anything).Return(result, errors.New("got error")).Once()
},
want: models.ArticleResponse{},
wantErr: true,
},
{
name: "success store repository",
fields: fields{
articleRepository: mockRepository,
},
args: args{
ctx: ctx,
request: models.ArticleCreateRequest{
Title: "test",
Content: "test content",
CreateAt: timeNow,
},
},
mock: func(args args) {
var result int64 = 1
mockRepository.On("Store", args.ctx, mock.Anything).Return(result, nil).Once()
},
want: models.ArticleResponse{
ID: 1,
Title: "test",
Content: "test content",
CreateAt: timeNow,
UpdateAt: timeNow,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mock(tt.args)
u := &Usecase{
articleRepository: tt.fields.articleRepository,
}
got, err := u.Store(tt.args.ctx, tt.args.request)
if (err != nil) != tt.wantErr {
t.Errorf("Usecase.Store() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got.ID, tt.want.ID) {
t.Errorf("Usecase.Store() = %v, want %v", got, tt.want)
}
if !reflect.DeepEqual(got.Title, tt.want.Title) {
t.Errorf("Usecase.Store() = %v, want %v", got, tt.want)
}
if !reflect.DeepEqual(got.Content, tt.want.Content) {
t.Errorf("Usecase.Store() = %v, want %v", got, tt.want)
}
})
}
}
func TestUsecase_Delete(t *testing.T) {
var ctx = context.Background()
var timeNow = time.Now()
mockRepository := new(repository.MockArticleRepository)
type fields struct {
articleRepository repository.ArticleRepository
}
type args struct {
ctx context.Context
id int64
}
tests := []struct {
name string
fields fields
args args
want bool
wantErr bool
mock func(args args)
}{
{
name: "failed when get by id was error",
fields: fields{
articleRepository: mockRepository,
},
args: args{
ctx: ctx,
id: 1,
},
mock: func(args args) {
var article *models.Article
mockRepository.On("GetByID", args.ctx, args.id).Return(article, errors.New("got error")).Once()
},
want: false,
wantErr: true,
},
{
name: "failed when article not found",
fields: fields{
articleRepository: mockRepository,
},
args: args{
ctx: ctx,
id: 1,
},
mock: func(args args) {
mockRepository.On("GetByID", args.ctx, args.id).Return(nil, nil).Once()
},
want: false,
wantErr: true,
},
{
name: "failed when delete repo was error",
fields: fields{
articleRepository: mockRepository,
},
args: args{
ctx: ctx,
id: 1,
},
mock: func(args args) {
var article = &models.Article{
ID: 1,
Title: "test",
Content: "test content",
CreateAt: timeNow,
UpdateAt: timeNow,
}
mockRepository.On("GetByID", args.ctx, args.id).Return(article, nil).Once()
mockRepository.On("Delete", args.ctx, args.id).Return(false, errors.New("got error")).Once()
},
want: false,
wantErr: true,
},
{
name: "success delete article",
fields: fields{
articleRepository: mockRepository,
},
args: args{
ctx: ctx,
id: 1,
},
mock: func(args args) {
var article = &models.Article{
ID: 1,
Title: "test",
Content: "test content",
CreateAt: timeNow,
UpdateAt: timeNow,
}
mockRepository.On("GetByID", args.ctx, args.id).Return(article, nil).Once()
mockRepository.On("Delete", args.ctx, args.id).Return(true, nil).Once()
},
want: true,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mock(tt.args)
u := &Usecase{
articleRepository: tt.fields.articleRepository,
}
got, err := u.Delete(tt.args.ctx, tt.args.id)
if (err != nil) != tt.wantErr {
t.Errorf("Usecase.Delete() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Usecase.Delete() = %v, want %v", got, tt.want)
}
})
}
}
Creating Unit tests on Handlers
At this handler level we don’t need a mock generator because we won’t use the handler for unit tests so there is no need to create or generate it.
Unlike the others, in this handler we create unit tests using the httptest
library which needs to be unit tested for handlers that have requests, responses and headers with the http protocol.
Starting with creating a unit test on initialization as below.
func TestNew(t *testing.T) {
mockUsecase := new(usecase.MockArticleUsecase)
type args struct {
articleUsecase usecase.ArticleUsecase
}
tests := []struct {
name string
args args
want *Delivery
}{
{
name: "success",
args: args{
articleUsecase: mockUsecase,
},
want: &Delivery{
articleUsecase: mockUsecase,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
New(tt.args.articleUsecase)
})
}
}
Unit test for GetAll
func TestDelivery_GetAll(t *testing.T) {
mockUsecase := new(usecase.MockArticleUsecase)
timeNow := time.Now().UTC()
type fields struct {
articleUsecase usecase.ArticleUsecase
validate *validator.Validate
}
type args struct {
params httprouter.Params
}
tests := []struct {
name string
fields fields
args args
wantStatusCode int
want models.ArticleListResponse
mock func(args args)
}{
{
name: "failed get all article",
fields: fields{
articleUsecase: mockUsecase,
},
mock: func(args args) {
var res []models.ArticleResponse
mockUsecase.On("GetAll", mock.Anything).Return(res, errors.New("got error")).Once()
},
wantStatusCode: http.StatusInternalServerError,
want: models.ArticleListResponse{
HeaderResponse: models.HeaderResponse{
Code: http.StatusInternalServerError,
Status: "got error",
},
},
},
{
name: "success get all article",
fields: fields{
articleUsecase: mockUsecase,
},
mock: func(args args) {
var res = []models.ArticleResponse{
{
ID: 1,
Title: "test",
Content: "test content",
CreateAt: timeNow,
UpdateAt: timeNow,
},
}
mockUsecase.On("GetAll", mock.Anything).Return(res, nil).Once()
},
wantStatusCode: http.StatusOK,
want: models.ArticleListResponse{
HeaderResponse: models.HeaderResponse{
Code: http.StatusOK,
Status: "OK",
},
Data: []models.ArticleResponse{
{
ID: 1,
Title: "test",
Content: "test content",
CreateAt: timeNow,
UpdateAt: timeNow,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/test", nil)
w := httptest.NewRecorder()
tt.mock(tt.args)
d := &Delivery{
articleUsecase: tt.fields.articleUsecase,
validate: tt.fields.validate,
}
d.GetAll(w, req, tt.args.params)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
var result models.ArticleListResponse
err := json.Unmarshal(body, &result)
if err != nil {
t.Errorf("error unmarshal %v", err)
}
assert.Equal(t, tt.wantStatusCode, resp.StatusCode)
assert.Equal(t, tt.want, result)
})
}
}
Unit test for GetByID
func TestDelivery_GetByID(t *testing.T) {
mockUsecase := new(usecase.MockArticleUsecase)
timeNow := time.Now().UTC()
type fields struct {
articleUsecase usecase.ArticleUsecase
validate *validator.Validate
}
type args struct {
params httprouter.Params
}
tests := []struct {
name string
fields fields
args args
wantStatusCode int
wantErr bool
want models.ArticleListResponse
mock func(args args)
}{
{
name: "failed get by id article",
fields: fields{
articleUsecase: mockUsecase,
},
args: args{
params: []httprouter.Param{{Key: "article_id", Value: "1"}},
},
mock: func(args args) {
var res = models.ArticleResponse{}
id, _ := strconv.ParseInt(args.params.ByName("article_id"), 0, 6)
mockUsecase.On("GetByID", mock.Anything, id).Return(res, errors.New("got error")).Once()
},
wantStatusCode: http.StatusInternalServerError,
want: models.ArticleListResponse{
HeaderResponse: models.HeaderResponse{
Code: http.StatusInternalServerError,
Status: "got error",
},
},
},
{
name: "failed params not found",
fields: fields{
articleUsecase: mockUsecase,
},
args: args{
params: []httprouter.Param{{Key: "article_id", Value: "a"}},
},
mock: func(args args) {},
wantStatusCode: http.StatusBadRequest,
want: models.ArticleListResponse{
HeaderResponse: models.HeaderResponse{
Code: http.StatusBadRequest,
Status: "strconv.ParseInt: parsing \"a\": invalid syntax",
},
},
},
{
name: "failed data not found",
fields: fields{
articleUsecase: mockUsecase,
},
args: args{
params: []httprouter.Param{{Key: "article_id", Value: "1"}},
},
mock: func(args args) {
var res = models.ArticleResponse{}
id, _ := strconv.ParseInt(args.params.ByName("article_id"), 0, 64)
mockUsecase.On("GetByID", mock.Anything, id).Return(res, nil).Once()
},
wantStatusCode: http.StatusNotFound,
want: models.ArticleListResponse{
HeaderResponse: models.HeaderResponse{
Code: http.StatusNotFound,
Status: "data not found",
},
},
},
{
name: "success get by id article",
fields: fields{
articleUsecase: mockUsecase,
},
args: args{
params: []httprouter.Param{{Key: "article_id", Value: "1"}},
},
mock: func(args args) {
var res = models.ArticleResponse{
ID: 1,
Title: "test",
Content: "test content",
CreateAt: timeNow,
UpdateAt: timeNow,
}
id, _ := strconv.ParseInt(args.params.ByName("article_id"), 0, 64)
mockUsecase.On("GetByID", mock.Anything, id).Return(res, nil).Once()
},
wantStatusCode: http.StatusOK,
want: models.ArticleListResponse{
HeaderResponse: models.HeaderResponse{
Code: http.StatusOK,
Status: "OK",
},
Data: []models.ArticleResponse{
{
ID: 1,
Title: "test",
Content: "test content",
CreateAt: timeNow,
UpdateAt: timeNow,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/test", nil)
w := httptest.NewRecorder()
tt.mock(tt.args)
d := &Delivery{
articleUsecase: tt.fields.articleUsecase,
validate: tt.fields.validate,
}
d.GetByID(w, req, tt.args.params)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
var result models.ArticleListResponse
err := json.Unmarshal(body, &result)
if err != nil {
t.Errorf("error unmarshal %v", err)
}
assert.Equal(t, tt.wantStatusCode, resp.StatusCode)
assert.Equal(t, tt.want, result)
})
}
}
unit test for Update
func TestDelivery_Update(t *testing.T) {
mockUsecase := new(usecase.MockArticleUsecase)
timeNow := time.Now().UTC()
type fields struct {
articleUsecase usecase.ArticleUsecase
validate *validator.Validate
}
type args struct {
params httprouter.Params
request models.ArticleUpdateRequest
}
tests := []struct {
name string
fields fields
args args
mock func(args args)
wantStatusCode int
wantErr bool
want models.ArticleListResponse
}{
{
name: "failed when article_id not support",
fields: fields{
articleUsecase: mockUsecase,
},
args: args{
params: []httprouter.Param{{Key: "article_id", Value: "a"}},
request: models.ArticleUpdateRequest{
ID: 1,
Title: "test data beneran",
Content: "test test data beneran",
UpdateAt: timeNow,
},
},
mock: func(args args) {
},
wantErr: true,
wantStatusCode: http.StatusBadRequest,
want: models.ArticleListResponse{
HeaderResponse: models.HeaderResponse{
Code: http.StatusBadRequest,
Status: "strconv.ParseInt: parsing \"a\": invalid syntax",
},
},
},
// {
// name: "failed when encode body",
// fields: fields{
// articleUsecase: mockUsecase,
// validate: validator.New(),
// },
// args: args{
// params: []httprouter.Param{{Key: "article_id", Value: "2"}},
// request: models.ArticleUpdateRequest{
// ID: 1,
// Title: "test",
// Content: "test",
// UpdateAt: timeNow,
// },
// },
// mock: func(args args) {},
// wantErr: true,
// wantStatusCode: http.StatusBadRequest,
// want: models.ArticleListResponse{
// HeaderResponse: models.HeaderResponse{
// Code: http.StatusBadRequest,
// Status: "strconv.ParseInt: parsing \"a\": invalid syntax",
// },
// },
// },
{
name: "failed when error validate",
fields: fields{
articleUsecase: mockUsecase,
validate: validator.New(),
},
args: args{
params: []httprouter.Param{{Key: "article_id", Value: "2"}},
request: models.ArticleUpdateRequest{
ID: 1,
Title: "test",
Content: "test",
UpdateAt: timeNow,
},
},
mock: func(args args) {},
wantErr: true,
wantStatusCode: http.StatusBadRequest,
want: models.ArticleListResponse{
HeaderResponse: models.HeaderResponse{
Code: http.StatusBadRequest,
Status: "Key: 'ArticleUpdateRequest.Title' Error:Field validation for 'Title' failed on the 'min' tag",
},
},
},
{
name: "failed when update usecase was error",
fields: fields{
articleUsecase: mockUsecase,
validate: validator.New(),
},
args: args{
params: []httprouter.Param{{Key: "article_id", Value: "2"}},
request: models.ArticleUpdateRequest{
ID: 2,
Title: "test data long",
Content: "test",
UpdateAt: timeNow,
},
},
mock: func(args args) {
var res models.ArticleResponse
mockUsecase.On("Update", mock.Anything, args.request).Return(res, errors.New("got error")).Once()
},
wantErr: true,
wantStatusCode: http.StatusInternalServerError,
want: models.ArticleListResponse{
HeaderResponse: models.HeaderResponse{
Code: http.StatusInternalServerError,
Status: "got error",
},
},
},
{
name: "success update article",
fields: fields{
articleUsecase: mockUsecase,
validate: validator.New(),
},
args: args{
params: []httprouter.Param{{Key: "article_id", Value: "2"}},
request: models.ArticleUpdateRequest{
ID: 2,
Title: "test data long",
Content: "test",
UpdateAt: timeNow,
},
},
mock: func(args args) {
var res models.ArticleResponse
mockUsecase.On("Update", mock.Anything, args.request).Return(res, nil).Once()
},
wantErr: false,
wantStatusCode: http.StatusOK,
want: models.ArticleListResponse{
HeaderResponse: models.HeaderResponse{
Code: http.StatusOK,
Status: "OK",
},
Data: []models.ArticleResponse{{}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var b = &bytes.Buffer{}
if tt.args.request.Title != "" {
err := json.NewEncoder(b).Encode(tt.args.request)
if err != nil {
t.Fatal(err)
}
}
req := httptest.NewRequest("POST", "/test", b)
w := httptest.NewRecorder()
tt.mock(tt.args)
d := &Delivery{
articleUsecase: tt.fields.articleUsecase,
validate: tt.fields.validate,
}
d.Update(w, req, tt.args.params)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
var result models.ArticleListResponse
err := json.Unmarshal(body, &result)
if err != nil {
t.Errorf("error unmarshal %v", err)
}
assert.Equal(t, tt.wantStatusCode, resp.StatusCode)
assert.Equal(t, tt.want, result)
})
}
}
Unit test for Store
func TestDelivery_Store(t *testing.T) {
mockUsecase := new(usecase.MockArticleUsecase)
timeNow := time.Now().UTC()
type fields struct {
articleUsecase usecase.ArticleUsecase
validate *validator.Validate
}
type args struct {
params httprouter.Params
request models.ArticleCreateRequest
}
tests := []struct {
name string
fields fields
args args
mock func(args args)
wantStatusCode int
wantErr bool
want models.ArticleListResponse
}{
// {
// name: "failed when parse json body",
// articleUsecase: mockUsecase,
// validate: validator.New(),
// },
// args: args{
// params: []httprouter.Param{{Key: "article_id", Value: "2"}},
// request: models.ArticleUpdateRequest{
// ID: 1,
// Title: "test",
// Content: "test",
// UpdateAt: timeNow,
// },
// },
// mock: func(args args) {},
// wantErr: true,
// wantStatusCode: http.StatusBadRequest,
// want: models.ArticleListResponse{
// HeaderResponse: models.HeaderResponse{
// Code: http.StatusBadRequest,
// Status: "strconv.ParseInt: parsing \"a\": invalid syntax",
// },
// },
// },
// },
{
name: "failed when error validate",
fields: fields{
articleUsecase: mockUsecase,
validate: validator.New(),
},
args: args{
params: []httprouter.Param{{Key: "article_id", Value: "2"}},
request: models.ArticleCreateRequest{
Title: "test",
Content: "test",
CreateAt: timeNow,
},
},
mock: func(args args) {},
wantErr: true,
wantStatusCode: http.StatusBadRequest,
want: models.ArticleListResponse{
HeaderResponse: models.HeaderResponse{
Code: http.StatusBadRequest,
Status: "Key: 'ArticleCreateRequest.Title' Error:Field validation for 'Title' failed on the 'min' tag",
},
},
},
{
name: "failed when update usecase was error",
fields: fields{
articleUsecase: mockUsecase,
validate: validator.New(),
},
args: args{
params: []httprouter.Param{{Key: "article_id", Value: "2"}},
request: models.ArticleCreateRequest{
Title: "test data long",
Content: "test",
CreateAt: timeNow,
},
},
mock: func(args args) {
var res models.ArticleResponse
mockUsecase.On("Store", mock.Anything, args.request).Return(res, errors.New("got error")).Once()
},
wantErr: true,
wantStatusCode: http.StatusInternalServerError,
want: models.ArticleListResponse{
HeaderResponse: models.HeaderResponse{
Code: http.StatusInternalServerError,
Status: "got error",
},
},
},
{
name: "success update article",
fields: fields{
articleUsecase: mockUsecase,
validate: validator.New(),
},
args: args{
params: []httprouter.Param{{Key: "article_id", Value: "2"}},
request: models.ArticleCreateRequest{
Title: "test data long",
Content: "test",
CreateAt: timeNow,
},
},
mock: func(args args) {
var res models.ArticleResponse
mockUsecase.On("Store", mock.Anything, args.request).Return(res, nil).Once()
},
wantErr: false,
wantStatusCode: http.StatusOK,
want: models.ArticleListResponse{
HeaderResponse: models.HeaderResponse{
Code: http.StatusOK,
Status: "OK",
},
Data: []models.ArticleResponse{{}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var b = &bytes.Buffer{}
if tt.args.request.Title != "" {
err := json.NewEncoder(b).Encode(tt.args.request)
if err != nil {
t.Fatal(err)
}
}
req := httptest.NewRequest("POST", "/test", b)
w := httptest.NewRecorder()
tt.mock(tt.args)
d := &Delivery{
articleUsecase: tt.fields.articleUsecase,
validate: tt.fields.validate,
}
d.Store(w, req, tt.args.params)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
var result models.ArticleListResponse
err := json.Unmarshal(body, &result)
if err != nil {
t.Errorf("error unmarshal %v", err)
}
assert.Equal(t, tt.wantStatusCode, resp.StatusCode)
assert.Equal(t, tt.want, result)
})
}
}
Unit test Delete
func TestDelivery_Delete(t *testing.T) {
mockUsecase := new(usecase.MockArticleUsecase)
type fields struct {
articleUsecase usecase.ArticleUsecase
validate *validator.Validate
}
type args struct {
params httprouter.Params
request models.ArticleCreateRequest
}
tests := []struct {
name string
fields fields
args args
mock func(args args)
wantStatusCode int
wantErr bool
want models.ArticleListResponse
}{
{
name: "failed delete by id article",
fields: fields{
articleUsecase: mockUsecase,
},
args: args{
params: []httprouter.Param{{Key: "article_id", Value: "1"}},
},
mock: func(args args) {
id, _ := strconv.ParseInt(args.params.ByName("article_id"), 0, 6)
mockUsecase.On("Delete", mock.Anything, id).Return(false, errors.New("got error")).Once()
},
wantStatusCode: http.StatusInternalServerError,
want: models.ArticleListResponse{
HeaderResponse: models.HeaderResponse{
Code: http.StatusInternalServerError,
Status: "got error",
},
},
},
{
name: "failed params not found",
fields: fields{
articleUsecase: mockUsecase,
},
args: args{
params: []httprouter.Param{{Key: "article_id", Value: "a"}},
},
mock: func(args args) {},
wantStatusCode: http.StatusBadRequest,
want: models.ArticleListResponse{
HeaderResponse: models.HeaderResponse{
Code: http.StatusBadRequest,
Status: "strconv.ParseInt: parsing \"a\": invalid syntax",
},
},
},
{
name: "failed data not found",
fields: fields{
articleUsecase: mockUsecase,
},
args: args{
params: []httprouter.Param{{Key: "article_id", Value: "0"}},
},
mock: func(args args) {
id, _ := strconv.ParseInt(args.params.ByName("article_id"), 0, 64)
mockUsecase.On("Delete", mock.Anything, id).Return(false, nil).Once()
},
wantStatusCode: http.StatusNotFound,
want: models.ArticleListResponse{
HeaderResponse: models.HeaderResponse{
Code: http.StatusNotFound,
Status: "article_id was not zero",
},
},
},
{
name: "success delete by id article",
fields: fields{
articleUsecase: mockUsecase,
},
args: args{
params: []httprouter.Param{{Key: "article_id", Value: "1"}},
},
mock: func(args args) {
id, _ := strconv.ParseInt(args.params.ByName("article_id"), 0, 64)
mockUsecase.On("Delete", mock.Anything, id).Return(true, nil).Once()
},
wantStatusCode: http.StatusOK,
want: models.ArticleListResponse{
HeaderResponse: models.HeaderResponse{
Code: http.StatusOK,
Status: "OK",
},
},
},
{
name: "delete by article_id but data not found",
fields: fields{
articleUsecase: mockUsecase,
},
args: args{
params: []httprouter.Param{{Key: "article_id", Value: "1"}},
},
mock: func(args args) {
id, _ := strconv.ParseInt(args.params.ByName("article_id"), 0, 64)
mockUsecase.On("Delete", mock.Anything, id).Return(false, nil).Once()
},
wantStatusCode: http.StatusInternalServerError,
wantErr: true,
want: models.ArticleListResponse{
HeaderResponse: models.HeaderResponse{
Code: http.StatusInternalServerError,
Status: "unknown error",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var b = &bytes.Buffer{}
if tt.args.request.Title != "" {
err := json.NewEncoder(b).Encode(tt.args.request)
if err != nil {
t.Fatal(err)
}
}
req := httptest.NewRequest("GET", "/test", b)
w := httptest.NewRecorder()
tt.mock(tt.args)
d := &Delivery{
articleUsecase: tt.fields.articleUsecase,
validate: tt.fields.validate,
}
d.Delete(w, req, tt.args.params)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
var result models.ArticleListResponse
err := json.Unmarshal(body, &result)
if err != nil {
t.Errorf("error unmarshal %v", err)
}
assert.Equal(t, tt.wantStatusCode, resp.StatusCode)
assert.Equal(t, tt.want, result)
})
}
}
All unit tests have been made so that all our functions can be tested properly and adjust to the needs of friends. This unit test is very important when we want to make improvements or add needs when there are changes, the unit test is used to test the existing flow and the newly added flow so that no errors occur in the system in the future.
Furthermore, to make it easier to run unit tests we need to add the unit test command to the Makefile
as follows
mock:
go generate ./...
test:
go test -race -cover ./...
The mock
command is used to re-generate when we add or change a function in an interface that has a mock. As for test
we use it to run unit test commands from all functions in this project.
Here is an example of our results running
➜ make test
go test -race -cover ./...
? github.com/santekno/learn-golang-restful/cmd [no test files]
? github.com/santekno/learn-golang-restful/internal/repository [no test files]
? github.com/santekno/learn-golang-restful/internal/usecase [no test files]
ok github.com/santekno/learn-golang-restful/internal/delivery/http (cached) coverage: 94.1% of statements
ok github.com/santekno/learn-golang-restful/internal/middleware (cached) coverage: 100.0% of statements
ok github.com/santekno/learn-golang-restful/internal/models (cached) coverage: 100.0% of statements
ok github.com/santekno/learn-golang-restful/internal/repository/mysql (cached) coverage: 94.7% of statements
? github.com/santekno/learn-golang-restful/pkg/database [no test files]
ok github.com/santekno/learn-golang-restful/internal/usecase/article (cached) coverage: 100.0% of statements
ok github.com/santekno/learn-golang-restful/pkg/middleware-chain (cached) coverage: 100.0% of statements
ok github.com/santekno/learn-golang-restful/pkg/util (cached) coverage: 83.3% of statements