← レッスン一覧に戻る

解答 03.2 — HTTP解剖(RequestStatus)¶

このノートブックは 03.2-http-anatomy.ipynb の練習問題の解答です。 まず問題の前提コードを再掲し、次に解答、最後にチェックを実行します。

先に自分の力で解いてから、答え合わせに使ってください。

前提コード(問題ノートブックと同じ定義)¶

In [1]:
import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"strconv"
	"strings"
)

// ErrUnanswered は、練習問題が未回答のときにプレースホルダ関数が返す特別なエラー。
var ErrUnanswered = errors.New("未回答: この関数はまだ実装されていません")

// newTestServer は、/users(一覧取得・新規作成)と /users/{id}(単体取得)を持つ
// テスト用サーバーを起動する。
func newTestServer() *httptest.Server {
	mux := http.NewServeMux()

	users := map[int]string{1: "tanaka", 2: "suzuki"}
	nextID := 3

	mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
		switch r.Method {
		case http.MethodGet:
			var b strings.Builder
			b.WriteString("[")
			first := true
			for id := 1; id < nextID; id++ {
				name, ok := users[id]
				if !ok {
					continue
				}
				if !first {
					b.WriteString(",")
				}
				fmt.Fprintf(&b, `{"id":%d,"name":"%s"}`, id, name)
				first = false
			}
			b.WriteString("]")
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			w.Write([]byte(b.String()))
		case http.MethodPost:
			var payload struct {
				Name string `json:"name"`
			}
			if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
				w.WriteHeader(http.StatusBadRequest)
				w.Write([]byte(`{"error":"invalid json"}`))
				return
			}
			id := nextID
			users[id] = payload.Name
			nextID++
			w.Header().Set("Content-Type", "application/json")
			w.Header().Set("Location", fmt.Sprintf("/users/%d", id))
			w.WriteHeader(http.StatusCreated)
			fmt.Fprintf(w, `{"id":%d,"name":"%s"}`, id, payload.Name)
		default:
			w.Header().Set("Allow", "GET, POST")
			w.WriteHeader(http.StatusMethodNotAllowed)
			w.Write([]byte(`{"error":"method not allowed"}`))
		}
	})

	mux.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet {
			w.Header().Set("Allow", "GET")
			w.WriteHeader(http.StatusMethodNotAllowed)
			w.Write([]byte(`{"error":"method not allowed"}`))
			return
		}
		idStr := strings.TrimPrefix(r.URL.Path, "/users/")
		id, err := strconv.Atoi(idStr)
		if err != nil {
			w.WriteHeader(http.StatusBadRequest)
			w.Write([]byte(`{"error":"invalid id"}`))
			return
		}
		name, ok := users[id]
		if !ok {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusNotFound)
			w.Write([]byte(`{"error":"not found"}`))
			return
		}
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		fmt.Fprintf(w, `{"id":%d,"name":"%s"}`, id, name)
	})

	return httptest.NewServer(mux)
}

解答: RequestStatus¶

http.NewRequest でリクエストを組み立て、srv.Client().Do で送るだけです。

In [2]:
func RequestStatus(srv *httptest.Server, method, path string, body io.Reader) (int, error) {
	req, err := http.NewRequest(method, srv.URL+path, body)
	if err != nil {
		return 0, err
	}
	resp, err := srv.Client().Do(req)
	if err != nil {
		return 0, err
	}
	defer resp.Body.Close()
	return resp.StatusCode, nil
}

チェック(問題ノートブックと同じ期待値)¶

答え合わせ用のヘルパー mustEqual を定義します。

In [3]:
import "reflect"

func mustEqual(got, want any, name string) {
	if reflect.DeepEqual(got, want) {
		fmt.Printf("✅ Passed: %s\n", name)
		return
	}
	panic(fmt.Sprintf("❌ %s\n  got  = %v (%T)\n  want = %v (%T)", name, got, got, want, want))
}
In [4]:
%%
srv := newTestServer()
defer srv.Close()

status1, err1 := RequestStatus(srv, http.MethodGet, "/users", nil)
if err1 != nil {
	panic(err1)
}
mustEqual(status1, http.StatusOK, "GET /users は200")

status2, err2 := RequestStatus(srv, http.MethodGet, "/users/999", nil)
if err2 != nil {
	panic(err2)
}
mustEqual(status2, http.StatusNotFound, "GET /users/999 は404")

status3, err3 := RequestStatus(srv, http.MethodPost, "/users", strings.NewReader(`{"name":"kimura"}`))
if err3 != nil {
	panic(err3)
}
mustEqual(status3, http.StatusCreated, "POST /users は201")

status4, err4 := RequestStatus(srv, http.MethodDelete, "/users", nil)
if err4 != nil {
	panic(err4)
}
mustEqual(status4, http.StatusMethodNotAllowed, "DELETE /users は405")

fmt.Println("🎉 すべてのチェックが通りました")
✅ Passed: GET /users は200
✅ Passed: GET /users/999 は404
✅ Passed: POST /users は201
✅ Passed: DELETE /users は405
🎉 すべてのチェックが通りました

解説¶

  1. http.NewRequest + Client.Do: http.Get/http.Post と違い、メソッドを自由に指定できる 汎用的な組み立て方です。ヘッダーを付けたい場合も req.Header.Set(...) が使えます
  2. ステータスコードだけを見れば十分な場面: ボディの中身を検証したいわけではなく「結果の種類」 だけを確認したい場合、resp.StatusCode だけを返す軽量な関数にする設計は実務でもよく使います