AI-translated. Read the original in KoreanEmbedded slides, videos and images remain in their original language.
Go Testing
This document provides Go testing methods and guidelines, including recommended libraries and how to use them. These are recommendations only; projects are free to adopt or propose other approaches. Even then, we encourage consistency within each project.
Testing Libraries
Testify
- For test suites
- For mocking
Faker
If every field in a test object has a fixed value, or fields are left empty, tests cannot cover a variety of cases. Faker lets selected object fields take random values.
Go SQLMock
gopkg.in/DATA-DOG/go-sqlmock.v2
- Validates queries themselves without a database dependency or actually executing the queries.
Guidelines
Test File and Package Names
- Match the test filename to the Go file being tested.
- Append
_testto the package under test for black-box testing. - If the package’s internal implementation is complex, you may use the same package name for white-box testing.
| Target | Test | |
|---|---|---|
| File | controller.go | controller_test.go |
| Package | package articlesvc | package articlsvc_test (package articlesvc) |
| Function | GetArticles() | Test_GetArticles() |
Mocking
- Mock referenced objects when testing.
Writing Unit Tests
Defining a Test Suite
Use a test suite to initialize and clean up each unit test. SetupTest initializes the object under test and any variables that need to be reset for each test. TearDownTest cleans up changes made during the test. For details, see Suite Package.
type ControllerTestSuite struct {
suite.Suite
controller articlesvc.Controller
engine *core.Engine
usecase *mocks.Usecase
}
func (ts *ControllerTestSuite) SetupTest() {
ts.engine = echo.New()
ts.usecase = new(mocks.Usecase)
ts.controller = articlesvc.NewController(ts.engine, ts.usecase)
}
// Write test definition with TestSuite.
func TestControllerSuite(t *testing.T) {
suite.Run(t, new(ControllerTestSuite))
}
Preparing Mock Objects
Create mock objects using mock.Mock. The following example builds a use-case mock. Implement it so the caller can specify each method’s response.
var _ article.Usecase = &Usecase{}
type Usecase struct {
mock.Mock
}
func (u *Usecase) GetBy(lang string) ([]*article.Article, error) {
ret := u.Called(lang)
return ret.Get(0).([]*article.Article), ret.Error(1)
}
func (u *Usecase) Save(a *article.Article) error {
ret := u.Called(a)
return ret.Error(0)
}
func (u *Usecase) Delete(a *article.Article) error {
ret := u.Called(a)
return ret.Error(0)
}
Writing Test Cases
Test every public method of the target class. The example below uses the mock created above to write controller tests. When mocking use-case methods, specify which value (en) should be passed to which method (GetBy), and define the response.
func (ts *ControllerTestSuite) TestController_GetArticles() {
// Given
var mockArticles []*article.Article
err := faker.FakeData(&mockArticles)
ts.NoError(err)
ts.usecase.On("GetBy", "en").Return(mockArticles, nil).Once() // 유스케이스 함수 모킹
req := (&network.Request{
Method: http.MethodGet,
Url: "/articles",
Params: &url.Values{
"lang": []string{"en"},
},
}).Build()
ts.NoError(err)
ctx, rec := ts.buildContextAndRecorder(req.GetHttpRequest())
// When
err = ts.controller.GetArticles(ctx)
// Then
ts.NoError(err)
ts.Equal(http.StatusOK, rec.Code)
ts.usecase.AssertExpectations(ts.T())
}
Defining the Test Runner
Once you have written tests for the target methods in a suite, write the test code that runs the suite. Its name must begin with Test and it must accept a *testing.T parameter.
func TestControllerSuite(t *testing.T) {
suite.Run(t, new(ControllerTestSuite))
}
Test Code Examples
Project Layout
Dependency flow: controller → use case in the Article domain → entity / repository → DbArticleGormSrouce in the data source
Test Targets and Examples
Controller
- api/articlesvc/controller.go
- Dependencies: Article domain use cases (Get / Save / Delete)
[go-rest-sample] controller_test.go
package articlesvc_test
// Step 3. Write test cases for each public method for the testing target.
// In this case, articles controller is the testing target and it has 3 functions. (Get / Post / Delete)
func (ts *ControllerTestSuite) TestController_GetArticles() {
// Given
var mockArticles []*article.Article
err := faker.FakeData(&mockArticles)
ts.NoError(err)
ts.usecase.On("GetBy", "en").Return(mockArticles, nil).Once()
req := (&network.Request{
Method: http.MethodGet,
Url: "/articles",
Params: &url.Values{
"lang": []string{"en"},
},
}).Build()
ts.NoError(err)
ctx, rec := ts.buildContextAndRecorder(req.GetHttpRequest())
// When
err = ts.controller.GetArticles(ctx)
// Then
ts.NoError(err)
ts.Equal(http.StatusOK, rec.Code)
ts.usecase.AssertExpectations(ts.T())
}
func (ts *ControllerTestSuite) TestController_PostArticles() {
var mockArticle article.Article
err := faker.FakeData(&mockArticle)
mockArticle.ID = 0 // 새로운 Article
ts.NoError(err)
ts.usecase.On("Save", mock.Anything).Return(nil).Once()
req := (&network.Request{
Method: http.MethodPost,
Url: "/articles",
Params: &url.Values{
"title": []string{mockArticle.Title},
"content": []string{mockArticle.Content},
"lang": []string{mockArticle.Lang},
},
}).Build()
ctx, rec := ts.buildContextAndRecorder(req.GetHttpRequest())
err = ts.controller.PostArticles(ctx)
ts.NoError(err)
ts.Equal(http.StatusOK, rec.Code)
ts.usecase.AssertExpectations(ts.T())
}
func (ts *ControllerTestSuite) TestController_DeleteArticles() {
var mockArticle article.Article
err := faker.FakeData(&mockArticle)
ts.NoError(err)
ts.usecase.On("Delete", mock.Anything).Return(nil).Once()
req := (&network.Request{
Method: http.MethodDelete,
Url: "/articles",
Params: &url.Values{
"id": []string{strconv.FormatInt(mockArticle.ID, 10)},
},
}).Build()
ctx, rec := ts.buildContextAndRecorder(req.GetHttpRequest())
err = ts.controller.DeleteArticles(ctx)
ts.NoError(err)
ts.Equal(http.StatusOK, rec.Code)
ts.usecase.AssertExpectations(ts.T())
}
func (ts *ControllerTestSuite) buildContextAndRecorder(httpRequest *http.Request) (ctx core.Context, rec *httptest.ResponseRecorder) {
rec = httptest.NewRecorder()
ctx = ts.engine.NewContext(httpRequest, rec)
return
}
// Step 2. Write test definition with TestSuite.
func TestControllerSuite(t *testing.T) {
suite.Run(t, new(ControllerTestSuite))
}
// Step 1. Define a test suite.
// Testing target and some pre-defined members should be initialized.
// In this case, controller is testing target and usecase is defined for mocking.
type ControllerTestSuite struct {
suite.Suite
controller articlesvc.Controller
engine *core.Engine
usecase *mocks.Usecase
}
func (ts *ControllerTestSuite) SetupTest() {
ts.engine = echo.New()
ts.usecase = new(mocks.Usecase)
ts.controller = articlesvc.NewController(ts.engine, ts.usecase)
}
Use Case
- article/usecase.go
- Dependencies: entity.go and repository.go in the domain
[go-rest-sample] usecase_test.go
package article_test
func (ts *UsecaseTestSuite) TestArticleUsecase_GetBy() {
var mockArticles []*article.Article
lang := "en"
err := faker.FakeData(&mockArticles)
ts.NoError(err)
for _, mn := range mockArticles {
mn.Lang = lang
}
ts.repo.On("Find", `lang = ?`, []interface{}{lang}).Return(mockArticles, nil).Once()
results, err := ts.usecase.GetBy("en")
ts.NoError(err)
ts.Equal(mockArticles, results)
ts.repo.AssertExpectations(ts.T())
}
func (ts *UsecaseTestSuite) TestArticleUsecase_Save() {
var mockArticle article.Article
err := faker.FakeData(&mockArticle)
ts.NoError(err)
mockArticle.ID = 0
ts.repo.On("Save", &mockArticle).Return(nil).Once()
err = ts.usecase.Save(&mockArticle)
ts.NoError(err)
ts.repo.AssertExpectations(ts.T())
}
func (ts *UsecaseTestSuite) TestArticleUsecase_Delete() {
var mockArticle article.Article
err := faker.FakeData(&mockArticle)
ts.NoError(err)
ts.repo.On("Delete", &mockArticle).Return(nil).Once()
err = ts.usecase.Delete(&mockArticle)
ts.NoError(err)
ts.repo.AssertExpectations(ts.T())
}
func TestControllerSuite(t *testing.T) {
suite.Run(t, new(UsecaseTestSuite))
}
var (
_ suite.SetupTestSuite = &UsecaseTestSuite{}
)
type UsecaseTestSuite struct {
suite.Suite
repo *mocks.Repository
usecase article.Usecase
}
func (ts *UsecaseTestSuite) SetupTest() {
ts.repo = new(mocks.Repository)
ts.usecase = article.NewUsecase(ts.repo)
}
[installedappsvc] usecase_test.go
package installedapp
func TestGetInstalledApps(t *testing.T) {
uid := int64(10001)
apps := []InstalledApp{InstalledApp{UserID: uid, PkgName: "com.test.package.1"}}
mockRepo := new(mockRepo)
mockRepo.On("GetByUserID", uid).Return(apps, nil).Once()
u := NewUsecase(mockRepo)
pkgs, err := u.GetInstalledApps(uid)
mockRepo.AssertExpectations(t)
assert.Nil(t, err)
assert.Equal(t, 1, len(pkgs))
assert.Equal(t, "com.test.package.1", pkgs[0].PkgName)
}
func TestSaveInstalledApps(t *testing.T) {
uid := int64(10001)
pkgName := "com.test.package.1"
pkgNames := []string{pkgName}
apps := []InstalledApp{InstalledApp{UserID: uid, PkgName: pkgName}}
mockRepo := new(mockRepo)
mockRepo.On("Create", apps).Return(nil).Once()
mockRepo.On("DeleteByUserIDExcept", uid, pkgNames).Return(nil).Once()
u := NewUsecase(mockRepo)
u.SaveInstalledApps(uid, pkgNames)
mockRepo.AssertExpectations(t)
}
Repository
- article/repo/repo.go
- Dependencies: datasource/dbarticle/source.go
[go-rest-sample] repo_test.go
//https://github.com/jirfag/go-queryset/blob/master/queryset/queryset_test.go
func (ts *RepoTestSuite) TestArticleRepository_Find() {
var mockArticles []*dbarticle.Article
lang := "en"
err := faker.FakeData(&mockArticles)
ts.NoError(err)
for _, mn := range mockArticles {
mn.Lang = lang
}
ts.dbSource.On("Find", `lang = ?`, []interface{}{[]interface{}{"en"}}).Return(mockArticles, nil).Once()
var results []*article.Article
results, err = ts.repo.Find("lang = ?", "en")
ts.NoError(err)
ts.Equal(len(mockArticles), len(results))
ts.dbSource.AssertExpectations(ts.T())
}
func (ts *RepoTestSuite) TestArticleRepository_Save() {
var mockDBArticle dbarticle.Article
err := faker.FakeData(&mockDBArticle)
ts.NoError(err)
mockDBArticle.ID = 0
mockDBArticle.DeletedAt = nil
mockDBArticle.CreatedAt = time.Time{}
mockDBArticle.UpdatedAt = time.Time{}
ts.dbSource.On("Save", &mockDBArticle).Return(nil).Once()
mockArticle := repo.DBArticleToArticle(mockDBArticle)
err = ts.repo.Save(&mockArticle)
ts.NoError(err)
ts.dbSource.AssertExpectations(ts.T())
}
func (ts *RepoTestSuite) TestArticleRepository_Delete() {
mockDBArticle := dbarticle.Article{
ID: 1,
}
mockDBArticle.DeletedAt = nil
ts.dbSource.On("Delete", &mockDBArticle).Return(nil).Once()
mockArticle := repo.DBArticleToArticle(mockDBArticle)
err := ts.repo.Delete(&mockArticle)
ts.NoError(err)
ts.dbSource.AssertExpectations(ts.T())
}
func TestRepoSuite(t *testing.T) {
suite.Run(t, new(RepoTestSuite))
}
type RepoTestSuite struct {
suite.Suite
dbSource *dbarticle.MockDBSource
repo article.Repository
}
func (ts *RepoTestSuite) SetupTest() {
ts.dbSource = &dbarticle.MockDBSource{}
ts.repo = repo.New(ts.dbSource)
}
[installedappsvc] repo_test.go
package iarepo
type repoTestSuite struct {
suite.Suite
db *sql.DB
}
func TestRepoTestSuite(t *testing.T) {
suite.Run(t, new(repoTestSuite))
}
func (s *repoTestSuite) SetupSuite() {
db, err := sql.Open("postgres", "")
require.Nil(s.T(), err)
s.db = db
}
func (s *repoTestSuite) SetupTest() {
_, err := s.db.Exec("DELETE FROM installed_apps")
require.Nil(s.T(), err)
}
func (s *repoTestSuite) TearDownSuite() {
s.db.Close()
}
func (s *repoTestSuite) TestGetByUserID() {
require := require.New(s.T())
assert := assert.New(s.T())
uid := int64(10001)
pkgName := "com.test.package.1"
_, err := s.db.Exec("INSERT INTO installed_apps (user_id, package_name) VALUES ($1, $2)", uid, pkgName)
require.Nil(err)
r := New(s.db)
as, err := r.GetByUserID(uid)
require.Nil(err)
require.Equal(1, len(as))
assert.Equal(pkgName, as[0].PkgName)
}
func (s *repoTestSuite) TestCreate() {
require := require.New(s.T())
assert := assert.New(s.T())
uid := int64(10001)
as := []ia.InstalledApp{
ia.InstalledApp{UserID: uid, PkgName: "com.test.package.1"},
ia.InstalledApp{UserID: uid, PkgName: "com.test.package.2"},
}
r := New(s.db)
err := r.Create(as)
require.Nil(err)
rows, err := s.db.Query("SELECT user_id, package_name, uninstalled_at FROM installed_apps")
require.Nil(err)
require.True(rows.Next())
var ra ia.InstalledApp
var ts *time.Time
rows.Scan(&ra.UserID, &ra.PkgName, &ts)
a := as[0]
assert.Equal(a.UserID, ra.UserID)
assert.Equal(a.PkgName, ra.PkgName)
assert.Nil(ts)
}
func (s *repoTestSuite) TestCreateWhenUninstalledPackageExists() {
require := require.New(s.T())
assert := assert.New(s.T())
uid := int64(10001)
pkgName := "com.test.package.1"
_, err := s.db.Exec("INSERT INTO installed_apps (user_id, package_name, uninstalled_at) VALUES ($1, $2, $3)", uid, pkgName, time.Now())
require.Nil(err)
as := []ia.InstalledApp{ia.InstalledApp{UserID: uid, PkgName: pkgName}}
r := New(s.db)
err = r.Create(as)
require.Nil(err)
rows, err := s.db.Query("SELECT uninstalled_at FROM installed_apps")
require.Nil(err)
require.True(rows.Next())
var ts *time.Time
rows.Scan(&ts)
assert.Nil(ts)
}
func (s *repoTestSuite) TestDeleteByUserIDExcept() {
require := require.New(s.T())
assert := assert.New(s.T())
uid := int64(10001)
pkgName := "com.test.package.1"
_, err := s.db.Exec("INSERT INTO installed_apps (user_id, package_name) VALUES ($1, $2)", uid, pkgName)
require.Nil(err)
r := New(s.db)
err = r.DeleteByUserIDExcept(uid, []string{})
require.Nil(err)
rows, err := s.db.Query("SELECT uninstalled_at FROM installed_apps")
require.Nil(err)
require.True(rows.Next())
var ts *time.Time
rows.Scan(&ts)
assert.NotNil(ts)
}
func (s *repoTestSuite) TestDeleteByUserIDExceptWhenFilterMatches() {
require := require.New(s.T())
assert := assert.New(s.T())
uid := int64(10001)
pkgName := "com.test.package.1"
_, err := s.db.Exec("INSERT INTO installed_apps (user_id, package_name) VALUES ($1, $2)", uid, pkgName)
require.Nil(err)
r := New(s.db)
err = r.DeleteByUserIDExcept(uid, []string{pkgName})
require.Nil(err)
rows, err := s.db.Query("SELECT uninstalled_at FROM installed_apps")
require.Nil(err)
require.True(rows.Next())
var ts *time.Time
rows.Scan(&ts)
assert.Nil(ts)
}
Data Source
- datasource/dbarticle/gorm_source.go
- Dependencies: gorm
[go-rest-sample] gorm_source_test.go
package dbarticle_test
//https://github.com/jirfag/go-queryset/blob/master/queryset/queryset_test.go
func (ts *RepoTestSuite) TestGormArticleRepository_Find() {
articles := getTestArticles(2)
req := "SELECT * FROM `articles` WHERE `articles`.`deleted_at` IS NULL"
ts.mock.ExpectQuery(fixedFullRe(req)).
WillReturnRows(getRowsForArticles(articles))
var results []*dbarticle.Article
results, err := ts.dbSource.Find("")
ts.NoError(err)
ts.Equal(articles, results)
}
func (ts *RepoTestSuite) TestGormArticleRepository_Save() {
n := getTestArticles(1)[0]
query := "INSERT INTO `articles` (`title`,`content`,`lang`,`author_id`,`created_at`,`updated_at`,`deleted_at`) VALUES (?,?,?,?,?,?,?)"
args := []driver.Value{n.Title, sqlmock.AnyArg(), n.Lang, sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), nil}
ts.mock.ExpectExec(fixedFullRe(query)).
WithArgs(args...).
WillReturnResult(sqlmock.NewResult(1, 1))
err := ts.dbSource.Save(n)
ts.NoError(err)
}
func (ts *RepoTestSuite) TestGormArticleRepository_Delete() {
n := getTestArticles(1)[0]
n.ID = 1
query := "UPDATE `articles` SET `deleted_at`=? WHERE `articles`.`deleted_at` IS NULL AND `articles`.`id` = ?"
args := []driver.Value{sqlmock.AnyArg(), n.ID}
ts.mock.ExpectExec(fixedFullRe(query)).
WithArgs(args...).
WillReturnResult(sqlmock.NewResult(1, 1))
err := ts.dbSource.Delete(n)
ts.NoError(err)
}
func getTestArticles(num int) []*dbarticle.Article {
articles := make([]*dbarticle.Article, 0)
for i := 0; i < num; i++ {
n := &dbarticle.Article{
Title: fmt.Sprintf("Title - %d", i),
Lang: "en",
}
articles = append(articles, n)
}
return articles
}
func getRowsForArticles(articles []*dbarticle.Article) *sqlmock.Rows {
var fieldNames = []string{"id", "title", "content", "lang"}
rows := sqlmock.NewRows(fieldNames)
for _, n := range articles {
rows = rows.AddRow(n.ID, n.Title, n.Content, n.Lang)
}
return rows
}
func fixedFullRe(s string) string {
return fmt.Sprintf("^%s$", regexp.QuoteMeta(s))
}
func TestRepoSuite(t *testing.T) {
suite.Run(t, new(RepoTestSuite))
}
type RepoTestSuite struct {
suite.Suite
mock sqlmock.Sqlmock
db *gorm.DB
dbSource dbarticle.DBSource
}
func (ts *RepoTestSuite) SetupTest() {
db, mock, err := sqlmock.New()
ts.NoError(err)
ts.mock = mock
ts.db, err = gorm.Open("mysql", db)
ts.NoError(err)
ts.db.LogMode(true)
ts.db = ts.db.Set("gorm:update_column", true)
ts.dbSource = dbarticle.NewSource(ts.db)
}
func (ts *RepoTestSuite) AfterTest(suiteName, testName string) {
_ = ts.db.Close()
}
Related guides
All guidesPython Testing
This document provides methods and guidelines for testing Python code, including recommended libraries and how to use them. These are recommendations: projects may adopt or propose…
Testing Principles
Well-designed tests help us build products faster and more reliably. This document provides the basic principles and guidelines for writing tests. Seven Testing Principles For a de…