프로그래밍(Web)/Golang

[바미] Go - test환경 만들기

Bami 2020. 12. 16. 16:12
728x90
반응형

지난번에 이어서 myapp이라는 폴더를 만든 뒤, app.go파일을 만들어 줍니다.
패키지를 분리하는게 테스팅하기 편리하기 때문에 여기에 지난번에 main에 만들었던 코드들을 들어낼 것인데요.

app.go

  package myapp

  import "net/http"


  type User struct {
    FirstName string    `json:"first_name"`
    LastName  string    `json:"last_name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
  }

  func indexHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello World")
  }

  type fooHandler struct{}

  func (f *fooHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    user := new(User)
    err := json.NewDecoder(r.Body).Decode(user)
    if err != nil {
      w.WriteHeader(http.StatusBadRequest)
      fmt.Fprint(w, "Bad Request: ", err)
      return
    }
    user.CreatedAt = time.Now()

    data, _ := json.Marshal(user)
    w.Header().Add("content-type", "application/json")
    w.WriteHeader(http.StatusCreated)
    fmt.Fprint(w, string(data))
  }

  func barHandler(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    if name == "" {
      name = "World"
    }
    fmt.Fprintf(w, "Hello %s!", name)
  }

  func NewHttpHandler() http.Handler {
    mux := http.NewServeMux() // 1
    mux.HandleFunc("/", indexHandler)

    mux.HandleFunc("/bar", barHandler)

    mux.Handle("/foo", &fooHandler{})
  }

1 : main에 있는 코드를 여기에 옮겨 줍니다.

 

그 후 main.go 파일도 수정 해줍니다.

  package main

  import (
    "net/http"

    "./myapp"
  )

  func main() {
    http.ListenAndServe(":3000", myapp.NewHttpHandler()) // 1
  }

1 : myapp안에서 핸들러를 만들어서 핸들러 자리에 등록해줍니다.

 

이 상태에서 실행해주면 hello world가 뜨는 것을 확인할 수 있습니다.

그리고 /bar를 해주면

그리고 /foo를 해주면 json파일이 없기 대문에 EOF가 뜨는 것을 확인 할 수 있습니다.

이제부터 테스팅 코드를 만들게 될건데 아까 만들었던 myapp폴더에 app_test.go파일을 만들어 줍니다.

Go에서는 _test만 붙여주면 Test코드로 작동합니다.

package myapp

func TestIndexPathHandler(t *testing.T) { // 1

}

1 : 이 부분은 양식이 정해져 있는데 testing패키지의 T포인터를 인자로 받게 됩니다.

이렇게 하면 앞에가 test로 시작하게 되고, testing패키지의 T포인터를 인자로 받게되는 함수인 테스트 함수가 되는 것입니다.

이제 패키지 하나를 추가 하게 될 것인데 smartystreetsgoconvey라는 것인데요.

백그라운드에서 돌면서 파일이 갱신될 때 마다 자동으로 테스트를 해줍니다.

goconvey 여기서 자세한 것을 확인 할 수 있습니다.

  go get github.com/smartystreets/goconvey

를 사용하여 다운 받아 봅시다!

 

그 다음 goconvey라는 명령어를 터미널에 실행하게 되면

http:localhost:8080에 테스트 서버가 돌아가는 것을 확인 할 수 있다.

먼저 build error부터 고쳐봅시다!

  package myapp

  import "testing"

  func TestIndexPathHandler(t *testing.T) { // 1

  }

으로 수정하게 되면 PASS가 되었음을 확인 할 수 있습니다.

 

 

이제 이어서 테스트 코드를 만들어보면

  package myapp

  import "testing"

  func TestIndexPathHandler(t *testing.T) { 
        res := httptest.NewRecorder() // 1
        req := httptest.NewRequest("GET", "/", nil) // 2
  }

1 : httptest패키지가 있는데 실제 http프로토콜을 사용하지 않고, 가짜 response 할 수 있는 NewRecorder()를 만들어주고,
2 : 테스트 용으로 request할 수 있는 코드인데 NewRequest() 인자가 3개가 들어간다. method, target, body가 들어갑니다.

그래서 methodget, targetindexpath , body는 없으니까 nil로 해줍니다.

그 뒤 myapp/app.go에 돌아와서 NewHttpHandler()부분을 수정하여 줍니다.

  func indexHandler(w http.ResponseWriter, r *http.Request) {
      fmt.Fprint(w, "Hello World")
  }

  func NewHttpHandler() http.Handler {
    mux := http.NewServeMux()
    mux.HandleFunc("/", indexHandler) // 1

    mux.HandleFunc("/bar", barHandler)

    mux.Handle("/foo", &fooHandler{})
    return mux
}

1 : 람다에서 indexHandler라는 함수로 분리를 시켜주었다.

다시 myapp/app_test.go로 넘어와서 이어서 작성해준다.

  package myapp

  import "testing"

  func TestIndexPathHandler(t *testing.T) { 
        res := httptest.NewRecorder() 
        req := httptest.NewRequest("GET", "/", nil)

      indexHandler(res, req) // 1

      if res.Code != http.StatusOK { // 2
        t.Fatal("Failed!" , res.Code)
      }
  }

1 : 아까 만들었던 함수를 request하고, response를 넣어서 불러줍니다.
2 : res.Code가 실패했을 경우 프로그램이 종료되도록 합니다.

 

이 상태로 저장을 하면 goconvey가 테스트를 진행 할 것이고, 결과가 뜨게 됩니다.

이번에 가져올 패키지는 stretchr의 testfy라는 패키지를 추가할 것인데, 여기서 assert를 가져올 것입니다.

  go get github.com/stretchr/testify/assert

를 새 터미널 창을 추가해서 설치해줍니다. 그 뒤 다시 돌아와서

  package myapp

  import (
        "io/ioutil"
        "net/http"
        "net/http/httptest"
        "testing"

        "github.com/stretchr/testify/assert"
  )

  func TestIndexPathHandler(t *testing.T) {
      assert := assert.New(t) // 1
        res := httptest.NewRecorder() 
        req := httptest.NewRequest("GET", "/", nil)

      indexHandler(res, req)

      assert.Equal(http.StatusOK, res.Code) // 2
      data, _ := ioutil.ReadAll(res.Body) // 3
        assert.Equal("Hello World", string(data)) 
  }

  func TestBarPathHandler_WithoutName(t *testing.T) { // 4
    assert := assert.New(t)

    res := httptest.NewRecorder()
    req := httptest.NewRequest("GET", "/bar", nil)

    barHandler(res, req)

    assert.Equal(http.StatusOK, res.Code)
    data, _ := ioutil.ReadAll(res.Body)
    assert.Equal("Hello World!", string(data))
  }

1 : assert를 추가 해줍니다.

2 : 기대하는 값(statusOK)과 맞는지 확인해줍니다.
-> 이렇게 하면 아까 썼던 if문을 쓰지 않아도 됩니다.

3 : 실제 결과값은 res.Body에 들어있는데 바로 가져올 수 없기 때문에 ioutil.ReadAll을 사용해서 버퍼값을 전부 가져오게 합니다.
-> 이것을 사용하면 return 값과 error값이 나오게 되는데 data만 가져옵니다.

4 : barHander 추가.

myapp/app.go에 barHandler 출력값이 'Hello World!'이므로 테스트코드에도 Hello World!로 넣어줍니다.

이 때 저장 후에 goconvey로 테스팅을 하면 PASS가 뜨게 됩니다.

  package myapp

  import (
        "io/ioutil"
        "net/http"
        "net/http/httptest"
        "testing"

        "github.com/stretchr/testify/assert"
  )

  func TestIndexPathHandler(t *testing.T) {
      assert := assert.New(t)
        res := httptest.NewRecorder() 
        req := httptest.NewRequest("GET", "/", nil)

      indexHandler(res, req)

      assert.Equal(http.StatusOK, res.Code)
      data, _ := ioutil.ReadAll(res.Body)
        assert.Equal("Hello World", string(data)) 
  }

  func TestBarPathHandler_WithoutName(t *testing.T) {
    assert := assert.New(t)

    res := httptest.NewRecorder()
    req := httptest.NewRequest("GET", "/", nil)

    barHandler(res, req)

    assert.Equal(http.StatusOK, res.Code)
    data, _ := ioutil.ReadAll(res.Body)
    assert.Equal("Hello World!", string(data))
  }

이제 '/bar'를 호출했는데, '/'로 바꾸어서 호출해보도록 하겠습니다.

그 뒤 저장하면 goconvey가 테스트 코드로 돌릴 것이고, 여전히 pass가 뜰 것 입니다.

여기서 이상한 것은 barHander로 호출되어 '/'로 보내졌을 때 '!'가 없어야 되는데 '!'가 있게 왔다는 의미입니다.
즉, mux를 제대로 사용하고 있지 않아 타겟이 '/'인데, barHandler()를 직접 호출하다 보니까 타겟이 적용이 안되었다는 것입니다.
그래서 barHandler(res, req)가 아니라 mux를 사용해야 타겟에 제대로 맞추어 사용된다는 것입니다.

그래서 코드를 수정하면

  func TestBarPathHandler_WithoutName(t *testing.T) {
    assert := assert.New(t)

    res := httptest.NewRecorder()
    req := httptest.NewRequest("GET", "/", nil)

    mux := NewHttpHandler() // 추가
    mux.ServeHTTP(res, req) // 추가

    assert.Equal(http.StatusOK, res.Code)
    data, _ := ioutil.ReadAll(res.Body)
    assert.Equal("Hello World!", string(data))
  }

이렇게 되는데 이때 테스팅을 하게 되면

FAIL이 뜨는 것을 확인 할 수 있습니다.

 

그래서 그 원인을 보면

'!'가 있어야 되는데 없다고 뜹니다. 정상적으로 인덱스 경로에 호출 되었다는 것을 알 수 있습니다.

그래서 인덱스 경로에서 '/bar'경로로 바꾸어 줍시다!

  func TestBarPathHandler_WithoutName(t *testing.T) {
    assert := assert.New(t)

    res := httptest.NewRecorder()
    req := httptest.NewRequest("GET", "/bar", nil)

    mux := NewHttpHandler()
    mux.ServeHTTP(res, req)

    assert.Equal(http.StatusOK, res.Code)
    data, _ := ioutil.ReadAll(res.Body)
    assert.Equal("Hello World!", string(data))
  }

그리고

      mux := NewHttpHandler()
      mux.ServeHTTP(res, req)

이 부분을 카피하여 IndexPathHandler에 붙여넣기 해주면 분리되게 됩니다.

  func TestIndexPathHandler(t *testing.T) {
    assert := assert.New(t)

    res := httptest.NewRecorder()
    req := httptest.NewRequest("GET", "/", nil)

    mux := NewHttpHandler()
    mux.ServeHTTP(res, req)

    assert.Equal(http.StatusOK, res.Code)
    data, _ := ioutil.ReadAll(res.Body)
    assert.Equal("Hello World", string(data))
  }

그래서 실행하면 PASS 되는 것을 확인할 수 있습니다.

 

이번에는 name과 함께 넣는 함수를 만들어 봅시다! 기존에 있던 TestBarPathHandler_WithoutName함수를 복붙해 줍니다.

  func TestBarPathHandler_WithName(t *testing.T) {
    assert := assert.New(t)

    res := httptest.NewRecorder()
    req := httptest.NewRequest("GET", "/bar?name=chagbeom", nil) // 1

    mux := NewHttpHandler()
    mux.ServeHTTP(res, req)

    assert.Equal(http.StatusOK, res.Code)
    data, _ := ioutil.ReadAll(res.Body)
    assert.Equal("Hello changbeom!", string(data)) // 2
  }

1 : URL에 이름을 넣습니다.
2 : 해당 string과 같은지 비교해 줍니다.

 

이렇게 해서 저장을 해주면 테스트 코드가 돌게 되고, 서버 실행 시 제대로 출력 됐음을 알 수 있습니다.

풀 소스


myapp/app_test.go

  package myapp

  import (
    "io/ioutil"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/stretchr/testify/assert"
  )

  func TestIndexPathHandler(t *testing.T) {
    assert := assert.New(t)

    res := httptest.NewRecorder()
    req := httptest.NewRequest("GET", "/", nil)

    mux := NewHttpHandler()
    mux.ServeHTTP(res, req)

    assert.Equal(http.StatusOK, res.Code)
    data, _ := ioutil.ReadAll(res.Body)
    assert.Equal("Hello World", string(data))
  }

  func TestBarPathHandler_WithoutName(t *testing.T) {
    assert := assert.New(t)

    res := httptest.NewRecorder()
    req := httptest.NewRequest("GET", "/bar", nil)

    mux := NewHttpHandler()
    mux.ServeHTTP(res, req)

    assert.Equal(http.StatusOK, res.Code)
    data, _ := ioutil.ReadAll(res.Body)
    assert.Equal("Hello World!", string(data))
  }

  func TestBarPathHandler_WithName(t *testing.T) {
    assert := assert.New(t)

    res := httptest.NewRecorder()
    req := httptest.NewRequest("GET", "/bar?name=chagbeom", nil)

    mux := NewHttpHandler()
    mux.ServeHTTP(res, req)

    assert.Equal(http.StatusOK, res.Code)
    data, _ := ioutil.ReadAll(res.Body)
    assert.Equal("Hello changbeom!", string(data))
  }

myapp/app.go

  package myapp

  import (
    "encoding/json"
    "fmt"
    "net/http"
    "time"
  )

  type User struct {
    FirstName string    `json:"first_name"`
    LastName  string    `json:"last_name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
  }

  func indexHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello World")
  }

  type fooHandler struct{}

  func (f *fooHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    user := new(User)
    err := json.NewDecoder(r.Body).Decode(user)
    if err != nil {
      w.WriteHeader(http.StatusBadRequest)
      fmt.Fprint(w, "Bad Request: ", err)
      return
    }
    user.CreatedAt = time.Now()

    data, _ := json.Marshal(user)
    w.Header().Add("content-type", "application/json")
    w.WriteHeader(http.StatusCreated)
    fmt.Fprint(w, string(data))
  }

  func barHandler(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    if name == "" {
      name = "World"
    }
    fmt.Fprintf(w, "Hello %s!", name)
  }

  func NewHttpHandler() http.Handler {
    mux := http.NewServeMux()
    mux.HandleFunc("/", indexHandler)

    mux.HandleFunc("/bar", barHandler)

    mux.Handle("/foo", &fooHandler{})
    return mux
  }

main.go

  package main

  import (
    "net/http"

    "./myapp"
  )

  func main() {

    http.ListenAndServe(":3000", myapp.NewHttpHandler())
  }

 

출처 : Turker의 Golang

728x90
반응형