GoFiber 기반의 테스트 가능한 API 서버 만들기

8858 단어 gofibergoFiberFiber

Fiber는 Node.js의 Express에서 영감을 얻어 개발된 Go 언어 서버 프레임워크이다. Fiber는 Go 언어에서 가장 빠른 HTTP 엔진인 Fasthttp 기반으로 개발되어, 실제로 밴치마크 성능이 동일한 Go 언어 서버 프레임워크인 Gin, Echo 등과 비교해 매우 좋다.

테스트 가능한?

API는 어떻게 테스트 하는 것일까? 테스트하고자 하는 API에 HTTP 요청을 보내고, 받은 응답이 기대한 바와 같고, 변경된 DB의 상태가 기대한 바와 같다면, 해당 API에 대한 테스트를 수행했다고 할 수 있을 것이다. API 서버의 API는 기본적으로 서버가 관리하는 자원(Resource. 여러가지가 있을 수 있지만, 일단 DB에 저장된 데이터로 범위를 좁혀보자)의 상태를 요청에 맞춰 변경하는 것을 의미한다. 그렇기 때문에, 입력값으로 넣는 테스트 데이터에는 DB 데이터의 사전 상태(Precondition), HTTP 요청이 될 것이고, 검사할 사항은 HTTP 응답과 변경된 DB 데이터 상태가 될 것이다.

Go 언어의 표준 테스트 라이브러리를 사용하면, 이 모든걸 Go 언어 내에서 완료할 수 있다. 굳이 Postman 을 깔고, MongoDB Compass 로 DB 내용을 수동으로 확인하지 않아도 된다. 단, 이를 가능하게 하기 위해서는 기본적으로 서버 코드를 "테스트 가능"하도록 작성해야 한다. 테스트를 가능하도록 하기 위해서는 다음 두가지 조건이 필요하다.

  1. 코드를 통해 서버 객체를 생성, 실행할 수 있어야 한다.
  2. DB 설정 등의 설정 객체를 programmatic하게 조절할 수 있어야 한다.

서버 객체 생성

Fiber 의 경우, fiber.App 객체의 Test(req *http.Request, msTimeout ...int) (resp *http.Response, err error) 함수를 통해, 서버 프로세스를 직접 띄우지 않고도 HTTP Request 를 보내 응답을 받아볼 수 있다.

func TestXxx(t *testing.T) {
	app := fiber.New()
    
    ...
    
    request := httptest.NewRequest(...) // httptest 는 Go 기본 테스트 패키지이다
    resp, err := app.Test(request)
    ...
}

그렇기 때문에, 라우터 설정까지 마친 fiber.App 객체를 생성하여, Test 함수를 사용하면, 마치 포트를 개방하고 실제 서버 프로세스 실행시킨 후, Postman 등의 도구로 API 호출을 보내는 것과 동일한 동작을 흉내낼 수 있다.

이를 위해, 라우터, 그리고 미들웨어 설정만 깔끔하게 추가하여, fiber.App 객체를 반환해주는 NewApp 과 같은 함수를 만들고, 이 함수를 실제 실행할 때는 main 함수에서, 테스트할 때는 TestXxx 함수에서 부를 수 있도록 해야 한다.

이것이 가능하려면, 정해진 경로에서 설정 파일을 읽어오는 등과 같이 외부 상황에 의존하는 코드는 최대한 빼주어야 한다. 사실 대부분 DB 주소나, 의존하고 있는 외부 파일 등에 대한 서버 설정이 여기에 해당한다.

Config 객체 분리

viper를 통해 설정을 관리한다. 프로덕션에서 실제 실행할 때는 당연히 외부 설정 파일로부터 서버 설정 내용들을 읽어오는데, 개인적으로는 config.yaml 파일을 만들고, 이를 읽어들여 서버 설정을 셋팅한다.

테스트를 위해, 서버 설정에 대해 다음과 같은 결정 사항을 내렸다.

  1. struct 로 엄격하게 정의된 Config 객체
  2. config 객체를 글로벌 변수로 정의하고, 2개의 Initialize 함수를 만든다: 첫번째는 config.yaml 파일을 읽어들여 글로벌 변수인configConfig 구조체 객체를 할당하는 함수이고, 다른 하나는 그냥 입력값으로 Config 구조체 객체를 받아서 글로벌 변수에 할당하는 함수이다. 첫번째 함수는 main 함수에서 호출하고, 두번째 함수는 테스트 파일들에서 호출한다.

테스트에서 사용할 설정들은 테스트 파일 내에서 코드로 작성하여 직접 config 객체에 할당해준다. 그리고 프로덕션 단계에서 배포 때 사용할 설정은 config.yaml 파일로 정의하여 빌드 스크립트에 넣어준다. 이를 통해, 테스트와 프로덕션에서의 설정 파일을 깔끔히 분리할 수 있다.

테스트 코드

테스트 코드 자체는 일반적인 Go 언어 테스트 코드와 동일하게 작성한다. 나는 별도의 apitest 라는 폴더를 만들어서, 거기에 테스트 코드 파일들을 모두 넣어두었다. 파일 이름은 {API 이름}_test.go 로 작성한다.

난 별도의 TestMain 을 만들지 않았다. 대신 모든 TestXxx 함수에 DB 초기화 코드를 넣었는데, 이는 각 테스트 함수들이 DB 상태에 대해 서로 의존성을 갖지 않고, 그 함수 내에서 완결되도록 하기 위함이다. 아마 매번 지웠다 새로 넣었다 하니 다소 비효율적일 수는 있는데, 테스트에서 중요한 건 효율이 아니라 완전성이니, 이는 감수하도록 한다.

func TestXxx(t *testing.T) {
	t.Log("CreateCategory")
	t.Cleanup(func() { // Cleanup 함수는 TestXxx 함수가 종료될 때 콜백 함수를 실행해준다.
		if err := db.Drop(...); err != nil {
			t.Fatal(err)
		}
	})

	testCases := []TestXxxData{ // 이 테스트에서 사용할 테스트케이스를 구조체로 명확하게 정의해주면 좋다.
    	{...}
    }
    
    for _, testCase := range testCases {
    	... // DB Precondition 셋팅
        
       	request := httptest.NewRequest(...) // HTTP 요청 생성
        
        app := myapp.NewApp() // app 은 *fiber.App 객체
        resp, err := app.Test(request) // HTTP 호출을 흉내냄
        
        ... // resp 를 이용하여 반환값 검사
        
        ... // DB에 직접 접근하여 DB 상태 변경 내용 검사
    }
}

이 테스트 코드는 테스트하려는 서버와 같은 코드 베이스 안에 있기 때문에, DB에 자유롭게 직접 접근하여 내용을 확인하고, DB에 저장된 객체들에 대한 타이핑도 명확히 할 수 있다.

반환값 또는 DB 상태 변경 내용 검사를 위한 Assert 라이브러리로는 stretchr/testify 라이브러리를 추천한다. 필요한 대부분의 Assertion 함수들이 존재한다.

테스트 실행

테스트를 실행하기 위해서는 일단 DB를 실행시켜야 한다. DB 자체는 API 서버와는 별도의 존재이기 때문에, 수동으로 띄워줘야한다. TestMain 함수에 DB 프로세스 실행 코드를 넣을 수 있지만, 이는 생각보다 꽤 번거롭다. Bash 스크립트에서는 한줄이면 될 일인데 말이다.

그래서 나는 별도의 test.sh 스크립트를 통해 테스트를 실행한다. 이 스크립트에는;

  1. DB 실행
  2. DB 실행이 완료될 때까지 대기 (보통 ping을 보내면서 대기한다)
  3. DB에 접속해서 테스트 DB, 테스트 User 등록
  4. Go 언어 테스트 실행 커맨드

로 구성된다. 1, 2번은 Zero To Production in Rust 책을 참고했다. (참고로 Rust 와 별개로 이 책은 정말 훌륭한 내용을 많이 담고 있다. Rust에 관심없어도 책 자체는 한번 읽어보길 추천한다) 해당 책에서는 PostgreSQL 을 대상으로 했지만, 난 이를 MongoDB 커맨드로만 바꾸어주었다.

이상 GoFiber 서버에서 테스트 하는 법에 대해 정리해보았다.

좋은 웹페이지 즐겨찾기