1

I'm setting unit test for api(golang).
It seems to use mocking. But I don't understand how to code to success.

article
  ├ client
  ├ api
  │  ├ main.go
  │  ├ contoroller
  │  │    ├ contoroller.go
  │  │    └ contoroller_test.go
  │  ├ service
  │  │    ├ service.go
  │  │    └ service_test.go
  │  ├ dao
  │  │    ├ dao.go
  │  │    └ dao_test.go
  │  ├ s3
  │  │    ├ s3.go
  │  │    └ s3_test.go
  │  ├ go.mod 
  │  ├ go.sum
  │  └ Dockerfile
  ├ nginx
  └ docker-compose.yml

Now I'm trying to set dao_test.go But it fails because dao.go calls method from s3.dao.

dao_test.go

package dao

// import

type DaoSuite struct {
    suite.Suite
    db   *sql.DB
    mock sqlmock.Sqlmock
    dao  *Dao
    s3   *s3.S3
}

func (s *DaoSuite) SetupTest() {

    var err error
    s.db, s.mock, err = sqlmock.New()
    s.Require().NoError(err)
    s.dao = NewDao(s.db, s.s3)
}

func (s *DaoSuite) TestDeleteArticleDao() {

    // some method

    // here test fails because DeleteArticleDao calls method from another package.
    s.dao.DeleteArticleDao("1")

}

func (s *DaoSuite) TearDownTest() {
    s.db.Close()
    s.Assert().NoError(s.mock.ExpectationsWereMet())
}

dao.go

package dao

// import

type Dao struct {
    database *sql.DB
    s3       *s3.S3
}

func NewDao(database *sql.DB, s3 *s3.S3) *Dao {
    objs := &Dao{database: database, s3: s3}
    return objs
}

func (d *Dao) DeleteArticleDao(id string) {
    //generate imageName

    //here calls method in package s3
    //here test fails 
    d.s3.DeleteS3Image(imageName)

}

s3.go

package s3

//import

type S3 struct {
    APPID  string
    SECRET string
}

type DaoInterface interface {
    DeleteS3Image(imageName util.ImageName) error
}

func NewS3(appid, secret string) *S3 {
    objs := &S3{APPID: appid, SECRET: secret}
    return objs
}


func (objs *S3) DeleteS3Image(imageName util.ImageName) error {
    // method
}

The full source code is here(fix-test-dao):
https://github.com/jpskgc/article/tree/fix-test-dao

I expect the test success in dao_test.go.
But the actual is it fails because dao.go calls method from s3 package.
I want to know how to mock DeleteS3Image in package s3 to avoid error and success test.

Here is the error when running go test -v at dao_test.go.

$ go test -v
--- FAIL: TestDaoSuite (0.00s)
    --- FAIL: TestDaoSuite/TestDeleteArticleDao (0.00s)
        dao_test.go:221: 
                Error Trace:    dao_test.go:221
                                                        suite.go:122
                                                        panic.go:522
                                                        panic.go:82
                                                        signal_unix.go:390
                                                        s3.go:66
                                                        dao.go:74
                                                        dao_test.go:156
                Error:          Received unexpected error:
                                there is a remaining expectation which was not matched: ExpectedBegin => expecting database transaction Begin
                Test:           TestDaoSuite/TestDeleteArticleDao
        suite.go:61: test panicked: runtime error: invalid memory address or nil pointer dereference
jpskgc.v5
  • 249
  • 1
  • 5
  • 9

1 Answers1

0

In your setup you do call s.dao = NewDao(s.db, s.s3) however you've never initialized s.s3 to anything, so s.dao.s3 remains nil and that's why d.s3.DeleteS3Image(imageName) panics.


In Go to be able to mock a method, the value on which the method is called must be an interface, not a concrete type. Put another way, it is not possible to mock a concrete method in Go.

So with a type like this:

type Dao struct {
    database *sql.DB
    s3       *s3.S3
}

you simply cannot mock s3.

What you can do, is change the type of the s3 field to an interface type, you already have one ready (s3.DaoInterface).

type Dao struct {
    database *sql.DB
    s3       s3.DaoInterface
}

now you can mock the s3 field.

What's left is for you to implement your mock and make sure that the s3 field is set to an instance of the mock implementation during test setup.

type MockS3 struct{}

func (MockS3) DeleteS3Image(imageName util.ImageName) error {
    // do whatever
    return nil
}

func (s *DaoSuite) SetupTest() {

    var err error
    s.db, s.mock, err = sqlmock.New()
    s.Require().NoError(err)
    s.dao = NewDao(s.db, s.s3)
    s.dao.s3 = MockS3{} // <- don't forget about me
}

It is up to you how you implement the mock, but if you're new to mocks I would recommend you take a look at https://github.com/golang/mock to help you with generating mocks.

mkopriva
  • 35,176
  • 4
  • 57
  • 71
  • Thanks for advice. when I follow this advice, I face compile error issue in dao_test.go. Please take a look at https://stackoverflow.com/questions/57840793/cannot-use-mockdaointerface-literal-type-mockdaointerface-as-type-s3-daointerf – jpskgc.v5 Sep 08 '19 at 09:43
  • @jpskgc.v5 You can see in my example I've implemented `DeleteS3Image` on a non-pointer `MockS3` therefore I can use `MockS3{}`. In your version you've implemented the method on a pointer `*MockDaoInterface` therefore you have to use `&MockDaoInterface{}`. – mkopriva Sep 08 '19 at 09:47