WEB5 폴더에 작성했다.
chrome://apps
에 들어가서 ARC를 키고POST
로 바디를 보내준다.(Id와 name들 기억)
GET
으로 users/5를 받아보면 id가 5인 정보들을 알 수 있다.
이제 DELETE하는 작업을 해보자
먼저 테스트 코드를 만들고
app_test.go
...
func TestDeleteUser(t *testing.T) {
assert := assert.New(t)
ts := httptest.NewServer(NewHandler())
defer ts.Close()
// Get과 Post와 다르게 기본적으로 delete는 제공해주지 않는다.
req, _ := http.NewRequest("DELETE", ts.URL+"/users/1", nil) // Id는 1로 임의 설정
resp, err := http.DefaultClient.Do(req)
assert.NoError(err)
assert.Equal(http.StatusOK, resp.StatusCode)
data, _ := ioutil.ReadAll(resp.Body)
assert.Contains(string(data), "No User Id:1") // 아무 것도 없는 상태이므로 없다고 출력되야 정상이다.
}
...
다음으로 Delete 핸들러를 만들 것이다.
func deleteUserHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) // Gorilla mux에서 제공하는 기능, 알아서 id부분을 추출해줌
id, err := strconv.Atoi(vars["id"]) // int로 변환
if err != nil { // 잘못 보냈을 때
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, err)
return
}
// 맵에 없는 Id를 지우려고 할 때
_, ok := userMap[id]
if !ok { // 해당 Id가 없으면
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "No User Id:", id)
return
}
// 있으면 해당 id를 usermap에서 삭제
delete(userMap, id)
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "Deleted User ID:", id)
}
테스트 코드를 실행해보면 아무것도 없는 상태에서 delete를 하므로 없다고 출력되는 것이 확인된다.
따라서 테스트 코드를 수정해서 정보가 있을 때와 없을 때를 비교해보겠다.
app_test.go
...
func TestDeleteUser(t *testing.T) {
...
// 정보를 받아서 다시 delete 수행해보기
resp, err = http.Post(ts.URL+"/users", "application/json",
strings.NewReader(`{"first_name":"tucker", "last_name":"kim", "email":"soosungp33@gmail.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)
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, _ = ioutil.ReadAll(resp.Body)
assert.Contains(string(data), "Deleted User ID:1") // 정보가 있는 상태이므로 잘 지워졌다고 출력된다.
}
정보가 없는 상태에서는 No User Id:1이 들어오고, 있는 상태에서는 Deleted User ID:1이 들어오므로 전부 PASS되는 것을 볼 수 있다.
먼저 테스트 코드와 핸들러를 만들어보자
app_test.go : 테스트 코드
func TestUpdateUser(t *testing.T) {
assert := assert.New(t)
ts := httptest.NewServer(NewHandler())
defer ts.Close()
// body를 업데이트 해야함
// PUT으로 받아온다.
req, _ := http.NewRequest("PUT", ts.URL+"/users",
strings.NewReader(`{"id":1, "first_name":"updated", "last_name":"updated", "email":"updated@nvaer.com"}`))
resp, err := http.DefaultClient.Do(req)
assert.NoError(err)
assert.Equal(http.StatusOK, resp.StatusCode)
}
app.go : 핸들러
...
func updateUserHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
...
test해보면 pass되는 것을 볼 수 있다.
이제 동작 처리를 만들어주자
1. Update or Create : 없을 경우 만들어준다.
2. Update : 없을 경우 에러를 띄워준다.
우리는 단순하게 2번을 만들어 볼 것 이다.
app.go
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)
fmt.Fprint(w, err)
return
}
_, ok := userMap[updateUser.ID]
if !ok { // 해당하는 유저가 없으면
w.WriteHeader(http.StatusOK) // OK를 주고
fmt.Fprint(w, "No User ID:", updateUser.ID) // 그 ID는 없다고 알려줌
return
}
}
다음으로 실제적으로 create한 다음에 데이터를 update를 해보자
app_test.go
func TestUpdateUser(t *testing.T) {
...
// 실제적으로 create한 다음에 update하는 테스트 코드
// ID를 create 한다.
resp, err = http.Post(ts.URL+"/users", "application/json",
strings.NewReader(`{"first_name":"tucker", "last_name":"kim", "email":"soosungp33@gmail.com"}`))
assert.NoError(err)
assert.Equal(http.StatusCreated, resp.StatusCode)
// create한 ID를 알아낸 다음
user := new(User)
err = json.NewDecoder(resp.Body).Decode(user)
assert.NoError(err)
assert.NotEqual(0, user.ID)
updateStr := fmt.Sprintf(`{"id":%d, "first_name":"updated"}`, user.ID) // 동적으로 ID를 받아서
// updateStr에 저장되어 있는 user의 정보를 변경해준다.
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)
// update된 유저 정보가 실제로 변경된 것인지 확인
updateUser := new(User)
err = json.NewDecoder(resp.Body).Decode(updateUser)
assert.NoError(err)
assert.Equal(updateUser.ID, user.ID) // create된 ID와 update한 ID가 같아야 한다.
assert.Equal("updated", updateUser.FirstName) // create한 후 update된 FirstName이 update로 변경되어 있어야 한다.
assert.Equal(user.LastName, updateUser.LastName) // update 후에도 LastName은 같아야 한다.
assert.Equal(user.Email, updateUser.Email) // update 후에도 Email도 같아야 한다.
}
app.go
func updateUserHandler(w http.ResponseWriter, r *http.Request) {
...
userMap[updateUser.ID] = updateUser
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
data, _ := json.Marshal(updateUser)
fmt.Fprint(w, string(data))
}
이 코드를 가지고 테스트해보면 Fail이 난다.
FirstName은 update
로 잘 변경되어 있지만 LastName과 Email이 기존과 같아야되는데 ""
으로 비워져있다.
그 이유는 app_test.go의 updateStr := fmt.Sprintf(`{"id":%d, "first_name":"updated"}`, user.ID)
이 부분에서 firstname만 셋팅해서 보냈다. json이 파싱을 할 때(정보를 읽을 때) 나머지 정보는 오지 않아서 default 값으로 보내지는데 app.go의 userMap[updateUser.ID] = updateUser
에서 그대로 맵에 덮어 씌워버려서 그 전에 저장되어 있는 값들이 전부 날라간 것이다.
따라서 update하고 싶은 항목만 변경해줘야 한다.
app.go
func updateUserHandler(w http.ResponseWriter, r *http.Request) {
...
user, ok := userMap[updateUser.ID]
if !ok { // 해당하는 유저가 없으면
w.WriteHeader(http.StatusOK) // OK를 주고
fmt.Fprint(w, "No User Id:", updateUser.ID) // 그 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 -> user가 포인트 타입이여서 굳이 덮어씌우지 않아도 위에 if에서 변경된다.
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
data, _ := json.Marshal(user)
fmt.Fprint(w, string(data))
}
테스트해보면 pass가 나는 것을 볼 수 있다.
하지만 이러면 또 문제가 발생한다.
예를 들어 LastName을 ""
로 비워주고 싶은데 위 코드는 ""
이면 변경해주지 않는 코드이기 때문에 비워지지 않는다.
실무에서도 많이 발생하는 문제라고 하는데 해결 방법으로는 struct를 따로 만든다고 한다.
코드를 보면
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"`
}
type UpdateUser struct {
ID int `json:"id"`
UpdatedFirstName bool `json:"updated_first_name`
FirstName string `json:"first_name"`
UpdatedLastName bool `json:"updated_last_name`
LastName string `json:"last_name"`
UpdatedEmail bool `json:"updated_email`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
이런 식으로 bool
을 이용해 update인지 아닌지를 판단한다고 한다.
너무 복잡해지기 때문에 이런식으로 해결한다는 것만 알고 넘어가자!
마지막으로 유저들의 list를 반환해주는 코드를 작성해보자!
userHandler의 리턴을 수정해야한다.
app.go
...
// userHandler를 수정
func usersHandler(w http.ResponseWriter, r *http.Request) {
//fmt.Fprint(w, "Get UserInfo by /users/{id}")
if len(userMap) == 0 { // user가 한 명도 없으면
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "No Users")
return
}
users := []*User{}
for _, u := range userMap {
users = append(users, u) // 맵에 있는 모든 유저 정보를 꺼내 users에 저장
}
data, _ := json.Marshal(users)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, string(data))
}
...
app_test.go
...
func TestUsers(t *testing.T) {
...
assert.Equal(string(data), "No Users")
}
...
// 추가
func TestUsers_WithUsersData(t *testing.T) {
assert := assert.New(t)
ts := httptest.NewServer(NewHandler())
defer ts.Close()
// 유저를 2개 등록한다.
resp, err := http.Post(ts.URL+"/users", "application/json",
strings.NewReader(`{"first_name":"tucker", "last_name":"kim", "email":"soosungp33@gmail.com"}`))
assert.NoError(err)
assert.Equal(http.StatusCreated, resp.StatusCode)
resp, err = http.Post(ts.URL+"/users", "application/json",
strings.NewReader(`{"first_name":"jason", "last_name":"park", "email":"jason@gmail.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) // json으로 디코더가 잘 되는지 확인
assert.NoError(err)
assert.Equal(2, len(users)) // 2개개를 만들었으니까 길이가 2여야 한다.
}
테스트해보면 pass가 된다.
이렇게 해서 RESTful API를 전부 만들어보았다.