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
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.