httptest를 이용한 Go 웹 애플리케이션 단위 및 통합 테스트
Daniel Hayes
Full-Stack Engineer · Leapcell

효과적인 테스트를 통한 견고한 Go 웹 앱 구축
Go에서 견고하고 신뢰할 수 있는 웹 애플리케이션을 개발하려면 포괄적인 테스트가 필요합니다. 강력한 테스트 기반이 없으면 사소한 코드 변경조차 예상치 못한 버그를 초래하여 사용자에게 불편을 주고 비용이 많이 드는 디버깅 주기로 이어질 수 있습니다. Go의 단순성과 강력한 타이핑은 본질적으로 특정 종류의 오류를 줄여주지만, 특히 웹 컨텍스트에서 다양한 구성 요소 간의 상호 작용은 전용 검증이 필요합니다. 이때 단위 및 통합 테스트가 필수적입니다.
이들은 개발자가 리팩토링, 새 기능 추가, 자신 있게 배포할 수 있도록 안전망을 제공합니다. 이 글에서는 Go 웹 애플리케이션을 효과적으로 테스트하는 방법, 특히 종종 복잡한 HTTP 요청 및 응답 시뮬레이션 작업을 단순화하는 Go 표준 라이브러리에서 제공하는 강력한 httptest
패키지에 초점을 맞춰 살펴봅니다.
웹 애플리케이션 테스트의 핵심 기둥 이해
실질적인 내용으로 들어가기 전에 웹 애플리케이션과 관련된 핵심 테스트 개념에 대한 일반적인 이해를 확립합시다.
단위 테스트(Unit Test): 단위 테스트는 애플리케이션의 가장 작은 테스트 가능한 부분, 종종 개별 함수 또는 메서드에 중점을 둡니다. 목표는 이러한 단위를 격리하고 데이터베이스나 HTTP 서버와 같은 외부 종속성과 독립적으로 올바름을 검증하는 것입니다. 웹 핸들러의 경우 단위 테스트는 HTTP 요청/응답 주기에서 분리하여 핸들러 함수 내부의 비즈니스 논리를 테스트할 수 있습니다.
통합 테스트(Integration Test): 통합 테스트는 애플리케이션의 다른 단위 또는 구성 요소가 올바르게 함께 작동하는지 확인합니다. 웹 애플리케이션의 맥락에서 이는 종종 들어오는 HTTP 요청부터 미들웨어, 핸들러, 최종적으로 생성된 HTTP 응답까지 전체 요청-응답 흐름을 테스트하는 것을 의미합니다. 통합 테스트는 구성 요소 간의 "연결부"를 확인합니다.
net/http
패키지: Go의 표준 라이브러리 net/http
패키지는 기본적인 HTTP 클라이언트 및 서버 구현을 제공합니다. 거의 모든 Go 웹 애플리케이션의 백본을 형성하며, http.Handler
인터페이스, http.Request
및 http.ResponseWriter
유형, http.Handle
및 http.ListenAndServe
와 같은 함수를 정의합니다.
net/http/httptest
패키지: 이것이 우리의 스타 플레이어입니다. httptest
패키지는 HTTP 테스트를 위한 유틸리티를 제공합니다. http.ResponseWriter
및 http.Request
객체를 프로그래밍 방식으로 생성할 수 있어 실제 네트워크 서버를 시작하지 않고도 http.Handler
구현에 HTTP 요청을 시뮬레이션하는 것이 매우 쉽습니다. 사실상 통합 테스트를 매우 빠르고 메모리 내 프로세스로 전환합니다.
httptest
사용의 기본 원리는 간단합니다. 실행 중인 서버에 실제 네트워크 요청을 보내는 대신 메모리에서 http.Request
객체를 구성하고 httptest.ResponseRecorder
( http.ResponseWriter
를 구현함)를 생성한 다음 http.Handler
의 ServeHTTP
메서드를 직접 호출합니다. ResponseRecorder
는 응답 상태, 헤더 및 본문을 캡처하여 이를 기준으로 어설션할 수 있게 합니다.
실용적인 예제를 통해 설명해 보겠습니다.
사용자에게 인사하는 핸들러가 있는 간단한 Go 웹 애플리케이션을 생각해 보겠습니다.
// main.go package main import ( "fmt" "log" "net/http" ) func GreetHandler(w http.ResponseWriter, r *http.Request) { name := r.URL.Query().Get("name") if name == "" { name = "Guest" } fmt.Fprintf(w, "Hello, %s!", name) } func main() { http.HandleFunc("/greet", GreetHandler) log.Fatal(http.ListenAndServe(":8080", nil)) }
GreetHandler 단위 테스트 (논리 중점)
GreetHandler
는 작지만, 더 복잡했다면 핵심 논리를 "단위 테스트"하는 방법을 보여줄 수 있습니다. 그러나 이러한 간단한 핸들러의 경우 단위 테스트와 통합 테스트의 경계가 흐려집니다. 진정으로 격리 가능한 "단위"를 위해서는 이상적으로는 인사 논리를 별도의 함수로 추출해야 합니다.
// handler_test.go package main import ( "net/http" "net/http/httptest" "testing" ) func TestGreetHandler_NoName(t *testing.T) { req, err := http.NewRequest("GET", "/greet", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() handler := http.HandlerFunc(GreetHandler) // 우리의 함수를 http.Handler로 래핑 handler.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } expected := "Hello, Guest!" if rr.Body.String() != expected { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) } } func TestGreetHandler_WithName(t *testing.T) { req, err := http.NewRequest("GET", "/greet?name=Alice", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() handler := http.HandlerFunc(GreetHandler) handler.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } expected := "Hello, Alice!" if rr.Body.String() != expected { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) } }
이 예제에서는 원하는 메서드와 URL을 가진 http.Request
를 생성한 다음 출력을 캡처하기 위한 httptest.ResponseRecorder
를 생성합니다. 그런 다음 handler.ServeHTTP(rr, req)
를 직접 호출합니다. 이렇게 하면 네트워크 스택이 완전히 우회되므로 테스트가 매우 빠르고 격리됩니다. 이것은 핸들러에 대한 통합 테스트의 한 형태로, HTTP 요청의 맥락에서 해당 동작을 검증합니다.
라우터 및 미들웨어와의 통합 테스트
실제 애플리케이션은 종종 라우팅 라이브러리(Gorilla Mux
, Chi
또는 Echo
와 같은)와 미들웨어를 사용합니다. httptest
는 이러한 시나리오에서도 마찬가지로 효과적입니다. Gorilla Mux
를 사용하는 애플리케이션을 고려해 봅시다.
// main.go (Gorilla Mux를 사용하도록 수정됨) package main import ( "fmt" "log" "net/http" "github.com/gorilla/mux" ) func GreetHandlerMux(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) name := vars["name"] if name == "" { name = "Guest" } fmt.Fprintf(w, "Hello, %s!", name) } func LoggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Printf("Request received: %s %s", r.Method, r.URL.Path) next.ServeHTTP(w, r) }) } func NewRouter() *mux.Router { r := mux.NewRouter() r.Use(LoggingMiddleware) r.HandleFunc("/greet/{name}", GreetHandlerMux).Methods("GET") r.HandleFunc("/greet", GreetHandlerMux).Methods("GET") // /greet?name=... 용 return r } func main() { router := NewRouter() log.Fatal(http.ListenAndServe(":8080", router)) }
이제 이 설정에 대한 통합 테스트를 작성하여 라우팅 및 미들웨어가 예상대로 작동하는지 확인하겠습니다.
// router_test.go package main import ( "net/http" "net/http/httptest" "testing" ) func TestRouter_GreetWithNameFromPath(t *testing.T) { router := NewRouter() // 구성된 라우터 가져오기 req, err := http.NewRequest("GET", "/greet/Bob", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() router.ServeHTTP(rr, req) // 라우터가 요청을 처리하도록 함 if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } expected := "Hello, Bob!" if rr.Body.String() != expected { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) } } func TestRouter_GreetWithQueryParam(t *testing.T) { router := NewRouter() req, err := http.NewRequest("GET", "/greet?name=Charlie", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() router.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } expected := "Hello, Charlie!" if rr.Body.String() != expected { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) } } func TestRouter_NotFoundRoute(t *testing.T) { router := NewRouter() req, err := http.NewRequest("GET", "/nonexistent", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() router.ServeHTTP(rr, req) // Gorilla Mux는 기본적으로 일치하지 않는 경로에 대해 404를 반환합니다. if status := rr.Code; status != http.StatusNotFound { t.Errorf("handler returned wrong status code for not found: got %v want %v", status, http.StatusNotFound) } }
이 예제에서는 전체 mux.Router
를 인스턴스화한 다음 httptest.ResponseRecorder
의 ServeHTTP
메서드에 전달합니다. 이렇게 하면 실제 네트워크 통신 오버헤드 없이 라우팅 및 적용된 미들웨어를 포함한 전체 요청 처리 파이프라인을 테스트할 수 있습니다.
고급 사용법: httptest.Server
때로는 클라이언트 측 논리 또는 실제 실행 중인 HTTP 서버를 진정으로 기대하는 외부 서비스 통합을 테스트해야 할 수도 있습니다. 이러한 경우 httptest
는 httptest.Server
를 제공합니다. 이 유틸리티는 실제(비록 로컬이지만) HTTP 서버를 사용 가능한 포트에서 시작합니다.
// client_test.go package main import ( "io/ioutil" "net/http" "net/http/httptest" "testing" ) func TestExternalServiceCall(t *testing.T) { // 우리의 모의 외부 서비스 핸들러 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/data" && r.Method == "GET" { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"message": "Hello from mock server!"}`)) } else { w.WriteHeader(http.StatusNotFound) } }) // 실제 HTTP 테스트 서버 시작 server := httptest.NewServer(handler) defer server.Close() // 테스트 후 서버가 닫히도록 보장 // 이제 server.URL을 사용하여 모의 서버에 실제 HTTP 요청을 할 수 있습니다. resp, err := http.Get(server.URL + "/data") if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Errorf("expected status OK, got %v", resp.StatusCode) } body, err := ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } expectedBody := `{"message": "Hello from mock server!"}` if string(body) != expectedBody { t.Errorf("expected body %q, got %q", expectedBody, string(body)) } // 실패해야 하는 경로도 테스트할 수 있습니다. resp, err = http.Get(server.URL + "/unknown") if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusNotFound { t.Errorf("expected status NotFound, got %v", resp.StatusCode) } }
httptest.Server
는 http.Client
를 사용하여 외부 API와 상호 작용하는 코드를 테스트할 때 매우 중요합니다. 이러한 외부 API를 시뮬레이션하고 응답을 제어하여 클라이언트 측 논리가 다양한 시나리오를 올바르게 처리하도록 보장할 수 있습니다.
결론
httptest
패키지는 견고하고 신뢰할 수 있는 Go 웹 애플리케이션을 구축하는 데 필수적인 도구입니다. 개발자는 실제 네트워크 통신 오버헤드 없이 HTTP 핸들러, 미들웨어 및 라우팅 논리에 대한 빠르고 격리된 포괄적인 단위 및 통합 테스트를 작성할 수 있습니다. httptest.ResponseRecorder
및 httptest.Server
를 활용하면 개발자는 웹 구성 요소의 정확성을 자신 있게 검증하여 더 안정적인 애플리케이션과 효율적인 개발 주기를 달성할 수 있습니다. httptest
를 마스터하면 Go 웹 애플리케이션 테스트를 마스터하게 됩니다.