Однажды мне дали тестовое задание. Задача состояла в том, чтобы парсить кучу страниц с помощью определенного фреймворка на основе python. Скромно говоря, я справился, меня хорошо оценили с одним огромным замечанием: никаких юнит-тестов. Вот и был в итоге приговор — мне отказали в заявлении. Кем я стал после этого? Профи? Не совсем. Самозванец? Возможно ближе. Независимо от ответа, урок усвоен. Итак, давайте поговорим о модульных тестах для вашего приложения Go 1.11 в Google AppEngine.
В своей предыдущей статье я придумал приветственное приложение, которое почти ничего не делает, просто сохраняет оптимизм и защищено пользовательской проверкой аутентификации. Напомним, что до сих пор у нас был только один обработчик для тестирования:
func (s *Service) WarmupRequestHandler(w http.ResponseWriter, r *http.Request) { s.writeResponseData(w, http.StatusOK, &Response{"OK"}) return }
и наши маршруты в main.go выглядели следующим образом:
r.HandleFunc("/_ah/warmup", s.WarmupRequestHandler).Methods("GET") r.HandleFunc("/test-admin", s.WarmupRequestHandler).Methods("GET") r.HandleFunc("/test-no-admin",s.WarmupRequestHandler).Methods("GET") r.HandleFunc("/auth",s.Auth(s.WarmupRequestHandler)).Methods("GET")
Auth – это промежуточное ПО, которое принимает экземпляр функции http.HttpHandler и возвращает его с некоторой логикой. В нашем примере это была проверка JWT (веб-токен JSON). Полный код промежуточного ПО можно найти по ссылке:
Проблема контекста
Google App Engine первого поколения заставляет вас передавать контекст повсюду. Это не было бы проблемой, если бы контекст был стандартным, то есть экземпляром context.Context. Но это не так — последний должен быть экземпляром контекста google.golang.org/appengine. И вам нужен экземпляр запроса для его инициализации. Идеальным решением было бы придумать промежуточное программное обеспечение контекста, где у нас есть экземпляр запроса, и контекст будет инициализирован из него. Тогда мы сможем использовать его везде, где есть спрос.
Лучшим местом для размещения контекста будет сам экземпляр запроса, поскольку последний позволяет вам делать это с контекстом. Чуть позже я покажу вам, как это связано с юнит-тестами, на данный момент мы хотим инициировать контекст из запроса, а затем вызвать любой обработчик:
func (s *Service) ContextMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := appengine.NewContext(r) ctx, _ = appengine.Namespace(ctx, "") next.ServeHTTP(w, r.WithContext(ctx)) } }
Затем, всякий раз, когда нам может понадобиться контекст, он всегда доступен, вызывая метод r.Context(). Например, в нашем промежуточном программном обеспечении авторизации мы регистрируем, кто вызывает наш API, следующим образом:
log.Infof(r.Context(), "URL requested by: %s", claims.Email)
Теперь, когда у нас есть оболочка контекста, давайте обновим наши маршруты:
r.HandleFunc("/_ah/warmup", s.ContextMiddleware(s.WarmupRequestHandler)).Methods("GET") r.HandleFunc("/test-admin", s.ContextMiddleware(s.WarmupRequestHandler)).Methods("GET") r.HandleFunc("/test-no-admin", s.ContextMiddleware(s.WarmupRequestHandler)).Methods("GET")
Как видите, изменилось не так уж много: мы только что обернули все наши обработчики с помощью s.ContextMiddleware. Что касается нашей доморощенной проверки авторизации, это будет выглядеть немного по-другому, но не так сильно: мы по-прежнему хотим сначала инициализировать контекст, затем выполнить проверку авторизации и только потом вызывать наш обработчик.
r.HandleFunc("/auth", s.ContextMiddleware(s.Auth(s.WarmupRequestHandler))).Methods("GET")
Хорошо, теперь все готово для перехода к юнит-тестам.
Тестирование
Google предоставляет нам тестовую библиотеку: google.golang.org/appengine/aetest. В основном он запускает локальный сервер разработки, что позволяет вам иметь доступ к контексту выполнения. Ранее я указывал, что вы должны использовать контекст appengine повсюду. В тестах мы будем иметь дело с этим тестовым контекстом.
Основная идея состоит в том, чтобы подготовить запрос, запустить тестируемое приложение, выполнить запрос, обслужить запрос и затем проверить ответ от приложения. Другими словами, вы можете вызвать сам обработчик, предоставив ему интерфейс записи и сам запрос. С учетом сказанного базовая структура теста будет выглядеть следующим образом:
func TestService(t *testing.T) { testContext, done, err := aetest.NewContext() if err != nil { t.Fatal(err) } testContext, _ = appengine.Namespace(testContext, "") defer done() t.Run("TEST_NAME", func(t *testing.T) { // Do the test with testContext })
Для выполнения теста вам в основном нужны четыре вещи: служба, запрос, контекст тестирования и реализация http.ResponseWriter. Последний прекрасно представлен библиотекой net/http/httptest и может быть инициализирован следующим образом:
rr := httptest.NewRecorder()
Часть запроса так же проста:
req, _ := http.NewRequest(http.MethodGet, "/_ah/warmup", &bytes.Buffer{})
Теперь, чтобы выполнить фактический запрос, мы должны вызвать наш обработчик, что в основном означает вызов метода ServeHTTP с нашим контекстом тестирования.
handlerFunc = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s.WarmupRequestHandler(w, r) }) handlerFunc.ServeHTTP(rr, req.WithContext(testContext))
Если совсем сложить:
func TestService(t *testing.T) { testContext, done, err := aetest.NewContext() if err != nil { t.Fatal(err) } testContext, _ = appengine.Namespace(testContext, "") defer done() t.Run("TEST_NAME", func(t *testing.T) { // Define the service under test s := &Service{} // Define the request and response recorder req, _ := http.NewRequest(http.MethodGet, "/_ah/warmup", &bytes.Buffer{}) rr := httptest.NewRecorder() // Define test handler and call the actual handler handlerFunc = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s.WarmupRequestHandler(w, r) }) handlerFunc.ServeHTTP(rr, req.WithContext(testContext)) // Status code check if statusCode := rr.Code; statusCode != test.expectedCode { t.Errorf("handler returned wrong status code: got %v want %v", statusCode, test.expectedCode) } // Body check var actualResponse Response json.NewDecoder(rr.Body).Decode(&actualResponse) if actualResponse != Response{"OK"} { t.Errorf("handler returned wrong response code: got %v want %v", actualResponse, Response{"OK"}) } })
Когда дело доходит до тестирования части аутентификации, просто убедитесь, что вы обернули обработчик так же, как это делается в main.go:
handlerFunc = s.Auth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s.WarmupRequestHandler(w, r) })) handlerFunc.ServeHTTP(rr, req.WithContext(testContext))
и не забудьте добавить заголовок токена в запрос
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "TOKEN"))
Полный код тестов можно найти здесь:
Я узнаю больше о модульном тестировании во Части 2. Удачного юнит-тестирования!