gqlgen test client 이용하여 간단하게 테스트하기

Sean Kim·2022년 4월 12일
0

GoLang

목록 보기
2/5
post-thumbnail

배경

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의 쿼리를 넣고, 응답을 받을 구조체를 넣어준다.

    • 주의사항!! 이때 응답을 받을 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를 넣어주기로 했다.

  1. 먼저 테스트 클라이언트는 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
    }
  2. 그렇다면 먼저 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)
        })
    }
  4. 이렇게 하면 MustPost 를 호출하는 테스트 클라이언트가 테스트서버에 testUser 즉, 임의의 유저 모델이 포함된 컨텍스트를 함께 넘겨주게 되어 정상적으로 인증절차를 수행할 수 있게 된다!


    궁금한건 댓글로 남겨달라.

profile
이것저것 해보고있습니다.

0개의 댓글