Как тестировать приложения Gofr?

Протестируем приложения, созданные в GoFr  —  специфическом веб-фреймворке, написанном на Golang.

Модульное тестирование  —  это написание для конкретных блоков кода отдельных тестовых функций, которые записываются в файлы с именами, оканчивающимися на _test.go, и распознаются в IDE. Соответствие фактического вывода ожидаемому результату этих функций проверяется утверждениями.

Почему именно модульное тестирование?

Приведем аналогию с выпечкой торта. В сфере разработки ПО торт  —  это конечный продукт, программное приложение.

Прежде чем испечь торт целиком, нужно убедиться, что каждый ингредиент хорош и сочетается с другими. Здесь и пригодится модульное тестирование.

Это как отдельная проверка каждого ингредиента, его свежести и адекватности рецепту.

Почему это важно? Проблемы с мукой или этапом замешивания, обнаруживаемые после того, как испечен весь торт, исправить гораздо сложнее. При модульном тестировании они выявляются раньше. Так что, когда торт готов  —  то есть запускается вся программа,  —  обеспечивается гармоничное сочетание всех ингредиентов и этапов.

Разработка через тестирование

Разработка через тестирование  —  это как построение с планом. При таком подходе сначала пишутся тесты, потом  —  код, баги обнаруживаются раньше, рефакторинг кода упрощается.

Но хватит теории, напишем код.

Начнем со структуры каталогов:

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

Для оптимальной разработки придерживаемся подхода многоуровневой архитектуры с акцентом на тестировании, весь проект с кодом реализации доступен по ссылке.

Тестирование уровня хранения

Сначала напишем тесты для уровня хранения.

Потребуется установить sqlmock.

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

Создаем служебную функцию для всех модульных тестов:

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
}

В ней настраивается среда тестирования: создаются логгер-заглушка и имитация базы данных SQL, а также сконфигурированный для их применения gofr.Context.

Напишем тест для конечной точки Create:

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)
}
}

Разберем эту строку:

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

Здесь ожидается, что при взаимодействии метода Create с базой данных  —  выполнении запроса createQuery  —  этот запрос выполнится с возвращением результата и указанием, что эта строка затронута идентификатором 1, и во время взаимодействия ошибок не возникнет.

Тестовой функцией Test_Create сначала настраивается среда тестирования: с помощью библиотеки sqlmock создаются имитированные контекст и база данных. Затем, чтобы проверить корректность работы метода Create в различных ситуациях, определяется два тестовых сценария.

Тестовая функция запускается по этим сценариям с вызовом метода Create для каждого случая и проверкой соответствия фактических результатов ожидаемым.

Если какая-либо часть теста не выполнится, то есть фактический результат или ошибка не соответствуют ожидаемым значениям, тестовой функцией сообщается об ошибке с указанием, какая часть теста не пройдена.

Так же пишутся тесты и для других конечных точек.

Тестирование уровня обработчика

Теперь, чтобы написать тесты для уровня обработчика, сымитируем уровень хранения  —  то же, что сделали с подключением к базе данных для тестов уровня хранения.

Для этого устанавливаем Mockgen.

Затем генерируем файл заглушки для уровня хранения, запуская в каталоге store такую команду:

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

В файле handler_test.go создаем эти функции:

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
}

newMock  —  служебная функция для создания мок-объектов в целях тестирования. Чтобы управлять жизненным циклом мок-объектов, ею помощью библиотеки gomock создается контроллер ctrl, которым затем генерируется хранилище mockemployee  —  для моделирования поведения реального хранилища сотрудников в контролируемой среде тестирования.

С помощью пакета log в GoFr создается логгер-заглушка:

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)
}

createContext  —  служебная функция для генерирования объекта gofr.Context в целях тестирования. Ею из указываемого метода, параметров URL-адреса и данных сотрудника конструируется HTTP-запрос, затем с помощью пакета gofr создается контекст.

Напишем тест для уровня обработчика, функции create:

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)
})
}
}

Так же пишутся тесты и для других конечных точек.

Заключение

Надеюсь, вы поняли важность разработки через тестирование и то, как этот подход применяется.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Mahak Singhania: How to test Gofr Based Applications ?

Предыдущая статьяРуководство по выбору оптимального карьерного пути в IT-сфере
Следующая статьяOTP-аутентификация c Devise