Skip to content
:tech
← Handbook

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

github.com/stretchr/testify

  • For test suites
  • For mocking

Faker

github.com/bxcodec/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 _test to 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.
TargetTest
Filecontroller.gocontroller_test.go
Packagepackage articlesvcpackage articlsvc_test (package articlesvc)
FunctionGetArticles()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()
}

All guides
Handbook

Python 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…

Handbook

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…