Skip to content

Instantly share code, notes, and snippets.

@ohtsuchi
Last active February 20, 2020 01:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ohtsuchi/2ed3033031d28a46a992517ab08961bd to your computer and use it in GitHub Desktop.
Save ohtsuchi/2ed3033031d28a46a992517ab08961bd to your computer and use it in GitHub Desktop.
Go言語でTDDする勉強会 第6回(2020/02/05) 発表メモ (番外編). 「Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作る」 第8章(8.3 8.4), 第3章(3.3.4) 解説

Go言語でTDDする勉強会 第6回(2020/02/05) 発表メモ (番外編). 「Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作る」 第8章(8.3 8.4), 第3章(3.3.4) 解説

第6回(2020/02/05) 発表メモ の続き

  • [追記] 第6回(2020/02/05) 発表後に内容を一部追加したので、 第7回(2020/02/19) 発表時に補足説明 します
    • 8.3 と 8.4 の フロー図
    • 3.3.4 ハンドラ関数のチェイン の 解説 と、フレームワーク echo の 該当コード

内容

公式サイト

正誤表

 


8.3 GoによるHTTPのテスト

8.3 前準備(1) github から ソースコード を download

https://github.com/mushahiroyuki/gowebprog

本をお読みでない方もダウンロードしてお試しいただけます。 次のいずれかの方法でダウンロードしてください。

ターミナルで $GOPATH/src など適当なディレクトリ(フォルダ)に移動してから、下記のコマンドを実行してください(gitが必要です)。
git clone https://github.com/mushahiroyuki/gowebprog.git

あるいは、ページ右上の[Clone or download]をクリック(タップ)して表示される[Download ZIP]でダウンロードしてください。

例1) ghq インストール 済みの場合

ghq get mushahiroyuki/gowebprog

または

例2) ghq 未インストール で $GOPATH/src 以下に展開する場合

mkdir -p $GOPATH/src/github.com/mushahiroyuki
git clone https://github.com/mushahiroyuki/gowebprog.git $GOPATH/src/github.com/mushahiroyuki/gowebprog

8.3 前準備(2) github.com/lib/pqgo get

go get github.com/lib/pq

補足: github.com/lib/pqdata.go で import しているライブラリ

import (
	"database/sql"
	_ "github.com/lib/pq"
)

8.3 前準備(3)

以下は local で docker-compose が動作する環境の方のみ 実行して下さい

docker image pull postgres:12.1-alpine

 


8.3 動作確認(1-1) ch08/11httptest_2 ディレクトリに移動

cd $GOPATH/src/github.com/mushahiroyuki/gowebprog
cd ch08/11httptest_2

ch08/11httptest_2 ディレクトリ 以下の 4ファイル を確認

% ls
data.go  server.go  server_test.go  setup.sql

8.3 動作確認(1-2) go test 実行

go test

結果が以下のように test 失敗 になれば OK

--- FAIL: TestHandleGet (0.00s)
    server_test.go:36: Response code is 500
    server_test.go:41: Cannot retrieve JSON post
--- FAIL: TestHandlePut (0.00s)
    server_test.go:51: Response code is 500
FAIL
exit status 1
FAIL	github.com/mushahiroyuki/gowebprog/ch08/11httptest_2	0.017s

 


以下は local で docker-compose が動作する環境の方のみ 試してみて下さい

8.3 動作確認(2-1) ch08/11httptest_2 ディレクトリ直下に docker-compose.yml 新規作成

version: "3"
services:
  postgres:
    image: postgres:12.1-alpine
    container_name: gowebprog_postgres
    environment:
      POSTGRES_USER: gwp
      POSTGRES_PASSWORD: gwp
      POSTGRES_DB: gwp
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=C"
    ports:
      - 5432:5432
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./setup.sql:/docker-entrypoint-initdb.d/setup.sql

volumes:
  postgres-data:

補足1: gwp という値

var Db *sql.DB

func init() {
	var err error
	Db, err = sql.Open("postgres", "user=gwp dbname=gwp password=gwp sslmode=disable")
	// 中略
}

補足2: /docker-entrypoint-initdb.d

  • MySQL や PostgreSQL の公式コンテナでは このディレクトリ直下に存在する *.sql or *.sql.gz or *.sh が実行される

-> https://hub.docker.com/_/postgres

If you would like to do additional initialization ...中略,
add one or more *.sql, *.sql.gz, or *.sh scripts under /docker-entrypoint-initdb.d (...中略).

After the entrypoint calls initdb to create the default postgres user and database,
it will run any *.sql files, run any executable *.sh scripts, and source any non-executable *.sh scripts found in that directory to do further initialization before starting the service.

Warning: scripts in /docker-entrypoint-initdb.d are only run if you start the container with a data directory that is empty;

追加の初期化を行いたい場合は
/docker-entrypoint-initdb.d の直下に 1つ以上の *.sql, *.sql.gz, *.sh script を追加 します

entrypoint が initdb を呼び出して デフォルト の postgres ユーザーとデータベースを作成した後,
service を開始する前に さらに初期化を行うため, その ディレクトリ で見つかった *.sql ファイルを実行し, 実行可能な *.sh script を実行し, 実行不可能な *.sh script を source コマンドで実行 します

警告: /docker-entrypoint-initdb.dの script は, 空の data ディレクトリ で container を起動した場合にのみ, 実行されます

8.3 動作確認(2-2) setup.sql 修正

setup.sql を 微修正

- drop table posts;
+ drop table IF EXISTS posts; -- IF EXISTS 追加

create table posts (
  id      serial primary key,
  content text,
  author  varchar(255)
);

+INSERT INTO posts VALUES (1, 'contentA', 'authorA'); -- 1行 追加

8.3 動作確認(2-3) docker-compose up -d 実行

docker-compose up -d
docker-compose ps
docker container ls

8.3 動作確認(2-4) go test 実行

go test

結果が以下のように test 成功 になれば OK

PASS
ok  	github.com/mushahiroyuki/gowebprog/ch08/11httptest_2	0.032s

 

8.3 動作確認(2-X) の 後始末

docker-compose down --volumes

再度 go test を実行すると 8.3 動作確認(1-2) と同様に test 失敗 になれば OK

 

8.3 GoによるHTTPのテスト 結論

  • local で DB (postgres) が動作していないと test が失敗 する

 


8.4 テストダブルと依存性の注入

8.4 動作確認(1-1) ch08/18dependency_injection ディレクトリに移動

cd $GOPATH/src/github.com/mushahiroyuki/gowebprog
cd ch08/18dependency_injection

ch08/18dependency_injection ディレクトリ 以下の 4ファイル を確認

% ls
data.go  doubles.go  server.go  server_test.go

8.4 動作確認(1-2) go test 実行

go test
  • コンパイルエラーになる
# github.com/mushahiroyuki/gowebprog/ch08/18dependency_injection
./server_test.go:40:3: Error call has possible formatting directive %v
FAIL	github.com/mushahiroyuki/gowebprog/ch08/18dependency_injection [build failed]
-		t.Error("Response code is %v", writer.Code)
+		t.Errorf("Response code is %v", writer.Code)

再度 go test 実行

go test

結果が以下のように test 成功 になれば OK

PASS
ok  	github.com/mushahiroyuki/gowebprog/ch08/18dependency_injection	0.013s

8.4 テストダブルと依存性の注入 結論

  • local で DB (postgres) が動作していなくても test が成功 する

 


前提知識: http.HandlerFunc

HandlerFuncHandler の コード例: Go Playground

前提知識: init() 関数

参考サイト: init関数のふしぎ #golang - Qiita 参照

以下のようにmainパッケージに書くとmain関数より先に実行されます。
mainパッケージでない場合は、importするだけで呼び出されます。

ちなみに、ファイルに1つという決まりもなく、複数書けるようになっています。

前提知識: TestMain

参考サイト: Golangでtestingことはじめ(2)〜テストにおける共通処理〜 - DeNA Testing Blog 参照

テストファイル内に TestMain が存在している場合、 go test はTestMainのみ実行します。
testing.M の Run を呼ぶことで各テストケースが実行され

 


8.38.4 の違い

8.3 GoによるHTTPのテスト の 構成

server_test.go

func TestMain(m *testing.M) {
	setUp()
	code := m.Run()
	// 中略
}

func setUp() {
	mux = http.NewServeMux()
	mux.HandleFunc("/post/", handleRequest)
	// 中略
}

server.go

type Post struct {
	Id      int    `json:"id"`
	// 中略
}

func main() {
	// 中略
	http.HandleFunc("/post/", handleRequest)
	server.ListenAndServe()
}

// main handler function
func handleRequest(w http.ResponseWriter, r *http.Request) {
	var err error
	switch r.Method {
	case "GET":
		err = handleGet(w, r)
	// 中略
	}
	// 中略
}

func handleGet(w http.ResponseWriter, r *http.Request) (err error) {
	// 中略
	post, err := retrieve(id)
	// 中略
}

data.go

var Db *sql.DB

func init() {
	var err error
	Db, err = sql.Open("postgres", "user=gwp dbname=gwp password=gwp sslmode=disable")
	// 中略
}

func retrieve(id int) (post Post, err error) {
	post = Post{}
	err = Db.QueryRow("select id, content, author from posts where id = $1", id).Scan(&post.Id, /* 中略 */)
	return
}

8.3 フロー図 (書籍 p266 図8.3)

main or test から 呼び出し
       |
       v
+---------------+
| handleRequest |
+---------------+
       |
       v
+---------------+
| handleGet     |
+---------------+
       |
       v
+---------------+
| retrieve      | ---> var Db *sql.DB ---> PostgreSQL
+---------------+

課題: sql.DB への依存関係分離したい

 ↓↓↓

8.4 テストダブルと依存性の注入 の 構成

  • 方針
    • sql.DB への直接の依存避ける
    • sql.DB を 別の struct でラップ
      • test では sql.DB を直接置き換えずに 外側の struct の テストダブル を使用
    • どの関数 (handleRequest, handleGet) も sql.DB を直接呼び出さない
  • main 実装
    • sql.DBPost の一部として 注入
  • test 実装
    • テストダブル (FakePost) を使用

8.4 フロー図 (書籍 p267 図8.4)

main or test から 呼び出し
       |
       |  +----------------+  +----------------+
       |  | Post           |  | FakePost       |
       |  | +------------+ |  +----------------+
       |  | | sql.DB     | |
       |  | +------------+ |
       |  +----------------+ ( main から sql.DB が Post の一部として 注入される )
       v
+---------------+
| handleRequest |
+---------------+
       |
       |  +----------------+  +----------------+
       |  | Post           |  | FakePost       |
       |  | +------------+ |  +----------------+
       |  | | sql.DB     | |
       |  | +------------+ |
       |  +----------------+
       |
       |                                 +----------------+
       |                                 | Post           |
       v                                 | +------------+ |
+---------------+ invoke `fetch` method  | | sql.DB     | --------> PostgreSQL
|               | ---------------------> | +------------+ |
|               | (main)                 +----------------+
| handleGet     |
|               | invoke `fetch` method  +----------------+
|               | ---------------------> | FakePost       |
+---------------+ (test)                 +----------------+

書籍の図は一部間違い有り(削除済みの retrieve 関数が載っている)

data.go

type Text interface {
  fetch(id int) (err error)
  // 中略
}
  • main 用の実装 (Post) と テストダブル (FakePost) が両方満たす interface を定義

data.go

type Post struct {
	Db      *sql.DB             // ここがポイント
	Id      int    `json:"id"`
	// 中略
}

func (post *Post) fetch(id int) (err error) {
	err = post.Db.QueryRow("select id, content, author from posts where id = $1", id).Scan(&post.Id, /* 中略 */)
	return
}
  • sql.DB の ポインタ型 を Post の一部として 定義

doubles.go

type FakePost struct {
	Id      int
	// 中略
}

func (post *FakePost) fetch(id int) (err error) {
	post.Id = id
	return
}
  • テストダブル の方には sql.DB は含まれない

server_test.go

func TestGetPost(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("/post/", handleRequest(&FakePost{}))     // テストダブル を DI
	// 中略
}

server.go

func main() {
	// connect to the Db
	var err error
	db, err := sql.Open("postgres", "user=gwp dbname=gwp password=gwp sslmode=disable")

	// 中略
	http.HandleFunc("/post/", handleRequest(&Post{Db: db}))  // sql.DB を Post の一部として DI
	server.ListenAndServe()
}

// main handler function
func handleRequest(t Text) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var err error
		switch r.Method {
		case "GET":
			err = handleGet(w, r, t)
		// 中略
		}
		// 中略
	}
}

func handleGet(w http.ResponseWriter, r *http.Request, post Text) (err error) {
	// 中略
	err = post.fetch(id)
	// 中略
}
  • handleRequest引数 の シグニチャー を変更
    • func(ResponseWriter, *Request) -> func(Text)
      • func(Text)HandlerFunc 型 を満たさない
  • しかし handleRequest戻り値 が HandlerFunc
    • func handleRequest(t Text) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ... } }
  • よって 以下の 呼び出し の 結果 は HandlerFunc 型 となる
    • (test code) handleRequest(&FakePost{})
    • (main 関数) handleRequest(&Post{Db: db})

 


3.3.4 ハンドラとハンドラ関数のチェイン

3.3.4 ch03/10chain_handlerfunc/server.go

ch03/10chain_handlerfunc/server.go

func log(h http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// 中略
		h(w, r)
	}
}
http.HandleFunc("/hello", log(hello))

 

-> ch03/10chain_handlerfunc/server.go の疑似コード例 (一部改修): Go Playground

var b http.Handler = log3(log2("hoge", log(hello)))

入れ子が深くなる. その対応法が以下のサイトに ↓

 

Middleware (Advanced) - Go Web Examples

Middleware (Advanced) - Go Web Examples

type Middleware func(http.HandlerFunc) http.HandlerFunc

func Logging() Middleware {
	return func(f http.HandlerFunc) http.HandlerFunc {
		return func(w http.ResponseWriter, r *http.Request) {
			// 中略
			f(w, r)
		}
	}
}

func Method(m string) Middleware {
	// 中略
}

func Chain(f http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc {
	for _, m := range middlewares {
		f = m(f)
	}
	return f
}

func Hello(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "hello world")
}

func main() {
	http.HandleFunc("/", Chain(Hello, Method("GET"), Logging()))
	http.ListenAndServe(":8080", nil)
}

Chain 関数 の 第2引数 (...Middleware) は 『「関数型」の「可変長引数」』
第6回(2020/02/05) 発表メモ で解説した Functional Option Pattern 参照

 

-> ch03/10chain_handlerfunc/server.go の疑似コード例 (Chain 利用): Go Playground

var b http.Handler = Chain(hello, Log(), Log2("hoge"), Log3())

ch03/10chain_handlerfunc/server.go の疑似コード例」 の 入れ子

var b http.Handler = log3(log2("hoge", log(hello)))

と比べてみると、 Chain 関数を利用した例は 可変長引数 でスッキリしている

 

フレームワーク echo の場合

ローカル環境の動作手順の例:

mkdir hello # Alternatively, clone it if it already exists in version control.
cd hello
go mod init github.com/user/hello
go get github.com/labstack/echo/v4
  • 任意のファイル (例:main.go) を作成 して echo の github の Example の内容をコピー
cat << EOF > main.go
package main

import (
  "net/http"
  "github.com/labstack/echo/v4"
  "github.com/labstack/echo/v4/middleware"
)

func main() {
  // Echo instance
  e := echo.New()

  // Middleware
  e.Use(middleware.Logger())
  e.Use(middleware.Recover())

  // Routes
  e.GET("/", hello)

  // Start server
  e.Logger.Fatal(e.Start(":1323"))
}

// Handler
func hello(c echo.Context) error {
  return c.String(http.StatusOK, "Hello, World!")
}
EOF
go run main.go
  • アクセスする度 に ターミナル に log 出力 されている事を確認
{"time":"2020-02-16T05:47:09.244109+09:00","id":"","remote_ip":"::1","host":"localhost:1323","method":"GET","uri":"/","user_agent":"(中略)","status":200,"error":"","latency":6936,"latency_human":"6.936µs","bytes_in":0,"bytes_out":13}

echo の Middleware

  // Middleware
  e.Use(middleware.Logger())
  e.Use(middleware.Recover())
  • Ctrl + C で停止
  • e.Use(middleware.Logger()) を コメントアウト してから 再度 実行
  • アクセスする度 に ターミナル に log 出力 されない 事を確認

 

echo.MiddlewareFunc

echo.MiddlewareFunc の godoc

type MiddlewareFunc func(HandlerFunc) HandlerFunc

この HandlerFunchttp.HandlerFunc ではなく echo.HandlerFunc (= func(echo.Context) error)

middleware.Logger(), middleware.Recover()

middleware.Logger() の godoc, middleware.Recover() の godoc

func Logger() echo.MiddlewareFunc
func Recover() echo.MiddlewareFunc

Echo.Use

Echo.Use の godoc

func (e *Echo) Use(middleware ...MiddlewareFunc)

echo.go ソースコード

Use メソッドの実装

// Use adds middleware to the chain which is run after router.
func (e *Echo) Use(middleware ...MiddlewareFunc) {
	e.middleware = append(e.middleware, middleware...)
}

e.middleware の定義部分

type (
	// Echo is the top-level framework instance.
	Echo struct {
		// -- 中略 --
		middleware       []MiddlewareFunc
		// -- 中略 --
	}
	// -- 中略 --
)

ServeHTTP, applyMiddleware の実装

// ServeHTTP implements `http.Handler` interface, which serves HTTP requests.
func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// -- 中略 --
	h := NotFoundHandler

	if e.premiddleware == nil {
		e.findRouter(r.Host).Find(r.Method, getPath(r), c)
		h = c.Handler()
		h = applyMiddleware(h, e.middleware...)
	} else {
		h = func(c Context) error {
			e.findRouter(r.Host).Find(r.Method, getPath(r), c)
			h := c.Handler()
			h = applyMiddleware(h, e.middleware...)
			return h(c)
		}
		h = applyMiddleware(h, e.premiddleware...)
	}
	
	// Execute chain
	if err := h(c); err != nil {
		e.HTTPErrorHandler(err, c)
	}
	// -- 中略 --
}

// -- 中略 --

func applyMiddleware(h HandlerFunc, middleware ...MiddlewareFunc) HandlerFunc {
	for i := len(middleware) - 1; i >= 0; i-- {
		h = middleware[i](h)
	}
	return h
}

echo の Middleware 参考サイト

 


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment