How to test Gofr Based Applications ?

Mahak Singhania
5 min readNov 16, 2023

GoFr is Opinionated Web Framework written in Go (Golang). This article walks through testing applications that were build using GoFr.

Unit testing: It involves writing separate test functions for individual units of code. Test functions are written in files with names ending in `_test.go` and are recognised by the IDEs. These functions use assertions, to check if the actual output of a function matches the expected result.

Why unit testing?

Let’s imagine you’re baking a cake. In the world of software development, the cake is like the final product — your software application. Now, before you bake the entire cake, you want to make sure each ingredient is good on its own and works well together. This is where unit testing comes in.Unit testing is like checking each ingredient separately to ensure it’s fresh and right for the recipe.

Why is this important? Well, if you discover that the flour is bad or the mixing step is flawed after you’ve baked the whole cake, it’s a lot harder to fix. Unit testing helps catch these issues early on, making sure that when you finally bake the cake (run the entire program), all the ingredients and steps work together harmoniously.

TDD — Test Driven Development

Test-driven development (TDD) is like building with a plan. It is an approach to software development where tests are written before the actual code, thereby helping in early bug detection and making it easier to refactor the code later on

Enough of Theory, Let’s begin coding!

Let’s start with Directory Structure :

sample-testing/ 
├── configs/
│ └── .env

├── handler/
│ └── handler.go
│ └── handler_test.go

├── model/
│ └── model.go

├── store/
│ ├── store.go
│ ├── store_test.go
│ └── interface.go
│ └── mock_interface.go

├── main.go
├── go.mod

We are following layered architecture approach for better development.
In this article, we would be only focusing on testing, in order to get the complete project along with the implementation code, please refer the github link:Employee API

Store Layer Testing

Let’s start by writing the tests for store layer first

Prerequisites : Install sqlmock

go get gopkg.in/DATA-DOG/go-sqlmock.v1

Creating a utility function which will be used in every unit test

func newMock(t *testing.T) (*gofr.Context, sqlmock.Sqlmock) {  
mockLogger := gofrLog.NewMockLogger(io.Discard)

db, mock, errMock := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
if errMock != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", errMock)
}

ctx := gofr.NewContext(nil, nil, &gofr.Gofr{DataStore: datastore.DataStore{ORM: db}, Logger: mockLogger})

ctx.Context = context.Background()

return ctx, mock
}

This function sets up a testing environment by creating a mock logger, a mock SQL database, and a gofr.Context configured to use these mocks.

Let’s see how we can write the test for Create endpoint :

func Test_Create(t *testing.T) {  
ctx, mock := newMock(t)
emp := &model.Employee{ID: 1, Name: "SAMPLE-NAME", Dept: "tech"}

testCases := []struct {
desc string
dbMock []interface{}
input *model.Employee
expectedRes *model.Employee
expectedErr error
}{
{desc: "success case", input: &model.Employee{ID: 1, Name: "SAMPLE-NAME", Dept: "tech"},
dbMock: []interface{}{
mock.ExpectExec(createQuery).WillReturnResult(sqlmock.NewResult(1, 1)).WillReturnError(nil)},
expectedRes: emp, expectedErr: nil},
{desc: "failure case", dbMock: []interface{}{
mock.ExpectExec(createQuery).
WillReturnError(errors.Error("error from db"))}, input: emp, expectedErr: errors.DB{Err: errors.Error("error from db")},
},
}

s := New()
for i, tc := range testCases {
res, err := s.Create(ctx, tc.input)

assert.Equal(t, tc.expectedRes, res, "Test[%d] Failed,Expected : %v\nGot : %v\n", i, tc.expectedErr, err)
assert.Equal(t, tc.expectedErr, err, "Test[%d] Failed,Expected : %v\nGot : %v\n", i, tc.expectedErr, err)
}
}

Let’s understand the below line:

dbMock: []interface{}{ mock.ExpectExec(createQuery).WillReturnResult(sqlmock.NewResult(1, 1)).WillReturnError(nil)}

In this line, we expect that when the Create method interacts with the database by executing a query (specified by createQuery), the query will be successful (returning a result indicating that one row was affected with ID 1), and there will be no errors during this interaction.

Therefore, the test function, Test_Create, first sets up a testing environment by creating a mock context and a simulated database using a library called sqlmock. It then defines two scenarios (test cases) to check if the Create method works correctly in different situations.
The test function runs through these scenarios, calling the `Create` method for each case and checking if the actual results match the expected outcomes. If any part of the test fails (i.e., the actual result or error doesn’t match the expected values), the test function reports an error, indicating which part of the test didn’t pass.

Similarly, we can write tests for other endpoints as well, for reference

Handler Layer Testing

Now, in order to write the tests for handler layer, we need to mock the store layer, as we mocked database connection in order to write store layer tests.

For mocking the store layer, we will be using mockgen.

Prerequisites :

After installing mockgen , run the following command in store directory to generate a mock file for store layer :

mockgen -destination=mock_interface.go -package=store -source=interface.go

In handler_test.go file, we create these functions:

func newMock(t *testing.T) (gofrLog.Logger, *store.MockEmployee) {  
ctrl := gomock.NewController(t)

defer ctrl.Finish()

mockStore := store.NewMockEmployee(ctrl)
mockLogger := gofrLog.NewMockLogger(io.Discard)

return mockLogger, mockStore
}

The newMock function is a utility function designed to facilitate the creation of mock objects for testing purposes. It utilises the gomock library by creating a controller (ctrl) to manage the lifecycle of mock objects. The mock employee store is then generated using the controller, allowing it to simulate the behaviour of the actual employee store in a controlled testing environment. A mock logger is created using the GoFr’s log package

func createContext(method string, params map[string]string, emp interface{}, logger gofrLog.Logger, t *testing.T) *gofr.Context {  
body, err := json.Marshal(emp)
if err != nil {
t.Fatalf("Error while marshalling model: %v", err)
}

r := httptest.NewRequest(method, "/dummy", bytes.NewBuffer(body))
query := r.URL.Query()

for key, value := range params {
query.Add(key, value)
}

r.URL.RawQuery = query.Encode()

req := request.NewHTTPRequest(r)

return gofr.NewContext(nil, req, nil)
}

This createContext function is a utility function for generating a gofr.Context object for testing purposes. It constructs an HTTP request based on the provided method, URL parameters, and employee data, and then creates a context using the gofr package

Now, lets write test for handler layer, create function

func Test_Create(t *testing.T) {  
mockLogger, mockStore := newMock(t)
h := New(mockStore)
emp := model.Employee{
ID: 1,
Name: "test emp",
Dept: "test dept",
}

testCases := []struct {
desc string
input interface{}
mockCalls []*gomock.Call
expRes interface{}
expErr error
}{
{"success case", emp, []*gomock.Call{
mockStore.EXPECT().Create(gomock.AssignableToTypeOf(&gofr.Context{}), &emp).Return(&emp, nil).Times(1),
}, &emp, nil},
{"failure case", emp, []*gomock.Call{
mockStore.EXPECT().Create(gomock.AssignableToTypeOf(&gofr.Context{}), &emp).Return(nil, errors.Error("test error")).Times(1),
}, nil, errors.Error("test error")},
{"failure case-bind error", "test", []*gomock.Call{}, nil, errors.InvalidParam{Param: []string{"body"}}},
}

for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
ctx := createContext(http.MethodPost, nil, tc.input, mockLogger, t)
res, err := h.Create(ctx)

assert.Equal(t, tc.expRes, res, "Test [%d] failed", i+1)
assert.Equal(t, tc.expErr, err, "Test [%d] failed", i+1)
})
}
}

Similarly, we can write tests for other endpoints as well, for reference

Conclusion

Hope you understood the importance of TDD approach and how to do it.Next we will be talking about Integration Testing, until then Happy Coding!

--

--