Golang gqlgen 이용하여 API 엔드포인트 테스트하기

배경
gqlgen 으로 구성된 API 서버의 엔드포인트(= query / mutation) 을 테스트해보자
해결
클라이언트 설정
gqlgen에서 테스트용 클라이언트 패키지를 제공한다.
테스트용 클라이언트를 만들고 POST 요청을 보낼 수 있도록 구성되어있다.
func TestResolver(t *testing.T) {
// 테스트 클라이언트가 접근할 서버(httpHandler)를 정의한다.
srv := handler.NewDefaultServer(apiServer.NewExecutableSchema(apiServer.Config{Resolvers: &Resolver{}}))
}
// 테스트 클라이언트를 정의한다.
c := client.New(srv)
t.Run("Create User", func(t *testing.T) {
... // 여기에 테스트 코드 작성
})
- 이런식으로 서버를 등록하고, 클라이언트가 그 서버에 요청을 보낼 수 있도록 등록해주면 된다.
테스트 코드
func TestResovler(t *testing.T) {
srv := handler.NewDefaultServer(apiServer.NewExecutableSchema(apiServer.Config{Resolvers: &Resolver{}}))
c := client.New(srv)
t.Run("login", func(t *testing.T) {
testUserId := "test"
testUserPw := "pw"
testUserJwt := GenerateJwt(testUserId, myDecrpytFunc(testUserPw)
var resp struct {
Login string
}
queryStr := fmt.Sprintf(`
mutation Login {
login(
input: {
id: "%s",
pw: "%s",
}
)
}`, testUserId, testUserPw)
c.MustPost(queryStr, &resp)
actualToken := resp.Login
require.Equal(t, testUserJwt, actualToken)
})
}
input으로 id와 pw를 받고 바로 string으로 access token을 응답하는 로그인 API이다.
c 로 초기화한 client의
MustPost메서드에 graphql의 쿼리를 넣고, 응답을 받을 구조체를 넣어준다.주의사항!! 이때 응답을 받을 resp 구조체는 대문자로 시작해야된다..

위와 같은 응답값이 있다면, resp 구조체는 아래의 컨벤션으로 작성되어야 한다.
var resp struct {
login string
}
var resp struct {
data struct {
login string
}
}
# 이 아니라,
var resp struct {
Login string
}
# 이어야 한다.
var resp map[string]interface{}
# 이렇게 빈 인터페이스도 언패킹이 가능하긴 하다.
- 모르겠으면 이 이슈를 참고해보자
다시 코드로 돌아가서,
c.MustPost(query, &resp)코드를 통해 테스트 클라이언트는 등록된 테스트 서버에 요청을 보내게 된다.MustPost메서드는 테스트 진행시 편의를 위해 err 발생시 panic을 띄워주는 메서드로, 이게 불편하다면Post메서드를 사용해도 된다.// github.com/99designs/gqlgen/client/client.go의 일부 코드 // MustPost is a convenience wrapper around Post that automatically panics on error func (p *Client) MustPost(query string, response interface{}, options ...Option) { if err := p.Post(query, response, options...); err != nil { panic(err) } } // Post sends a http POST request to the graphql endpoint with the given query then unpacks // the response into the given object. func (p *Client) Post(query string, response interface{}, options ...Option) error { respDataRaw, err := p.RawPost(query, options...) if err != nil { return err } // we want to unpack even if there is an error, so we can see partial responses unpackErr := unpack(respDataRaw.Data, response) if respDataRaw.Errors != nil { return RawJsonError{respDataRaw.Errors} } return unpackErr }테스트는 잘 작동한다.
=== RUN TestResolvers === RUN TestResolvers/Login --- PASS: TestResolvers (0.07s) --- PASS: TestResolvers/Login (0.02s) PASS
테스트 클라이언트에 context 주입하기
로그인이 필요한 API 엔드포인트를 테스트하는 상황을 가정해보자.
일반적으로는 http middleware를 서버에 등록해두고, 인증이 필요한 API에 요청이 올때마다 미들웨어가 access_token을 확인하여 유저 정보를 불러오고 이를 context에 넣어 다음 요청에 넘겨주는 방식으로 처리 할 것이다. 예를 들면 이렇게 ! gqlgen authentication 예제 링크
하지만 gqlgen 테스트 클라이언트는 실제로 HTTP 요청을 보내는 것이 아니기 떄문에 테스트 서버에 미들웨어를 등록해도 사용할 수 없다고 한다!! 참고
그래서 Test Client의 요청에 직접 인증에 필요한 유저 context를 넣어주기로 했다.
- 먼저 테스트 클라이언트는 Option Pattern 을 활용해 여러 Option 값을 가변인자로 받을 수 있게 설계되어있다. 아래의 코드에서 볼 수 있듯,
New메서드로 테스트 클라이언트를 생성할때 option 값으로 Request 구조체에 해당하는 값들을 넣어줄 수 있다. 여기서 HTTP 필드에 컨텍스트를 넘겨줄 수 있는데, 그 이유는 HTTP 필드의 타입이 http.Request의 포인터이기 때문이다.
// github.com/99designs/gqlgen/client/client.go의 일부 코드
type (
// Client used for testing GraphQL servers. Not for production use.
Client struct {
h http.Handler
opts []Option
}
// Option implements a visitor that mutates an outgoing GraphQL request
//
// This is the Option pattern - https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
Option func(bd *Request)
// Request represents an outgoing GraphQL request
Request struct {
Query string `json:"query"`
Variables map[string]interface{} `json:"variables,omitempty"`
OperationName string `json:"operationName,omitempty"`
HTTP *http.Request `json:"-"`
}
// Response is a GraphQL layer response from a handler.
Response struct {
Data interface{}
Errors json.RawMessage
Extensions map[string]interface{}
}
)
// New creates a graphql client
// Options can be set that should be applied to all requests made with this client
func New(h http.Handler, opts ...Option) *Client {
p := &Client{
h: h,
opts: opts,
}
return p
}
- 그렇다면 먼저 Option 자리에서 클라이언트에 context를 주입해주는 코드를 작성하자.
func addContext(user *domain.UserDAO) client.Option {
return func(bd *client.Request) {
ctx := bd.HTTP.Context()
ctx = context.WithValue(ctx, "userAuthCtx", user)
bd.HTTP = bd.HTTP.WithContext(ctx)
}
}
유저 모델을 인자로 받아. HTTP의 컨텍스트에 우리 서버가 정의한 인증절차에 맞게 유저 컨텍스트를 주입한다. 그리고 bd.HTTP에 이 컨텍스트가 들어있는 bd.HTTP를 할당해준다. 3. 이제 이 addContext를 테스트 클라이언트에 넣어주면 된다.
// Test UpdateUserInfo
t.Run("UpdateUserInfo", func(t *testing.T) {
var resp struct {
UpdateUserInfo struct {
Id string
Mobile string
Name string
Email string
}
}
//var resp map[string]interface{}
name := "테스트"
email := "test@gqltest.com"
testUser, _ := repo.User.GetByEmail(email)
queryStr := fmt.Sprintf(`
mutation UpdateUserInfo {
updateUserInfo(
input: {
uuid: "%s"
mobile: "%s"
name: "%s"
email: "%s"
}
) {
id, uuid, mobile, name, email
}
}
`, testUserUUID, testUserMobile, name, email)
// MustPost 요청의 Option 인자로 위에서 정의한 addContext 함수를 넣어주면 된다!!
c.MustPost(queryStr, &resp, addContext(testUser))
require.Equal(t, resp.UpdateUserInfo.Name, name)
require.Equal(t, resp.UpdateUserInfo.Email, email)
})
}
- 이렇게 하면
MustPost를 호출하는 테스트 클라이언트가 테스트서버에 testUser 즉, 임의의 유저 모델이 포함된 컨텍스트를 함께 넘겨주게 되어 정상적으로 인증절차를 수행할 수 있게 된다!
궁금한건 댓글로 남겨달라.




