I have a user service that validates the user data and formats it and then calls Firebase service which creates a firebase user and return firebase id and then it pass that user data to repository layer. My user struct has an ID field is populated by uuid in user service before passing to repository layer. I am mocking the firebase service and repository layer using strecher/testify. But the test is failing since the ID is populated by service layer and I cannot pass the ID to the user data used by mock function.
user := model.User{
ID: "",
FirebaseID: "",
FirstName: "Test",
LastName: "User",
FullName: "Test User",
Email: "testuser#email.com"
Password: "password",
}
Service layer code
func (u userService) CreateUser(user model.User) error {
err := validateFields(user)
if err != nil {
return fmt.Errorf("userService CreateUser: %w", err)
}
user.FullName = user.FirstName + " " + user.LastName
user.FirebaseID, err = u.authClient.CreateUser(user)
if err != nil {
return fmt.Errorf("userService CreateUser: %w", err)
}
user.ID = uuid.NewString()
err = u.userRepo.CreateUser(user)
if err != nil {
return fmt.Errorf("userService CreateUser: %w", err)
}
return nil
}
Test code
func TestCreateUser(t *testing.T) {
mockFirebaseAuthClient := new(MockFirebaseAuthClient)
mockPostgresRepo := new(MockPostgresRepo)
userService := NewUserService(mockPostgresRepo, mockFirebaseAuthClient)
t.Run("Valid data", func(t *testing.T) {
user := model.User{
ID: "",
FirebaseID: "firebaseuniqueid",
FirstName: "Test",
LastName: "User",
FullName: "Test User",
Email: "testuser#email.com",
Password: "password",
}
mockFirebaseAuthClient.On("CreateUser", user).Return("firebaseuniqueid", nil)
mockPostgresRepo.On("CreateUser", user).Return(nil)
err := userService.CreateUser(user)
if err != nil {
t.Fatalf("Expectd: nil, got: %v", err)
}
})
Error while testing
mock: Unexpected Method Call
-----------------------------
CreateUser(model.User)
0: model.User{ID:"f87fd2f3-5801-4359-a565-a4eb13a6de37", FirebaseID:"firebaseuniqueid", FirstName:"Test", LastName:"User", FullName:"Test User", Email:"testuser#email.com", Password:"password"}
The closest call I have is:
CreateUser(model.User)
0: model.User{ID:"", FirebaseID:"firebaseuniqueid", FirstName:"Test", LastName:"User", FullName:"Test User", Email:"testuser#email.com", Password:"password"}
Difference found in argument 0:
--- Expected
+++ Actual
## -1,3 +1,3 ##
(model.User) {
- ID: (string) "",
+ ID: (string) (len=36) "f87fd2f3-5801-4359-a565-a4eb13a6de37",
FirebaseID: (string) (len=16) "firebaseuniqueid",
Diff: 0: FAIL: (model.User={f87fd2f3-5801-4359-a565-a4eb13a6de37 firebaseuniqueid Test User Test User testuser#email.com password}) != (model.User={ firebaseuniqueid Test User Test User testuser#email.com password}) [recovered]
Is there any way I could check the dynamically created uuid or ignore the values in the struct in the test?
if you don't want to consider mockFirebaseAuthClient.On("CreateUser", user).Return("firebaseuniqueid", nil) and mockPostgresRepo.On("CreateUser", user).Return(nil) and just want to mock that calls, then you can use mock.Anything as the argument in both the calls instead of user like this mockFirebaseAuthClient.On("CreateUser", mock.Anything).Return("firebaseuniqueid", nil) . So the arguments will not be considerd and the mock calls will return required value.
Regarding your question of
Since uuid is not injected into the service layer, how can it be mocked? It is an imported package.
Like this, first, define an interface with the same method we want to mock
type uuidGen interface {
String() string
}
Then, define a mock type in which we're going to define our method
type mockGen struct{}
Then, define the method on the type
func (u *mockGen) String() string {
return "test"
}
Update CreateUser function to receive a uuidGen parameter that shares the method String() with uuid's package.
func (u userService) CreateUser(uuid uuidGen, user User) error {
err := validateFields(user)
if err != nil {
return fmt.Errorf("userService CreateUser: %w", err)
}
user.FullName = user.FirstName + " " + user.LastName
user.FirebaseID, err = u.authClient.CreateUser(user)
if err != nil {
return fmt.Errorf("authClient CreateUser: %w", err)
}
user.ID = uuid.String()
err = u.userRepo.CreateUser(user)
if err != nil {
return fmt.Errorf("userService CreateUser: %w", err)
}
return nil
}
Now we can write the test like this, see how the 2 methods accept different types that implement the interface uuidGen and can call a method String()
func TestCreateUser(t *testing.T) {
mockFirebaseAuthClient := new(MockFirebaseAuthClient)
mockPostgresRepo := new(MockPostgresRepo)
userService := NewUserService("test", "test")
t.Run("Valid data", func(t *testing.T) {
user := User{
ID: "",
FirebaseID: "firebaseuniqueid",
FirstName: "Test",
LastName: "User",
FullName: "Test User",
Email: "testuser#email.com",
Password: "password",
}
mockFirebaseAuthClient.On("CreateUser", user).Return("firebaseuniqueid", nil)
mockPostgresRepo.On("CreateUser", user).Return(nil)
gen := new(mockGen)
err := userService.CreateUser(gen, user)
if err != nil {
t.Fatalf("Expectd: nil, got: %v", err)
}
realUUID := uuid.New()
err = userService.CreateUser(realUUID, user)
if err != nil {
t.Fatalf("Expectd: nil, got: %v", err)
}
t.Log("Mock UUID:", gen.String()) // prints test
t.Log("Real UUID UUID:", realUUID.String()) // prints a UUID
})
}
Run it with go test -v to see the output of t.Log(...)
Related
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.
I am trying to test a function that gets some details then update details in db. I am using gorm for my ORM and mocking db exec with DATA-DOG/sqlmock. But I keep getting expected begin error but not sure where I am messing up. I tried many variations of the code where I remove the expectBegin expectCommit etc.
Here is the code I am trying to test:
// UpsertUserProfile saves the user if doesn't exists and adds the OAuth profile
// and updates existing user info if record exist in db
func (o *ORM) UpsertUserProfile(gu *goth.User) (*models.User, error) {
db := o.DB
up := &models.UserProfile{}
u, err := models.GothUserToDBUser(gu, false)
if err != nil {
return nil, err
}
tx := db.Where("email = ?", gu.Email).First(u)
if tx.Error != nil && tx.Error != gorm.ErrRecordNotFound {
return nil, tx.Error
}
if tx := db.Model(u).Save(u); tx.Error != nil {
return nil, tx.Error
}
tx = db.Where("email = ? AND provider = ? AND external_user_id = ?", gu.Email, gu.Provider, gu.UserID).First(up)
if tx.Error != nil && tx.Error != gorm.ErrRecordNotFound {
return nil, tx.Error
}
up, err = models.GothUserToDBUserProfile(gu, false)
if err != nil {
return nil, err
}
up.User = *u
if tx := db.Model(up).Save(up); tx.Error != nil {
return nil, tx.Error
}
return u, nil
}
And here is the test code
func TestORM_UpsertUserProfile(t *testing.T) {
mockdb, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
gormDB, err := gorm.Open(postgres.New(postgres.Config{
Conn: mockdb,
}), &gorm.Config{})
if err != nil {
t.Fatal(err.Error())
}
defer mockdb.Close()
id, _ := uuid.NewV4()
rows := sqlmock.NewRows([]string{"id", "first_name", "last_name"}).
AddRow(id.String(), "fname", "lname")
profileRows := sqlmock.NewRows([]string{"email"}).AddRow("email")
// Test cases
t.Run("success", func(t *testing.T) {
o := &ORM{
DB: gormDB,
}
now := time.Now()
fName := "fname"
lName := "lname"
wantUsr := models.User{
Email: "email",
FirstName: &fName,
LastName: &lName,
BaseModelSoftDelete: models.BaseModelSoftDelete{
BaseModel: models.BaseModel{
ID: id,
CreatedAt: &now,
UpdatedAt: &now,
},
},
}
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "users"`)).WithArgs("email").WillReturnRows(rows)
mock.ExpectBegin()
mock.ExpectExec(regexp.QuoteMeta(`UPDATE`)).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
mock.ExpectBegin()
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user_profiles"`)).WithArgs("email", "provider", "testID").WillReturnRows(profileRows)
mock.ExpectCommit()
mock.ExpectBegin()
got, err := o.UpsertUserProfile(&goth.User{Email: "email", UserID: "testID", Provider: "provider"})
if (err != nil) != false {
t.Errorf("ORM.UpsertUserProfile() error = %v, wantErr %v", err, false)
return
}
if !reflect.DeepEqual(got, wantUsr) {
t.Errorf("ORM.UpsertUserProfile() = %v, want %v", got, wantUsr)
}
})
}
but the test keeps returning this error
SELECT * FROM "user_profiles" WHERE email = 'email' AND provider = 'provider' AND external_user_id = 'testID' ORDER BY "user_profiles"."id" LIMIT 1
/Users/public/Projects/go-rest-service/internal/orm/orm_test.go:260: ORM.UpsertUserProfile() error = call to Query 'SELECT * FROM "user_profiles" WHERE email = $1 AND provider = $2 AND external_user_id = $3 ORDER BY "user_profiles"."id" LIMIT 1' with args [{Name: Ordinal:1 Value:email} {Name: Ordinal:2 Value:provider} {Name: Ordinal:3 Value:testID}], was not expected, next expectation is: ExpectedBegin => expecting database transaction Begin, wantErr false
I have a handlerAuthentication function that I need to test:
func handlerAuthentication(c *gin.Context) {
session := Session.GetSession(c)
var login Login
err := c.BindJSON(&login)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
client, err := initClient(c, login)
fmt.Println("Error: ",err)
if err != nil {
fmt.Println("There's an error !")
c.JSON(http.StatusUnauthorized, gin.H{"error": ErrorWrongLogin})
return
}
err = (*client).Logout(c)
if err != nil {
return
}
session.Set("username", login.Username)
session.Set("password", login.Password)
err = session.Save()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "an error occurred during the save of the session:" + err.Error()})
return
}
c.JSON(http.StatusOK, "Connected")
}
To do so,I made this:
func TestHandlerAuthentication(t *testing.T) {
UrlOdoo = "https://isi.nc"
resp := httptest.NewRecorder()
gin.SetMode(gin.TestMode)
c, r := gin.CreateTestContext(resp)
r.POST("/test", func(c *gin.Context) {
handlerAuthentication(c)
})
ctrl := gomock.NewController(t)
defer ctrl.Finish()
Odoo = OdooRPC{createMockOdooClient}
client = mock_odoorpc.NewMockOdooClient(ctrl)
client.EXPECT().Authenticate(gomock.Any(), gomock.Any(), invalidUsername, invalidPassword).AnyTimes().Return(fmt.Errorf("invalid login"))
client.EXPECT().Authenticate(gomock.Any(), gomock.Any(), validUsername, validPassword).AnyTimes().Return(nil)
client.EXPECT().Logout(gomock.Any()).AnyTimes().Return(nil)
session = mock_session.NewMockSession(ctrl)
Session = SessionGetter{createMockSession}
session.EXPECT().Set("username", validUsername).AnyTimes().Return()
session.EXPECT().Set("password", validPassword).AnyTimes().Return()
session.EXPECT().Save().AnyTimes().Return(nil)
for name, test := range map[string]struct {
input Login
want int
}{
"valid login": {
input: Login{
Username: validUsername,
Password: validPassword,
},
want: 200,
},
"invalid login": {
input: Login{
Username: invalidUsername,
Password: invalidPassword,
},
want: 401,
},
} {
t.Run(name, func(t *testing.T) {
body, _ := json.Marshal(test.input)
c.Request, _ = http.NewRequest(http.MethodPost, "/test", strings.NewReader(string(body)))
r.ServeHTTP(resp, c.Request)
assert.Equal(t, test.want, resp.Code)
resp.Flush()
})
}
}
The problem I'm facing is that if I do the tests one by (valid login and invalid login), they all pass, but when I do the two tests at the same time, the second test fails.
Here's an exemple of execution of the two tests at the same time:
=== RUN TestHandlerAuthentication
=== RUN TestHandlerAuthentication/valid_login
Error: <nil> //No error, so resp.Code should be equal to 200
=== RUN TestHandlerAuthentication/invalid_login
Error: invalid login //Error, so resp.Code should be equal to 401
There's an error !
main_test.go:394:
Error Trace: main_test.go:394
Error: Not equal:
expected: 401
actual : 200
Test: TestHandlerAuthentication/invalid_login
--- FAIL: TestHandlerAuthentication (0.00s)
--- PASS: TestHandlerAuthentication/valid_login (0.00s)
--- FAIL: TestHandlerAuthentication/invalid_login (0.00s)
Expected :401
Actual :200
As expected, an error occured when the login is invalid, but the resp.Code is still 200.
And if I do the invalid login test first, the resp.Code will still be 401.
Is it happening because the tests are parallelized and the httptest ResponseRecorder doesn't work in parallel ?
Thank you for your help.
Thank you leaf bebop
I needed to initialize a new httptest.ResponseRecorder for each test.
To do so, I move the initialisation to the t.Run(name,func(t *testing.T) function:
func TestHandlerAuthentication(t *testing.T) {
UrlOdoo = "https://isi.nc"
ctrl := gomock.NewController(t)
defer ctrl.Finish()
Odoo = OdooRPC{createMockOdooClient}
client = mock_odoorpc.NewMockOdooClient(ctrl)
client.EXPECT().Authenticate(gomock.Any(), gomock.Any(), invalidUsername, invalidPassword).AnyTimes().Return(fmt.Errorf("invalid login"))
client.EXPECT().Authenticate(gomock.Any(), gomock.Any(), validUsername, validPassword).AnyTimes().Return(nil)
client.EXPECT().Logout(gomock.Any()).AnyTimes().Return(nil)
session = mock_session.NewMockSession(ctrl)
Session = SessionGetter{createMockSession}
session.EXPECT().Set("username", validUsername).AnyTimes().Return()
session.EXPECT().Set("password", validPassword).AnyTimes().Return()
session.EXPECT().Save().AnyTimes().Return(nil)
for name, test := range map[string]struct {
input Login
want int
}{
"valid login": {
input: Login{
Username: validUsername,
Password: validPassword,
},
want: 200,
},
"invalid login": {
input: Login{
Username: invalidUsername,
Password: invalidPassword,
},
want: 401,
},
} {
t.Run(name, func(t *testing.T) {
resp := httptest.NewRecorder()
gin.SetMode(gin.TestMode)
c, r := gin.CreateTestContext(resp)
r.POST("/test", func(c *gin.Context) {
handlerAuthentication(c)
})
body, _ := json.Marshal(test.input)
c.Request, _ = http.NewRequest(http.MethodPost, "/test", strings.NewReader(string(body)))
r.ServeHTTP(resp, c.Request)
assert.Equal(t, test.want, resp.Code)
})
}
}
Go here. Trying to figure out how to use SQL mock v2.
Here's my interface:
type OrderPersister interface {
FetchOrderById(string) (*Order, error)
}
And my implementation of that interface:
type DbPersister struct {
Config config.DbConfig
GormDB *gorm.DB
}
func (op DbPersister) FetchOrderById(orderId string) (*Order, error) {
Order := &Order{}
orderUuid, err := uuid.Parse(orderId)
if err != nil {
return nil, err
}
if err := op.GormDB.Table("orders").
Select(`orders.order_id,
orders.user_id,
orders.quantity,
orders.status
addresses.line_1,
users.email`).
Joins("join addresses on addresses.address_id = orders.address_id").
Joins("join users on users.user_id = orders.user_id").
Where("orders.order_id = ?", orderUuid).
First(Order).Error; err != nil {
return nil, err
}
return Order, nil
}
And my unit test (including setup/init):
import (
"database/sql"
"testing"
"github.com/google/uuid"
"github.com/jinzhu/gorm"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"gopkg.in/DATA-DOG/go-sqlmock.v2"
)
type Suite struct {
suite.Suite
DB *gorm.DB
mock sqlmock.Sqlmock
dbPersister OrderPersister
}
func (s *Suite) SetupSuite() {
var (
db *sql.DB
err error
)
db, s.mock, err = sqlmock.New()
require.NoError(s.T(), err)
s.DB, err = gorm.Open("postgres", db)
require.NoError(
s.T(), err)
s.DB.LogMode(true)
s.dbPersister = DbPersister{
Config: config.DbConfig{
DbHost: "",
DbPort: "",
DbName: "",
DbUsername: "",
DbPassword: "",
},
GormDB: s.DB,
}
}
func (s *Suite) BeforeTest(_, _ string) {
var (
db *sql.DB
err error
)
db, s.mock, err = sqlmock.New()
require.NoError(s.T(), err)
s.DB, err = gorm.Open("postgres", db)
require.NoError(s.T(), err)
s.DB.LogMode(true)
}
func (s *Suite) AfterTest(_, _ string) {
require.NoError(s.T(), s.mock.ExpectationsWereMet())
}
func TestInit(t *testing.T) {
suite.Run(t, new(Suite))
}
func (s *Suite) TestFetchOrderById() {
// given
orderId := uuid.New()
quantity := 1
status := "ready"
line1 := "201"
email := "jsmith#example.com"
// s.mock.ExpectBegin()
s.mock.ExpectQuery(`SELECT`).
WillReturnRows(sqlmock.NewRows([]string{"orders.order_id","orders.user_id","orders.quantity","orders.status",
"addresses.line_1","user_logins.email"}).
AddRow(sqlmock.AnyArg(), sqlmock.AnyArg(), quantity, status, totalExclTax, shippingExclTax,
totalTaxAmt, line1, state, zip, locality, upc, email, firstName, lastName))
_, err := s.dbPersister.FetchOrderById(orderId.String())
s.mock.ExpectCommit()
require.NoError(s.T(), err)
}
When this runs the test fails for the following reason:
--- FAIL: TestInit (0.00s)
--- FAIL: TestInit/TestFetchOrderById (0.00s)
db_test.go:67:
Error Trace: db_test.go:67
suite.go:137
panic.go:969
rows.go:134
db_test.go:99
Error: Received unexpected error:
there is a remaining expectation which was not matched: ExpectedQuery => expecting Query, QueryContext or QueryRow which:
- matches sql: 'SELECT'
- is without arguments
Test: TestInit/TestFetchOrderById
All I'm trying to do is confirm that the GormDB instance was queried with the SELECT statement specified in the FetchOrderById function.
Does anybody know what I need to do to achieve this and get the test to pass?
I decided to Go (no pun intended) with Java instead.
I am building a simple function that calls an API that returns a Post using GraphQL (https://github.com/machinebox/graphql). I wrapped the logic in a service that looks like this:
type Client struct {
gcl graphqlClient
}
type graphqlClient interface {
Run(ctx context.Context, req *graphql.Request, resp interface{}) error
}
func (c *Client) GetPost(id string) (*Post, error) {
req := graphql.NewRequest(`
query($id: String!) {
getPost(id: $id) {
id
title
}
}
`)
req.Var("id", id)
var resp getPostResponse
if err := c.gcl.Run(ctx, req, &resp); err != nil {
return nil, err
}
return resp.Post, nil
}
Now I'd like to add test tables for the GetPost function with a fail case when id is set to empty string which causes an error in the downstream call c.gcl.Run.
What I am struggling with is the way the gcl client can be mocked and forced to return the error (when no real API call happens).
My test so far:
package apiClient
import (
"context"
"errors"
"github.com/aws/aws-sdk-go/aws"
"github.com/google/go-cmp/cmp"
"github.com/machinebox/graphql"
"testing"
)
type graphqlClientMock struct {
graphqlClient
HasError bool
Response interface{}
}
func (g graphqlClientMock) Run(_ context.Context, _ *graphql.Request, response interface{}) error {
if g.HasError {
return errors.New("")
}
response = g.Response
return nil
}
func newTestClient(hasError bool, response interface{}) *Client {
return &Client{
gcl: graphqlClientMock{
HasError: hasError,
Response: response,
},
}
}
func TestClient_GetPost(t *testing.T) {
tt := []struct{
name string
id string
post *Post
hasError bool
response getPostResponse
}{
{
name: "empty id",
id: "",
post: nil,
hasError: true,
},
{
name: "existing post",
id: "123",
post: &Post{id: aws.String("123")},
response: getPostResponse{
Post: &Post{id: aws.String("123")},
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
client := newTestClient(tc.hasError, tc.response)
post, err := client.GetPost(tc.id)
if err != nil {
if tc.hasError == false {
t.Error("unexpected error")
}
} else {
if tc.hasError == true {
t.Error("expected error")
}
if cmp.Equal(post, &tc.post) == false {
t.Errorf("Response data do not match: %s", cmp.Diff(post, tc.post))
}
}
})
}
}
I am not sure if passing the response to the mock like this is the right way to do it. Also, I'm struggling to set the right value to the response, since an interface{} type is passed and I don't know how to convert it to the getPostResponse and set the value to Post there.
Your test cases should not go beyond the implementation. I'm specifically referring to the empty-vs-nonempty input or any kind of input really.
Let's take a look at the code you want to test:
func (c *Client) GetPost(id string) (*Post, error) {
req := graphql.NewRequest(`
query($id: String!) {
getPost(id: $id) {
id
title
}
}
`)
req.Var("id", id)
var resp getPostResponse
if err := c.gcl.Run(ctx, req, &resp); err != nil {
return nil, err
}
return resp.Post, nil
}
Nothing in the implementation above is doing anything based on the id parameter value and therefore nothing in your tests for this piece of code should really care about what input is passed in, if it is irrelevant to the implementation it should also be irrelevant to the tests.
Your GetPost has basically two code branches that are taken based on a single factor, i.e. the "nilness" of the returned err variable. This means that as far as your implementation is concerned there are only two possible outcomes, based on what err value Run returns, and therefore there should only be two test cases, a 3rd or 4th test case would be just a variation, if not an outright copy, of the first two.
Your test client is also doing some unnecessary stuff, the main one being its name, i.e. what you have there is not a mock so calling it that is not helpful. Mocks usually do a lot more than just return predefined values, they ensure that methods are called, in the expected order and with the expected arguments, etc. And actually you don't need a mock here at all so it's a good thing it isn't one.
With that in mind, here's what I would suggest you do with your test client.
type testGraphqlClient struct {
resp interface{} // non-pointer value of the desired response, or nil
err error // the error to be returned by Run, or nil
}
func (g testGraphqlClient) Run(_ context.Context, _ *graphql.Request, resp interface{}) error {
if g.err != nil {
return g.err
}
if g.resp != nil {
// use reflection to set the passed in response value
// (i haven't tested this so there may be a bug or two)
reflect.ValueOf(resp).Elem().Set(reflect.ValueOf(g.resp))
}
return nil
}
... and here are the necessary test cases, all two of them:
func TestClient_GetPost(t *testing.T) {
tests := []struct {
name string
post *Post
err error
client testGraphqlClient
}{{
name: "return error from client",
err: errors.New("bad input"),
client: testGraphqlClient{err: errors.New("bad input")},
}, {
name: "return post from client",
post: &Post{id: aws.String("123")},
client: testGraphqlClient{resp: getPostResponse{Post: &Post{id: aws.String("123")}}},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := Client{gql: tt.client}
post, err := client.GetPost("whatever")
if !cmp.Equal(err, tt.err) {
t.Errorf("got error=%v want error=%v", err, tt.err)
}
if !cmp.Equal(post, tt.post) {
t.Errorf("got post=%v want post=%v", post, tt.post)
}
})
}
}
... there's a bit of repetition going on here, the need to spell out the post and err twice but that's a small price to pay when compared to a more sophisticated/complicated test setup that would populate the test client from the test case's expected output fields.
Addendum:
If you were to update GetPost in such a way that it checks for the empty id and returns an error before it sends a request to graphql then your initial setup would make much more sense:
func (c *Client) GetPost(id string) (*Post, error) {
if id == "" {
return nil, errors.New("empty id")
}
req := graphql.NewRequest(`
query($id: String!) {
getPost(id: $id) {
id
title
}
}
`)
req.Var("id", id)
var resp getPostResponse
if err := c.gcl.Run(ctx, req, &resp); err != nil {
return nil, err
}
return resp.Post, nil
}
... and updating the test cases accordingly:
func TestClient_GetPost(t *testing.T) {
tests := []struct {
name string
id string
post *Post
err error
client testGraphqlClient
}{{
name: "return empty id error",
id: "",
err: errors.New("empty id"),
client: testGraphqlClient{},
}, {
name: "return error from client",
id: "nonemptyid",
err: errors.New("bad input"),
client: testGraphqlClient{err: errors.New("bad input")},
}, {
name: "return post from client",
id: "nonemptyid",
post: &Post{id: aws.String("123")},
client: testGraphqlClient{resp: getPostResponse{Post: &Post{id: aws.String("123")}}},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := Client{gql: tt.client}
post, err := client.GetPost(tt.id)
if !cmp.Equal(err, tt.err) {
t.Errorf("got error=%v want error=%v", err, tt.err)
}
if !cmp.Equal(post, tt.post) {
t.Errorf("got post=%v want post=%v", post, tt.post)
}
})
}
}