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

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

https://t.me/Golang_google

Протестируем приложения, созданные в 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)  
      })  
   }  
}

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

Заключение

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

Источник


Report Page