Golang에서 모의 ​​HTTP 호출

23210 단어 testhttpgobeginners


이 블로그 게시물 코드는 go1.16.2에서 실행 중입니다.

테스트할 API 인터페이스




type API interface {
  // this function will do http call to external resource
  FetchPostByID(ctx context.Context, id int) (*APIPost, error)
}

type APIPost struct {
  ID int `json:"id"`
  UserID int `json:"userId"`
  Title string `json:"title"`
  Body string `json:"body"`
}


다음과 같이 API interface FetchPostByID function의 모의 구현을 생성하여 단위 테스트에서 API interface 결과를 간단히 모의할 수 있습니다.

API 모의 구현




type APIMock struct {}

func (a APIMock) FetchPostByID(ctx context.Context, id int) (*APIPost, error) {
  return nil, fmt.Errorf(http.StatusText(http.StatusNotFound))
}


그러나 이렇게 하면 테스트 범위가 증가하지 않으며 실제 구현FetchPostByID 내부의 나머지 코드를 건너뜁니다.

그래서 우리는 API interface의 테스트 가능한 실제 구현을 먼저 만들 것입니다.

구현



HTTP 호출만 모의 처리하려면 http.Client 모의 구현을 만들어야 합니다. real http.Client에는 HTTP 호출을 원할 때마다 실행되는 Do function가 있습니다. 그래서 우리는 Do function 를 조롱해야 합니다. http.Client에는 구현된 인터페이스가 없으므로 하나 만들어야 합니다.

HTTP 클라이언트 모의




type HTTPClient interface {
  Do(*http.Request) (*http.Response, error)
}

type HTTPClientMock struct {
  // DoFunc will be executed whenever Do function is executed
  // so we'll be able to create a custom response
  DoFunc func(*http.Request) (*http.Response, error)
}

func (H HTTPClientMock) Do(r *http.Request) (*http.Response, error) {
  return H.DoFunc(r)
}


API 구현 구조




func NewAPI(client HTTPClient, baseURL string, timeout time.Duration) API {
  return &apiV1{
    c: client,
    baseURL: baseURL,
    timeout: timeout,
  }
}

type apiV1 struct {
  // we need to put the http.Client here
  // so we can mock it inside the unit test
  c HTTPClient
  baseURL string
  timeout time.Duration
}

func (a apiV1) FetchPostByID(ctx context.Context, id int) (*APIPost, error) {
  u := fmt.Sprintf("%s/posts/%d", a.baseURL, id)

  ctx, cancel := context.WithTimeout(ctx, a.timeout)
  defer cancel()

  req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
  if err != nil {
    return nil, err
  }

  resp, err := a.c.Do(req)
  if err != nil {
    return nil, err
  }
  defer resp.Body.Close()

  if resp.StatusCode != http.StatusOK {
    return nil, fmt.Errorf(http.StatusText(resp.StatusCode))
  }

  var result *APIPost
  return result, json.NewDecoder(resp.Body).Decode(&result)
}


단위 테스트




var (
  // our custom client
  client = &HTTPClientMock{}
  // our api
  api = NewAPI(client, "", 0)
)

func TestApiV1_FetchPostByID(t *testing.T) {
  // test table
  tt := []struct {
    // Body mock the response body
    Body string
    // StatusCode mock the response statusCode
    StatusCode int

    // Expected Result
    Result *APIPost
    // Expected Error
    Error error
  }{
    {
      Body: `{"userId": 1,"id": 1,"title": "test title","body": "test body"}`,
      StatusCode: 200,
      Result: &APIPost{
        ID: 1,
        UserID: 1,
        Title: "test title",
        Body: "test body",
      },
      Error: nil,
    },
    {
      Body: `{"userId": 2,"id": 2,"title": "test title2","body": "test body2"}`,
      StatusCode: 200,
      Result: &APIPost{
        ID: 2,
        UserID: 2,
        Title: "test title2",
        Body: "test body2",
      },
      Error: nil,
    },
    {
      Body: ``,
      StatusCode: http.StatusNotFound,
      Result: nil,
      Error: fmt.Errorf(http.StatusText(http.StatusNotFound)),
    },
    {
      Body: ``,
      StatusCode: http.StatusBadRequest,
      Result: nil,
      Error: fmt.Errorf(http.StatusText(http.StatusBadRequest)),
    },
  }

  for _, test := range tt {
    // we adjust the DoFunc for each test case
    client.DoFunc = func(r *http.Request) (*http.Response, error) {
      return &http.Response{
        // create the custom body
        Body: io.NopCloser(strings.NewReader(test.Body)),
        // create the custom status code
        StatusCode: test.StatusCode,
      }, nil
    }

    // execute the func
    p, err := api.FetchPostByID(context.Background(), 0)

    // validation
    if err != nil && err.Error() != test.Error.Error() {
      t.Fatalf("want %v, got %v", test.Error, err)
    }

    if !reflect.DeepEqual(p, test.Result) {
      t.Fatalf("want %v, got %v", test.Result, p)
    }
  }
}

http.Client만 변경하기 때문에 FetchPostByID func는 다음 줄을 제외하고 있는 그대로 테스트됩니다.

resp, err := a.c.Do(req)

a.c.Do는 단위 테스트 내에서 모의DoFunc로 이미 조정되었으므로 a.c.Do 동작은 다음 줄에 따라 변경됩니다.

client.DoFunc = func(r *http.Request) (*http.Response, error) {
  return &http.Response{
    Body: io.NopCloser(strings.NewReader(test.Body)),
    StatusCode: test.StatusCode,
  }, nil
}


테스트를 실행하자

$ go test ./... -race -coverprofile /tmp/coverage.out && go tool cover -html=/tmp/coverage.out


좋은 웹페이지 즐겨찾기