Unit testing with gorm - unit-testing

I have found a lot of questions regarding gorm mocking related to the old V1 package: github.com/jinzhu/gorm. with the usage of github.com/DATA-DOG/go-sqlmock.
I didn't find much with the v2.
My simple question is:
Suppose I have this storage package code:
...
type Storage struct {
GormDB *gorm.DB
SqlDB *sql.DB
mutex sync.Mutex
ReadTimeout int
WriteTimeout int
}
func (ps *Storage) Open(settings *Settings) error {
if err := settings.Validate(); err != nil {
return err
}
ps.mutex.Lock()
defer ps.mutex.Unlock()
if ps.GormDB != nil {
return nil
}
gormDB, err := gorm.Open(postgres.New(postgres.Config{
DSN: settings.GetDSN(),
}), &gorm.Config{
SkipDefaultTransaction: true,
})
if err != nil {
return fmt.Errorf("%s: %v", DBConnectError, err)
}
ps.GormDB = gormDB
sqlDB, err := ps.GormDB.DB()
if err != nil {
return fmt.Errorf("%s: %v", DBRetrievalError, err)
}
ps.SqlDB = sqlDB
ps.SqlDB.SetMaxIdleConns(settings.MaxIdleConnections)
ps.SqlDB.SetMaxOpenConns(settings.MaxOpenConnections)
ps.ReadTimeout = settings.ReadTimeout
ps.WriteTimeout = settings.WriteTimeout
return nil
}
How can I unit-test this function with a simple check that gorm.Open received the expected config?
I don't see any other way than passing the ORM interface to this method... It would be a tough solution to write a gorm interface and mock it myself...
Can anyone please provide a simple example of mocking such a function?
P.S.
I don't want to run the docker with Postgres for this test. It is a simple unit test, not integration.
EDIT:
Suppose I just want to mock the connection to make gorm.Open to not return an error. How can I do it?
sqlmock.NewWithDSN(settings.GetDSN()) does not help

Answering my own question here.
Thanks to #flimzy for pointing me in the right direction.
To be able to test the storage open need to modify the function:
func (ps *Storage) Open(settings *Settings, postgresConfig *postgres.Config) error {
if err := settings.Validate(); err != nil {
return err
}
ps.mutex.Lock()
defer ps.mutex.Unlock()
if ps.GormDB != nil {
return nil
}
gormDB, err := gorm.Open(postgres.New(*postgresConfig), &gorm.Config{
SkipDefaultTransaction: true,
})
if err != nil {
return fmt.Errorf("%s: %v", DBConnectError, err)
}
ps.GormDB = gormDB
sqlDB, err := ps.GormDB.DB()
if err != nil {
return fmt.Errorf("%s: %v", DBRetrievalError, err)
}
ps.SqlDB = sqlDB
ps.SqlDB.SetMaxIdleConns(settings.MaxIdleConnections)
ps.SqlDB.SetMaxOpenConns(settings.MaxOpenConnections)
ps.ReadTimeout = settings.ReadTimeout
ps.WriteTimeout = settings.WriteTimeout
return nil
}
To mock gorm connection we just pass go-sqlmock connection in the postgres config of the form postgres driver:
package postgres
import (
"github.com/DATA-DOG/go-sqlmock"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Postgres Storage", func() {
...
Describe("Open", func() {
Context("when settings are valid", func() {
It("should open a DB connection and initialize storage", func() {
sqlDB, sqlMock, _ := sqlmock.New()
sqlMock.ExpectPing()
err := storage.Open(settings, postgres.Config{Conn: sqlDB})
dbStats := storage.sqlDB.Stats()
Expect(err).ShouldNot(HaveOccurred())
Expect(dbStats.MaxOpenConnections).To(Equal(settings.MaxOpenConnections))
Expect(dbStats.OpenConnections).To(Equal(1))
Expect(storage.ReadTimeout).To(Equal(settings.ReadTimeout))
Expect(storage.WriteTimeout).To(Equal(settings.WriteTimeout))
Expect(sqlMock.ExpectationsWereMet()).To(BeNil())
})
})
})
})

Related

Problem writing a unit test using gorm and sql-mock

this is my funckion using package testing, gorm and sql-mock:
func Test_Create(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Errorf("Failed to open mock sql db, got error: %v", err)
}
if db == nil {
t.Error("db is null")
}
if mock == nil {
t.Error("mock is null")
}
defer db.Close()
pDb, err := gorm.Open(postgres.New(postgres.Config{
PreferSimpleProtocol: false,
DriverName: "postgres",
Conn: db,
}))
if err != nil {
t.Fatalf("gorm postgres fatal: %v", err)
}
student := Student{
ID: 12345,
Name: "Test user",
}
mock.ExpectBegin()
mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO "students" ("id","name") VALUES ($1,$2) RETURNING "id"`)).WithArgs(student.ID, student.Name).WillReturnResult(sqlmock.NewResult(student.ID, 1))
mock.ExpectCommit()
err = pDb.Create(student).Error
if err != nil {
t.Error("error:", err)
}
}
I don't understand why I have an error creating a unit test as in the example below, can anyone help?
okay, I've already solved the problem. here's the working func:
func Test_Create(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Errorf("Failed to open mock sql db, got error: %v", err)
}
if db == nil {
t.Error("db is null")
}
if mock == nil {
t.Error("mock is null")
}
defer db.Close()
dialector := postgres.New(postgres.Config{
DSN: "sqlmock_db_0",
DriverName: "postgres",
Conn: db,
PreferSimpleProtocol: true,
})
pDb, err := gorm.Open(dialector, &gorm.Config{})
if err != nil {
t.Fatalf("gorm postgres fatal: %v", err)
}
student := Student{
ID: 12345,
Name: "Test user",
}
mock.ExpectBegin()
mock.ExpectQuery(
regexp.QuoteMeta(`INSERT INTO "students" ("name","id") VALUES ($1,$2) RETURNING "id"`)).
WithArgs(student.Name, student.ID).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(student.ID))
mock.ExpectCommit()
if err = pDb.Create(&student).Error; err != nil {
t.Errorf("Failed to insert to gorm db, got error: %v", err)
t.FailNow()
}
err = mock.ExpectationsWereMet()
if err != nil {
t.Errorf("Failed to meet expectations, got error: %v", err)
}
}

Unit-testing grpc functions in golang

I have created a function that utilizes the grpc package in golang. I don't know if it is relevant but the purpose is the communication with a GoBGP router over grpc. An example is the following function which prints all the peers (neighbors) of the router:
func (gc *Grpc) Peers(conn *grpc.ClientConn) error {
defer conn.Close()
c := pb.NewGobgpApiClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
p := pb.ListPeerRequest{}
peer, err := c.ListPeer(ctx, &p)
if err != nil {
return err
}
for {
res, err := peer.Recv()
if err != nil {
return err
}
fmt.Println(res)
}
return nil
}
Now, I want to create unit tests for the function. To do so, I used google.golang.org/grpc/test/bufconn package, and initialized the following:
type server struct {
pb.UnimplementedGobgpApiServer
}
func (s *server) ListDefinedSet(in *pb.ListDefinedSetRequest, ls pb.GobgpApi_ListDefinedSetServer) error {
return nil
}
var lis *bufconn.Listener
const bufSize = 1024 * 1024
func init() {
lis = bufconn.Listen(bufSize)
s := grpc.NewServer()
pb.RegisterGobgpApiServer(s, &server{})
go func() {
if err := s.Serve(lis); err != nil {
fmt.Println("Server failed!")
}
}()
}
func bufDialer(context.Context, string) (net.Conn, error) {
return lis.Dial()
}
This way, I can run a unit-test creating a connection as follows:
ctx := context.Background()
conn, _ := grpc.DialContext(ctx, "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithInsecure())
Peers(conn)
However, the problem is that the stream seems to be always empty and thus the peer.Recv()
always returns EOF. Is there any way to populate the stream with dummy data? If you have experience, is my methodology correct?

How to write an unit test for a handler that invokes a function that interacts with db in Golang using pgx driver?

I've been trying to write unit tests for my http handler. The code segment is as below:
func (s *Server) handleCreateTicketOption(w http.ResponseWriter, r *http.Request) {
var t ticket.Ticket
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, er.ErrInternal.Error(), http.StatusInternalServerError)
return
}
err = json.Unmarshal(body, &t)
if err != nil {
http.Error(w, er.ErrInvalidData.Error(), http.StatusBadRequest)
return
}
ticket, err := s.TicketService.CreateTicketOption(r.Context(), t)
if err != nil {
http.Error(w, er.ErrInternal.Error(), http.StatusInternalServerError)
return
}
res, err := json.Marshal(ticket)
if err != nil {
http.Error(w, er.ErrInternal.Error(), http.StatusInternalServerError)
return
}
log.Printf("%v tickets allocated with name %v\n", t.Allocation, t.Name)
s.sendResponse(w, res, http.StatusOK)
}
Actual logic that interacts with DB. This code segment is invoked by the handler as you can see in the code above. ticket, err := s.TicketService.CreateTicketOption(r.Context(), t)
func (t *TicketService) CreateTicketOption(ctx context.Context, ticket ticket.Ticket) (*ticket.Ticket, error) {
tx, err := t.db.dbPool.Begin(ctx)
if err != nil {
return nil, er.ErrInternal
}
defer tx.Rollback(ctx)
var id int
err = tx.QueryRow(ctx, `INSERT INTO ticket (name, description, allocation) VALUES ($1, $2, $3) RETURNING id`, ticket.Name, ticket.Description, ticket.Allocation).Scan(&id)
if err != nil {
return nil, er.ErrInternal
}
ticket.Id = id
return &ticket, tx.Commit(ctx)
}
And that is my unit test for the handler.
func TestCreateTicketOptionHandler(t *testing.T) {
caseExpected, _ := json.Marshal(&ticket.Ticket{Id: 1, Name: "baris", Description: "test-desc", Allocation: 10})
srv := NewServer()
// expected := [][]byte{
// _, _ = json.Marshal(&ticket.Ticket{Id: 1, Name: "baris", Description: "test-desc", Allocation: 20}),
// // json.Marshal(&ticket.Ticket{Id: 1, Name: "baris", Description: "test-desc", Allocation: 20})
// }
tt := []struct {
name string
entry *ticket.Ticket
want []byte
code int
}{
{
"valid",
&ticket.Ticket{Name: "baris", Description: "test-desc", Allocation: 10},
caseExpected,
http.StatusOK,
},
}
var buf bytes.Buffer
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
json.NewEncoder(&buf).Encode(tc.entry)
req, err := http.NewRequest(http.MethodPost, "/ticket_options", &buf)
log.Println("1")
if err != nil {
log.Println("2")
t.Fatalf("could not create request: %v", err)
}
log.Println("3")
rec := httptest.NewRecorder()
log.Println("4")
srv.handleCreateTicketOption(rec, req)
log.Println("5")
if rec.Code != tc.code {
t.Fatalf("got status %d, want %v", rec.Code, tc.code)
}
log.Println("6")
if reflect.DeepEqual(rec.Body.Bytes(), tc.want) {
log.Println("7")
t.Fatalf("NAME:%v, got %v, want %v", tc.name, rec.Body.Bytes(), tc.want)
}
})
}
}
I did research about mocking pgx about most of them were testing the logic part not through the handler. I want to write unit test for both handler and logic itself seperately. However, the unit test I've written for the handler panics as below
github.com/bariis/gowit-case-study/psql.(*TicketService).CreateTicketOption(0xc000061348, {0x1485058, 0xc0000260c0}, {0x0, {0xc000026dd0, 0x5}, {0xc000026dd5, 0x9}, 0xa})
/Users/barisertas/workspace/gowit-case-study/psql/ticket.go:24 +0x125
github.com/bariis/gowit-case-study/http.(*Server).handleCreateTicketOption(0xc000061340, {0x1484bf0, 0xc000153280}, 0xc00018e000)
/Users/barisertas/workspace/gowit-case-study/http/ticket.go:77 +0x10b
github.com/bariis/gowit-case-study/http.TestCreateTicketOptionHandler.func2(0xc000119860)
/Users/barisertas/workspace/gowit-case-study/http/ticket_test.go:80 +0x305
psql/ticket.go:24: tx, err := t.db.dbPool.Begin(ctx)
http/ticket.go:77: ticket, err := s.TicketService.CreateTicketOption(r.Context(), t)
http/ticket_test.go:80: srv.handleCreateTicketOption(rec, req)
How can I mock this type of code?
Create an interface which has the required DB functions
Your DB handler implements this interface. You use the handler in actual execution
Create a mock handler using testify/mock and use this in place of DB handler in test cases
From what I can read, you have the following structure:
type Server struct {
TicketService ticket.Service
}
type TicketService struct {
db *sql.Db // ..or similar
}
func (ts *TicketService) CreateTicketOption(...)
The trick to mock this is by ensuring ticket.Service is an interface instead of a struct.
Like this:
type TicketService interface {
CreateTicketOption(ctx context.Context, ticket ticket.Ticket) (*ticket.Ticket, error) {
}
By doing this, your Server expects a TicketService interface.
Then you could do this:
type postgresTicketService struct {
db *sql.Db
}
func (pst *postgresTicketService) CreateTicketOption(...)...
Which means that the postgresTicketService satisfies the requirements to be passed as a ticket.Service to the Server.
This also means that you can do this:
type mockTicketService struct {
}
func (mts *mockTicketService) CreateTicketOption(...)...
This way you decouple the Server from the actual implementation, and you could just init the Server with the mockTicketService when testing and postgresTicketService when deploying.

Golang Unit Testing Erorr when test function with query db

i start learning unit test on golang, i have do a simple test than it work, but try on real case, i found some errors, this is my test code:
test code
func TestGetAllCompetency(t *testing.T) {
_, err := competencyService.GetAllCompetency()
if err != nil {
t.Error("failed to get competency")
}
}
GetAllCompetency Code
func GetAllCompetency() ([]models.Competency, error) {
data := []models.Competency{}
res := database.Conn.
Table("competency").Order("name").
Where("competency.is_delete = ?", false).
Find(&data)
err := res.Error
if err != nil {
log.Println("[Error] competencyService.GetAllData : ", err)
return []models.Competency{}, err
}
return data, nil
}

How to do unit testing of HTTP requests using Ginkgo?

i have just started learning to write unit tests for the http requests, i went through several blogs but i didn't understood how to write tests for this using Ginkgo.
func getVolDetails(volName string, obj interface{}) error {
addr := os.Getenv("SOME_ADDR")
if addr == "" {
err := errors.New("SOME_ADDR environment variable not set")
fmt.Println(err)
return err
}
url := addr + "/path/to/somepage/" + volName
client := &http.Client{
Timeout: timeout,
}
resp, err := client.Get(url)
if resp != nil {
if resp.StatusCode == 500 {
fmt.Printf("VSM %s not found\n", volName)
return err
} else if resp.StatusCode == 503 {
fmt.Println("server not reachable")
return err
}
} else {
fmt.Println("server not reachable")
return err
}
if err != nil {
fmt.Println(err)
return err
}
defer resp.Body.Close()
return json.NewDecoder(resp.Body).Decode(obj)
}
// GetVolAnnotations gets annotations of volume
func GetVolAnnotations(volName string) (*Annotations, error) {
var volume Volume
var annotations Annotations
err := getVolDetails(volName, &volume)
if err != nil || volume.Metadata.Annotations == nil {
if volume.Status.Reason == "pending" {
fmt.Println("VSM status Unknown to server")
}
return nil, err
}
// Skipped some part,not required
}
I went through this blog and it exactly explains what my code requires but it uses Testing package and i want to implement this using ginkgo.
Take a look at ghttp package:
http://onsi.github.io/gomega/#ghttp-testing-http-clients
https://godoc.org/github.com/onsi/gomega/ghttp
A rough sketch might look like:
import (
"os"
. "github.com/onsi/ginkgo/tmp"
"github.com/onsi/gomega/ghttp"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("GetVolAnnotations", func() {
var server *ghttp.Server
var returnedVolume Volume
var statusCode int
BeforeEach(func() {
server = ghttp.NewServer()
os.Setenv("SOME_ADDR", server.Addr())
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", "/path/to/somepage/VOLUME"),
ghttp.RespondWithJSONEncodedPtr(&statusCode, &returnedVolume),
)
)
})
AfterEach(func() {
server.Close()
})
Context("When when the server returns a volume", func() {
BeforeEach(func() {
returnedVolume = Volume{
Metadata: Metadata{
Annotations: []string{"foo"}
}
}
statusCode = 200
})
It("returns the annotations associated with the volume", func() {
Expect(GetVolAnnotations("VOLUME")).To(Equal([]string{"foo"}))
})
})
Context("when the server returns 500", func() {
BeforEach(func() {
statusCode = 500
})
It("errors", func() {
value, err := GetVolAnnotations("VOLUME")
Expect(value).To(BeNil())
Expect(err).To(HaveOccurred())
})
})
Context("when the server returns 503", func() {
BeforEach(func() {
statusCode = 503
})
It("errors", func() {
value, err := GetVolAnnotations("VOLUME")
Expect(value).To(BeNil())
Expect(err).To(HaveOccurred())
})
})
})
I think you've got a few issues with your code though. If you get a 500 or 503 status code you won't necessarily have an err so you'll need to create and send back a custom error from your server.