본문 바로가기

GO lang/web

[GO] Web - Restful API, TDD

패키지 설치 위치 - 본인의 프로젝트 폴더에서 실행.
패키지 설치 방법
1. go get -u github.com/gorilla/mux

D:\workspace\GO\tuckersGo\goWeb\web05>go get -u github.com/gorilla/mux
go: downloading github.com/gorilla/mux v1.8.0
go get: added github.com/gorilla/mux v1.8.0

 

환경 설정은 아래와 같이 설정함.

PS D:\workspace\GO\tuckersGo\goWeb> cd .\web05\  
PS D:\workspace\GO\tuckersGo\goWeb\web05> go mod init GO/tuckersGo/goWeb/web05 
go: creating new go.mod: module GO/tuckersGo/goWeb/web05
go: to add module requirements and sums:
        go mod tidy
PS D:\workspace\GO\tuckersGo\goWeb\web05> go mod tidy
PS D:\workspace\GO\tuckersGo\goWeb\web05> code .

 

//go.mod
module GO/tuckersGo/goWeb/web05

go 1.17

 

app_test.go

 

package myapp

import "testing"

func TestIndex(t *testing.T) {

}

 

// goconvey 실행하고 TDD 방식으로 개발 진행
PS D:\workspace\GO\tuckersGo\goWeb\web05> goconvey
2021/11/03 17:25:13 goconvey.go:116: GoConvey server: 
2021/11/03 17:25:13 goconvey.go:121:   version: v1.7.2
2021/11/03 17:25:13 goconvey.go:122:   host: 127.0.0.1
2021/11/03 17:25:13 goconvey.go:123:   port: 8080
2021/11/03 17:25:13 goconvey.go:124:   poll: 250ms
2021/11/03 17:25:13 goconvey.go:125:   cover: true
2021/11/03 17:25:13 goconvey.go:126:
2021/11/03 17:25:13 tester.go:19: Now configured to test 10 packages concurrently.
2021/11/03 17:25:13 goconvey.go:243: Serving HTTP at: http://127.0.0.1:8080
2021/11/03 17:25:13 goconvey.go:146: Launching browser on 127.0.0.1:8080
2021/11/03 17:25:13 integration.go:122: File system state modified, publishing current folders... 0 4907867274
2021/11/03 17:25:13 goconvey.go:159: Received request from watcher to execute tests...
2021/11/03 17:25:13 goconvey.go:152: exec: "start": executable file not found in %PATH%
2021/11/03 17:25:13 goconvey.go:154:
2021/11/03 17:25:14 executor.go:69: Executor status: 'executing'
2021/11/03 17:25:14 coordinator.go:46: Executing concurrent tests: GO/tuckersGo/goWeb/web05
2021/11/03 17:25:14 coordinator.go:46: Executing concurrent tests: GO/tuckersGo/goWeb/web05/myapp
2021/11/03 17:25:14 shell.go:89: Coverage output: ?     GO/tuckersGo/goWeb/web05        [no test files]
2021/11/03 17:25:14 shell.go:91: Run without coverage
2021/11/03 17:25:15 parser.go:24: [no test files]: GO/tuckersGo/goWeb/web05
2021/11/03 17:25:15 parser.go:24: [passed]: GO/tuckersGo/goWeb/web05/myapp
2021/11/03 17:25:15 executor.go:69: Executor status: 'idle'

 

여기까지 기본 스타일입니다. 이제 코딩을 시작합니다.


중간에 assert 패키지 임포트 오류가 발생하여 다시 go get 처리함.

D:\workspace\GO\tuckersGo\goWeb\web05>go get github.com/stretchr/testify/assert
go get: added github.com/davecgh/go-spew v1.1.0
go get: added github.com/pmezard/go-difflib v1.0.0
go get: added github.com/stretchr/testify v1.7.0
go get: added gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c

 

go.mod & go.sum - 패키지를 추가하면 변경되는 사항들, 참고용입니다.

 

//go.mod
module GO/tuckersGo/goWeb/web05

go 1.17

require (
	github.com/davecgh/go-spew v1.1.0 // indirect
	github.com/gorilla/mux v1.8.0 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	github.com/stretchr/testify v1.7.0 // indirect
	gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)


//go.sum
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

 

./main.go

 

package main

import (
	"GO/tuckersGo/goWeb/web05/myapp"
	"net/http"
)

const portNumber = ":3000"

func main() {
	mux := myapp.NewHttpHandler()
	http.ListenAndServe(portNumber, mux)
}

 

./myapp/app.go

 

// 핸들러를 등록 관리하는 파일
package myapp

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strconv"
	"time"

	"github.com/gorilla/mux"
)

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

var userMap map[int]*User
var lastId int

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

func usersHandler(w http.ResponseWriter, r *http.Request) {
	if len(userMap) == 0 {
		w.WriteHeader(http.StatusOK)
		fmt.Fprint(w, "No Users")
		return
	}
	users := []*User{}
	for _, u := range userMap {
		users = append(users, u)
	}
	data, _ := json.Marshal(users)
	w.Header().Add("content-type", "application/json")
	w.WriteHeader(http.StatusOK)
	fmt.Fprint(w, string(data))
}

func createUserHandler(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, err)
		return
	}

	// created User
	lastId += 1
	user.Id = lastId
	user.CreatedAt = time.Now()
	userMap[user.Id] = user
	log.Println("createUserHandler:", *userMap[user.Id])

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

func getUserInfoHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id, err := strconv.Atoi(vars["id"])
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprint(w, err)
		return
	}

	user, ok := userMap[id]
	if !ok {
		w.WriteHeader(http.StatusOK)
		fmt.Fprint(w, "No User ID:", id)
		log.Println(userMap[id])
		return
	}

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

// delete a user
func deleteUserHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id, err := strconv.Atoi(vars["id"])
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprint(w, err)
		return
	}

	_, ok := userMap[id]
	if !ok {
		w.WriteHeader(http.StatusOK)
		fmt.Fprint(w, "No User ID:", id)
		log.Println("user.Id:", id)
		return
	}

	delete(userMap, id)
	w.WriteHeader(http.StatusOK)
	fmt.Fprint(w, "Deleted User ID:", id)

}

func updateUserHandler(w http.ResponseWriter, r *http.Request) {

	updateUser := new(User)
	err := json.NewDecoder(r.Body).Decode(updateUser)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		log.Println("http.StatusBadRequest:", http.StatusBadRequest)
		fmt.Fprint(w, err)
		return
	}
	user, ok := userMap[updateUser.Id]
	if !ok {
		w.WriteHeader(http.StatusOK)
		fmt.Fprint(w, "No User ID:", updateUser.Id)
		return
	}
	if updateUser.FirstName != "" {
		user.FirstName = updateUser.FirstName
	}
	if updateUser.LastName != "" {
		user.LastName = updateUser.LastName
	}
	if updateUser.Email != "" {
		user.Email = updateUser.Email
	}

	userMap[updateUser.Id] = user
	w.Header().Add("content-type", "application/json")
	w.WriteHeader(http.StatusOK)
	data, _ := json.Marshal(user)
	fmt.Fprint(w, string(data))
}

// making a new my handler
func NewHttpHandler() http.Handler {
	userMap = make(map[int]*User)
	lastId = 0
	// 인스턴스를 만들고 해당 인스턴스에 등록해서 사용하는 예제 코드.
	mux := mux.NewRouter() // gorilla mux 사용법
	// mux := http.NewServeMux() //gorilla mux 로 대체
	mux.HandleFunc("/", indexHandler)
	mux.HandleFunc("/users", usersHandler).Methods("GET")
	mux.HandleFunc("/users", createUserHandler).Methods("POST")
	mux.HandleFunc("/users", updateUserHandler).Methods("PUT")
	mux.HandleFunc("/users/{id:[0-9]+}", getUserInfoHandler).Methods("GET")
	mux.HandleFunc("/users/{id:[0-9]+}", deleteUserHandler).Methods("DELETE")

	return mux
}

 

./myapp/app_test.go

 

package myapp

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/http/httptest"
	"strconv"
	"strings"
	"testing"

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

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

	ts := httptest.NewServer((NewHttpHandler()))
	defer ts.Close()

	resp, err := http.Get(ts.URL)
	assert.NoError(err)
	assert.Equal(http.StatusOK, resp.StatusCode)
	data, _ := ioutil.ReadAll(resp.Body)
	assert.Equal("Hello World", string(data))
}

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

	ts := httptest.NewServer((NewHttpHandler()))
	defer ts.Close()

	resp, err := http.Get(ts.URL + "/users")
	assert.NoError(err)
	assert.Equal(http.StatusOK, resp.StatusCode)
	data, _ := ioutil.ReadAll(resp.Body)
	assert.Equal(string(data), "No Users")
}

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

	ts := httptest.NewServer((NewHttpHandler()))
	defer ts.Close()

	resp, err := http.Get(ts.URL + "/users/1")
	assert.NoError(err)
	assert.Equal(http.StatusOK, resp.StatusCode)
	data, _ := ioutil.ReadAll(resp.Body)
	log.Println("TestGetUserInfo:", string(data))
	assert.Contains(string(data), "No User ID:1")
}

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

	ts := httptest.NewServer((NewHttpHandler()))
	defer ts.Close()

	resp, err := http.Post(ts.URL+"/users", "application/json",
		strings.NewReader(`{"first_name":"bs", "last_name":"kim", "email":"kimbs@kimbs.com"}`))
	assert.NoError(err)
	assert.Equal(http.StatusCreated, resp.StatusCode)

	user := new(User)
	err = json.NewDecoder(resp.Body).Decode(user)
	assert.NoError(err)
	assert.NotEqual(0, user.Id)

	id := user.Id
	log.Println("TestCreateUser user.Id:", id)
	resp, err = http.Get(ts.URL + "/users/" + strconv.Itoa(id))
	assert.NoError(err)
	assert.Equal(http.StatusOK, resp.StatusCode)
	user2 := new(User)
	err = json.NewDecoder(resp.Body).Decode(user2)
	assert.NoError(err)
	assert.Equal(user.Id, user2.Id)
	assert.Equal(user.FirstName, user2.FirstName)

}

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

	ts := httptest.NewServer((NewHttpHandler()))
	defer ts.Close()

	resp, err := http.Post(ts.URL+"/users", "application/json",
		strings.NewReader(`{"first_name":"bs", "last_name":"kim", "email":"kimbs@kimbs.com"}`))
	assert.NoError(err)
	assert.Equal(http.StatusCreated, resp.StatusCode)

	req, _ := http.NewRequest("DELETE", ts.URL+"/users/1", nil)
	resp, err = http.DefaultClient.Do(req)
	assert.NoError(err)
	assert.Equal(http.StatusOK, resp.StatusCode)

	data, err := ioutil.ReadAll(resp.Body)
	log.Println("TestDeleteUser:", string(data))
	assert.NoError(err)
	assert.Equal("Deleted User ID:1", string(data))

}

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

	ts := httptest.NewServer((NewHttpHandler()))
	defer ts.Close()

	// update wrong user
	req, _ := http.NewRequest("PUT", ts.URL+"/users",
		strings.NewReader(`{"id":1, "first_name":"updated", "last_name":"updated", "email":"updated@kimbs.com"}`))
	resp, err := http.DefaultClient.Do(req)
	assert.NoError(err)
	assert.Equal(http.StatusOK, resp.StatusCode)

	// no user updated
	data, _ := ioutil.ReadAll(resp.Body)
	assert.Contains(string(data), "No User ID:1")
	log.Printf("TestUpdateUser:[%s]", string(data))

	// create test user info
	resp, err = http.Post(ts.URL+"/users", "application/json",
		strings.NewReader(`{"first_name":"bs", "last_name":"kim", "email":"kimbs@kimbs.com"}`))
	assert.NoError(err)
	assert.Equal(http.StatusCreated, resp.StatusCode)

	user := new(User)
	err = json.NewDecoder(resp.Body).Decode(user)
	assert.NoError(err)
	assert.NotEqual(0, user.Id)

	// update the test user info from create
	newUser := new(User)
	newUser.Id = user.Id
	newUser.FirstName = "updated"
	newUser.LastName = "up"
	newUser.Email = "up@up.com"

	updateStr := fmt.Sprintf(`{"id":%d, "first_name":"%s", "last_name":"%s", "email":"%s"}`,
		newUser.Id, newUser.FirstName, newUser.LastName, newUser.Email)

	req, _ = http.NewRequest("PUT", ts.URL+"/users", strings.NewReader(updateStr))
	resp, err = http.DefaultClient.Do(req)
	assert.NoError(err)
	assert.Equal(http.StatusOK, resp.StatusCode)

	// checking update result from updateUserHandler
	updateUser := new(User)
	err = json.NewDecoder(resp.Body).Decode(updateUser)
	assert.NoError(err)
	log.Printf("TestUpdateUser Id[%d] data:[%v]", updateUser.Id, *updateUser)

}

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

	ts := httptest.NewServer((NewHttpHandler()))
	defer ts.Close()

	// create test user info
	resp, err := http.Post(ts.URL+"/users", "application/json",
		strings.NewReader(`{"first_name":"bs", "last_name":"kim", "email":"kimbs@kimbs.com"}`))
	assert.NoError(err)
	assert.Equal(http.StatusCreated, resp.StatusCode)

	// create test user info
	resp, err = http.Post(ts.URL+"/users", "application/json",
		strings.NewReader(`{"first_name":"jason", "last_name":"park", "email":"jason@kimbs.com"}`))
	assert.NoError(err)
	assert.Equal(http.StatusCreated, resp.StatusCode)

	resp, err = http.Get(ts.URL + "/users")
	assert.NoError(err)
	assert.Equal(http.StatusOK, resp.StatusCode)

	users := []*User{}
	err = json.NewDecoder(resp.Body).Decode(&users)
	assert.NoError(err)
	assert.Equal(2, len(users))
	for _, d := range users {
		log.Print("TestUsers_withUsersData:", *d)
	}
}

 

test result

 

log.Println 함수를 이용해서 서버쪽 로그 및 테스트쪽 로그를 출력

 

  • 주의사항)
// 정상동작함, json 형식으로 보여짐
w.Header().Add("content-type", "application/json")
w.WriteHeader(http.StatusCreated)


// 정상동작 안함, 문자열처럼 일렬로 보여짐
w.WriteHeader(http.StatusCreated)
w.Header().Add("content-type", "application/json")

 

 

참고자료 [https://www.youtube.com/watch?v=0sp6KhLgNxg&list=PLy-g2fnSzUTDALoERcKDniql16SAaQYHF&index=5]