gqlgen 으로 구성된 API 서버의 엔드포인트(= query / mutation) 을 테스트해보자
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의 쿼리를 넣고, 응답을 받을 구조체를 넣어준다.
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
먼저 테스트 클라이언트는 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를 할당해준다.
이제 이 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 즉, 임의의 유저 모델이 포함된 컨텍스트를 함께 넘겨주게 되어 정상적으로 인증절차를 수행할 수 있게 된다!
궁금한건 댓글로 남겨달라.