『テスト駆動開発』を読んだ
同僚が楽しそうに本書を紹介してくれたので読みました。
3部構成になっていて、第1部は多国通貨、第2部はテスティングフレームワークであるxUnitを題材にTDDのプロセスを体験します。こういう本を読む場合、題材が自分の知識外の知識を必要とするものだと結構読むの大変だったりするんですが、本書の題材はそこまで難解じゃないので、TDDのプロセスに集中することができました。
ひとつ意外だったというか、安心できたのは第1部の最後に出てくる以下の記述。
そもそも多国通貨のコードは書き終わったと言えるのだろうか。答えはノーだ。 (中略)
私は「完了」という言葉を信じていない。完璧さを求めるための手法としてTDDを使うこともできるが、そこはTDDが最も活躍する場所ではない。大きなシステムに関わっている場合、毎日触るような部分は日々鍛えられ、変更も自信も持って行える。システムの周辺部分の、あまり変更しない箇所では、テスト抜けは多いし設計も適当になるが、それでも自信の程度は変わらない。 p250
ここを読んだときに、ぼくの大好きな漫画『BLEACH』に出てくる科学者涅マユリの名言を思い出しました。
完璧であれば、それ以上は無い。
そこに創造の余地は無く、それは知恵も才能も立ち入る
隙がないと言う事だ。
我々科学者にとって、完璧とは絶望だよ。
(中略)
今まで存在した何物よりも素晴しくあれ、
だが、決して完璧であるなかれ。
あと勇気を貰えるのはTDDは「"自分ひとり"でできる"技術"だ」というメッセージですね。
付録CではTDDの歴史を紐解きながら現在の解釈まで書かれていて、本編顔負けの発見に富んだ付録だったと思います。ソフトウェアテストにおけるTDDのカバー範囲を「アジャイルテストの4象限」で説明されていたり、単にテストを先に書く書き方という誤解とか。
『システム運用アンチパターン』を読んだ
◯◯アンチパターン本としては『SQLアンチパターン』が思い浮かぶのですが、「システム運用」って結構広いので何が書かれているかわくわくしながら読みました。
広いだけあって、章ごとに結構毛色が変わっていて且つ基本的に独立してるので、1日2章読むみたいに決めて読んでいきました。章ごとに面白くてあっと言う間に読み終わった章、興味が薄いのでサーっと読み進めた章、活かせそうな考え方が多くてメモを取りながらじっくり読んだ章と様々でした。
3章「盲目状態での運用」では、運用の可視化をするうえでプロダクトを理解していないと価値のないメトリクスが集まったダッシュボードができあがったり、文脈のないログが溢れたりするアンチパターンを知ったり。
7章「空の道具箱」では、自動化の取り組みを行う際の主な領域として待ち時間・実行時間・実行頻度・実行のばらつき、の4つが紹介されていました。自分の業務の中から手動でやっている作業を棚卸しして、この4つの観点で考えてみるのは面白そうです。普段漠然と行っていた手動作業に一定の評価観点が追加され、「今自動化し(て)ない理由」が本当に合理的かどうかの判断を助け、自動化を進められる気がします。また、ケネビンフレームワークなどを活用してタスクの複雑さを分類して理解するといったアプローチが紹介されていて、実際に自動化は進めたいがどう優先度を付けていいか分からないとか、タスクの安全性をどの程度担保したらいいか不安だ、といった際に活用できそうです。
10章「情報のため込み:ブレントだけが知っている」では、「意図せず」情報を溜め込んでいる人が組織によって形成されること、ドキュメントを大切に思ってはいるが大切にしていないこと、など耳が痛い話が続きます。ゲートキーパーは聞けば教えてくれるので、良心があり、その行動を評価しがちですが、「その情報を手に入れるために、あなたのチームが提供できるほかの方法はありますか?」と問いを立ててみると前に進めそうです。
11章「命じられた文化」では文化チーフという概念が登場します。
文化チーフとは、組織の文化的価値観を体現する社員のことです。組織の中での階層にかかわらず、会社の中で影響力のある人物とみなされます。チームやグループの感情面でのリーダーとみなされることもあります。 p272
なんだかすごそうな人に見えますが、この観点から組織を見たり自分を見たりするのは良い内省のきっかけになると思います。12章「多すぎる尺度」では優先順位の文脈から目標設定や目標達成のための話も出てくるため、例えば自分の過去半年を振り返って「チームの目標達成を至上命題とする文化チーフとして振る舞えていただろうか」と考えたりするのは効果的でしょう。自分はマネージャーではないですが、マネージャーでなくともチームに関わる話として読み進めると発見があります。
というわけで、章ごとにテーマがほぼ独立していて全体をまとめるのが難しい本ですが、DevOpsの観点から組織や自分の仕事を見つめ直すきっかけに溢れた本だと思いました。
『LEADER’s KPT』を読んだ
KPTとは何か?どうやるか?についての入門的な内容がまとめられていました。
すでにKPTに慣れ親しんだ自分にとっては目新しい発見こそ少なかったですが、つまらなかったわけでもないです。例えばKeep/Problem/Tryの数や比率を継続的に計測してチームの状態を把握する、みたいな方法は、繰り返し行われることの多いKPTと相性が良く合理的で良いなぁと思いました。
最近読んでいる本と違って、紹介されている理論の解説が薄かったり、参考文献が(ほとんど)出てこないような本なので、よく言えばすごくライトに読めました。
Goのリンターstaticcheckのルールを全部読んだからいくつか紹介
Goのリンターの1つであるstaticcheckのルールを全部読みました。
全部書くとただのコピーになってしまうので、その中からかいつまんでいくつか紹介します。
SA1 – Various misuses of the standard library
SA1004 - Suspiciously small untyped constant in time.Sleep
time.Sleep関数に渡されたtime.Durationはナノ秒単位なので、あまりにも小さい値の場合はバグの原因として注意してくれるようです。他の言語だとこういうAPIの場合ミリ秒とかだったりしますからね。意図的に小さい値を使いたい場合はtime.NanosecondeをかければOK。
time.Sleep(1) // NG // main.go:19:13: sleeping for 1 nanoseconds is probably a bug; be explicit if it isn't (SA1004) time.Sleep(1 * time.Nanosecond) // OK
SA1005 - Invalid first argument to exec.Command
exec.Command関数の第一引数がシェルコマンドだったらその次の引数がプログラム名やパスじゃないと注意してくれる。ほぇ〜。ただ第二引数以降に入ってる場合は注意なし。
exec.Command("ls arg1") // NG // main.go:8:15: first argument to exec.Command looks like a shell command, but a program name or path are expected (SA1005) exec.Command("ls ./") // OK
SA1006 - Printf with dynamic first argument and no further arguments
ユーザー入力などの動的な文字列をフォーマットとしてfmt.Printf関数に渡すのを注意してくれる。この例だと別のルール(SA5009)にも引っ掛かりますね。
s := "Interest rate: 5%" fmt.Printf(s) // NG // main.go:9:2: printf-style function with dynamic format string and no further arguments should use print-style function instead (SA1006) // main.go:9:13: couldn't parse format string (SA5009) fmt.Printf("%s", s) // OK
SA1008 - Non-canonical key in http.Header map
http.Headerのキーは大文字始まりで正規化されていて、http.Header.Addのような用意されている関数経由ならそうなるが、mapに直接追加するなどするとそうならないので注意してくれる...って書いてあるけど手元で試してみたところ引っかからなかった。
追加時ではなく参照するコードに対してチェックしているようだった。
h := http.Header{} h["etag"] = []string{"1234"} // staticcheckでは怒られない h.Add("etag", "5678") // map[Etag:[5678] etag:[1234]] _ = h["etag"] // NG // main.go:12:6: keys in http.Header are canonicalized, "etag" is not canonical; fix the constant or use http.CanonicalHeaderKey (SA1008) _ = http.CanonicalHeaderKey("etag") // OK -> "Etag"
SA1019 - Using a deprecated function, variable, constant or field
deprecatedになったAPIの利用を注意してくれる。すごくありがたいけどどうやって検知してるんだろう?と思ってtestdataコードを読むと各バージョンごとにルールを作ってるみたいでした。
SA1024 - A string cutset contains duplicate characters
strings.TrimLeftとstrings.TrimPrefixの違いが分かる。strings.TrimLeftやTrimRightは第二引数に取り除きたい文字のセットを渡すので、重複がいらない。
fmt.Println(strings.TrimLeft("42133word", "12334")) // NG // word // main.go:10:44: cutset contains duplicate characters (SA1024) fmt.Println(strings.TrimLeft("42133word", "1234")) // OK // word
SA1028 - sort.Slice can only be used on slices
sort.Slice関数の第一引数xはanyなんですが、GoDocにもあるとおりスライスじゃないとpanicしてしまう。こういうのを注意してくれるのは助かりますね。
SA2 – Concurrency issues
SA2001 - Empty critical section, did you mean to defer the unlock?
Lock直後に書かれたUnlockにdeferが付いてなければ注意してくれる。ただそれが有用な場合もあるとのことで、追々理解したい。
SA4 – Code that isn't really doing anything
_人人人人人人人人人人人人_ > 何もしていないコード <  ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^ ̄
SA4系のルールは全てありがたいと思いました。
SA4023 - Impossible comparison of interface value with untyped nil
ほぇぇとなったやつ。インタフェース変数にnil値を格納するとインタフェース変数はnilではない。ここもうちょっとちゃんと理解したい。
type MyInterface interface{} func nilOrNotNil() any { var p *MyInterface = nil return p } func main() { if nilOrNotNil() != nil { fmt.Println("not nil") // reached } } // main.go:13:5: this comparison is always true (SA4023)
SA5 – Correctness issues
SA5003 - Defers in infinite loops will never execute
deferはその関数がスコープなので、無限ループの中では絶対に実行されない。
SA5011 - Possible nil pointer dereference
ポインタがデリファレンスされる際にnilの可能性があるコードを注意してくれる。
SA6 – Performance issues
SA6005 - Inefficient string comparison with strings.ToLower or strings.ToUpper
strings.EqualFold関数は文字の大文字小文字を区別せずUnicode標準のCase Foldingという操作で比較を行うためstrings.ToLowerやToUpperを行ってから比較するより速い。実際に以下のようなコードで測ってみると速度的にメモリ的にも優れていますね。
var s1 = "HoGe" var s2 = "hOgE" func Benchmark_ToLower(b *testing.B) { for i := 0; i < b.N; i++ { strings.ToLower(s1) strings.ToLower(s2) } } func Benchmark_CaseFolding(b *testing.B) { for i := 0; i < b.N; i++ { strings.EqualFold(s1, s2) } }
% go test -bench . -benchmem goos: darwin goarch: arm64 pkg: sample Benchmark_ToLower-8 31507820 37.83 ns/op 8 B/op 2 allocs/op Benchmark_CaseFolding-8 124359349 9.807 ns/op 0 B/op 0 allocs/op PASS ok sample 4.479s
SA9 – Dubious code constructs that have a high probability of being wrong
「間違っている可能性のある」コードを教えてくれる。ただし他のルールに比べてこれは偽陽性の可能性があるとのこと。
SA9005 - Trying to marshal a struct with no public fields nor custom marshaling
非公開のフィールドに対してencoding/jsonなどによるmarshal、unmarshalは効かないのでそれを検知してくれる。これはgo vetでも同様のチェックを行ってくれますよね。
S1 – Code simplifications
S1012 - Replace time.Now().Sub(x) with time.Since(x)
time.Now().Sub(x)よりもtime.Since(x)。このルールに限らず標準パッケージの使い方によってよりシンプルに書ける方法が見つかるので読んでいて楽しい。
S1025 - Don’t use fmt.Sprintf("%s", x) unnecessarily
fmt.Sprintfを使う必要がない場合を検知してくれる。これ結構嬉しいかも、その変数がStringerインタフェースを実装してるのかどうかを毎回完璧に把握するの大変だと思うし。
ST1 – Stylistic issues
ここからデフォルトでは無効化されているルールが登場しますが、見た感じ全て有効化して問題ない気がしました。
ST1000 - Incorrect or missing package comment (non-default)
デフォルトでは無効化されているルール。パッケージコメントをCodeReviewCommentsに記載されているルールで縛るかどうか。
ST1003 - Poorly chosen identifier (non-default)
デフォルトでは無効化されているルール。変数名やパッケージ名を、Effective GoやCodeReviewCommentsに記載されているルールで縛るかどうか。
これは有効化してもいいんじゃないですかね、mixed-capsとかinitialismsとかが他のリンターで検知できるなら別ですけど。基本この辺りの細かい書き方はEffective GoやCodeReviewCommentsに従って書く人による違いをできるだけなく無くした方が「読みやすい」コードになると思うです。
QF1 – Quickfixes
自動で直されるやつ。
QF1012 - Use fmt.Fprintf(x, ...) instead of x.Write(fmt.Sprintf(...))
こういうのは知らないと気づかないだろうしリンターにチェックしてもらえるなら学習になると思うです。
というわけで全ルールを読んで、その中からかいつまんで紹介しました。知らなかった標準パッケージのAPIを知れたり、Goの仕様を知れたり、Goに限らず「こう書いた方がシンプルだよね」って書き方を再確認したりできました。Go初学者は目を通してみると良いんじゃないでしょうか 👍
『なぜあの人の解決策はいつもうまくいくのか?』を読んだ
ラノベみたいなタイトル。システム思考についての本をはじめて読んだのですが、いいですねこれ。
システム思考 is 問題をパターンとして捉えて、パターンを生み出している構造を理解して、構造に働きかけることで問題解決をしていくこと。
私たちは問題があると、すぐに問題解決をしようとし、問題の近くに解決策を探します。この例でいえば「うまくいっていた好循環がうまくいかなくなった。好循環を元通りに回すことが解決であり、そのためにどうしたらよいか」と考えるのです。ところが、解決策は問題の近くにあるとは限りません。一見、問題からはほど遠いところに効果的な解決策がある場合も多いのです。 p17
このあたりは常に気をつけたい。
六人の人が一つのルービックキューブに向かって、それぞれ自分の目の前の一面をきれいに合わせようとしている様子を想像してみてください。自分の目の前を合わせようとキューブを動かすと、その動きは他の人の面に影響を与えます。これではいつまでたっても、六つの面がきれいにそろうことはないでしょう。 p39
全体は部分の総和ではない。
しかし、私たちは「物事を変えたり創り出したりするために必要な時間」を無視して、時期尚早に成果を判断し、せっかくの取組みを「芽が出る前に」摘んでしまうことがよくあります。 p234
シンプルな問題であれば、図8-9のように、まっすぐに望ましい状態に向かって進捗していくでしょう。しかし、複雑で難しい問題の場合は、図8-10のように「いったん悪化してから、改善する」ことがよくあります。「高く飛びあがるには、深く沈み込む必要がある」ともいえるでしょう。 p234
このあたりは時間軸について。
本書で出てくる交通渋滞の例が結構分かりやすいですね。「なぜ道路を拡張したのに渋滞がひどくなったのか?」「なぜ道路を狭くする、自動車の通過に追加で税金を設ける、バス専用車線を追加する、といった直感に反するような解決策がうまくいくのか?」といったところからシステム思考の理解を深めることができます。
構造を理解するためのツールであるループ図は、自分自身で問題を理解したり、チームで共通の理解を促したりするのにかなり役立ちそうだなと思います。問題の近くにある変数だけじゃなく、遠くにある変数も含めてどういうループが回っているのか俯瞰して見れるようになると取りうる解決策の幅がかなり広がるだろうなぁ。
httptest.ResponseRecorderを使ったテストでHTTPステータスコードが意図した値にならないとき
httptest.ResponseRecorderを使ってHTTPサーバーのテストをしている際、レスポンスのHTTPステータスコードが意図した値にならないなぁと思って色々調べたのでそのメモです。
例えば以下のようなコードで再現することができます。
type MyHandler struct{} func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte("sample")) w.WriteHeader(http.StatusTeapot) } func main() { log.Fatalln(http.ListenAndServe(":8080", &MyHandler{})) }
このプログラムを実行してリクエストを送るとsampleという文字列がHTTPステータス200で返ってきます。
sample % curl -i localhost:8080 HTTP/1.1 200 OK ~省略~ sample
テストコードを簡単に書くとこのようになりますが、こちらも同様です。
import ( "net/http" "net/http/httptest" "testing" ) func Test(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/", nil) res := httptest.NewRecorder() h := &MyHandler{} h.ServeHTTP(res, req) if want, got := http.StatusTeapot, res.Code; want != got { t.Errorf("status code should be %d, but got %d", want, got) } }
=== RUN Test main_test.go:18: status code should be 418, but got 200 --- FAIL: Test (0.00s) FAIL
結論
- 200以外のHTTPステータスコードを設定したいならWriteメソッドの前にWriteHeaderメソッドを呼び出す必要がある
この辺りは、net/http.ResponseWriterインタフェースのドキュメントに書いてありますね。 pkg.go.dev
// WriteHeader sends an HTTP response header with the provided // status code. // // If WriteHeader is not called explicitly, the first call to Write // will trigger an implicit WriteHeader(http.StatusOK). // Thus explicit calls to WriteHeader are mainly used to // send error codes. // // The provided code must be a valid HTTP 1xx-5xx status code. // Only one header may be written. Go does not currently // support sending user-defined 1xx informational headers, // with the exception of 100-continue response header that the // Server sends automatically when the Request.Body is read. WriteHeader(statusCode int)
以下、結論を補足する形で実際にhttptest.ResponseRecorderの実装を読んでいきます。
httptest.ResponseRecorderの実装を読んでいく
HTTPサーバーのテストで利用するhttptest.ResponseRecorderの実装を追います。今回は、動機となった「レスポンスのHTTPステータスコードが意図した値にならない」の理由を知ることを目的に読んでいきます。
httptest.ResponseRecorderはhttp.ResponseWriterインタフェースを実装した型なので、Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
の3つのメソッドの周辺を調査します。
まず直接HTTPステータスコードを設定できるWriteHeader()メソッドの実装を眺めます。
func (rw *ResponseRecorder) WriteHeader(code int) { if rw.wroteHeader { return } checkWriteHeaderCode(code) rw.Code = code rw.wroteHeader = true if rw.HeaderMap == nil { rw.HeaderMap = make(http.Header) } rw.snapHeader = rw.HeaderMap.Clone() }
早速レシーバーのwroteHeaderフィールドが真ならリターンしていて怪しいです。wroteHeaderフィールドにはGoDocがありませんでしたが、名前の通りですね。では、いつどこでwroteHeaderフィールドが書き換えられているのか。
wroteHeaderで検索をかけると、参照している箇所がいくつか、値を代入している箇所が1箇所見つかります。値を代入している1箇所がWriteHeaderメソッドの中ですね。
よって同一のResponseRecorderインスタンスでは、WriteHeaderメソッドによるHTTPステータスコードの設定は一度のみできることが分かります。
次にWriteメソッドがどういう実装になっているかを見ます。
func (rw *ResponseRecorder) Write(buf []byte) (int, error) { rw.writeHeader(buf, "") if rw.Body != nil { rw.Body.Write(buf) } return len(buf), nil }
1行目でwriteHeaderという非公開メソッドを呼んでいるので、そいつを見にいきます。
func (rw *ResponseRecorder) writeHeader(b []byte, str string) { if rw.wroteHeader { return } if len(str) > 512 { str = str[:512] } m := rw.Header() _, hasType := m["Content-Type"] hasTE := m.Get("Transfer-Encoding") != "" if !hasType && !hasTE { if b == nil { b = []byte(str) } m.Set("Content-Type", http.DetectContentType(b)) } rw.WriteHeader(200) }
今回の目的に沿って読むならば、気になる箇所は2箇所あります。まず1行目では、公開メソッドのWriteHeaderと同様にwroteHeaderフィールドがtrueなら何もせずreturnしてますね。そしてメソッドの最後にWriteHeader(200)メソッドを読んでいます。前述した通りWriteHeaderメソッドは内部でwroteHeaderフィールドをtrueにします。よってWriteメソッドは(Bodyへの書き込み前に)HTTPステータスコードに200を設定することがわかります。
ここまで読むと、最初に載せたサンプルコードでなぜHTTPステータスコードが418にならないのか、なぜテストが失敗するのかが理解できます。
func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte("sample")) // この時点で200が設定され w.WriteHeader(http.StatusTeapot) // 上書きはできない }
それから、「同一インスタンスで一度のみ」ということは、例えばTable Driven Testなどでhttptest.ResponseRecorderのインスタンスを使い回しているとHTTPステータスコードが以前の値に固定されてしまうので、サブテスト内で初期化する必要があるということですね。
ドキュメントや実装を読むことで理解を深めることができました。
参考
『ファシリテーションの教科書 - 組織を活性化させるコミュニケーションとリーダーシップ』を読んだ
https://www.amazon.co.jp/dp/B00P28A5M8
『多様性の科学』を読んでからというもの、集団で仕事をする -> 集合知を高める(集団脳を構築する)と考えた時に、集合知を高める観点で周りを見渡すとまた色々な領域で学ぶことがあるな〜と思っています。
会議もそのひとつで、会議は一人ではやらないので、二人以上で集合知を利用してよりよい結論に辿り着くためのイベントなはずです。会議を因数分解していくと「ファシリテーション」があり、そういえばファシリテーションをちゃんと学んだことないな...社会人はみんな新卒1,2年目くらいで学んだりするのかな...ぐぬぬと思ったので本書を手に取りました。
結果からいうと会議のファシリテーションに限らず、人と話すうえで相手の発言をどう整理するかとか、欠落したあるいは曖昧になっている主張や根拠をどう引き出すかなど、日頃の仕事の考え方が変わるような知見に溢れた本でした。やったね。
ただ同時にファシリテーション難しすぎじゃね?とも思いました。考えるべきことが多すぎて、ひとりの人間がやることじゃあないなぁと思ったので、理想は会議の参加者全員が本書に書いてあるような、そもそも会議の構成を意識したり、論点整理力だったり、聴く/伝える力を鍛えたりするのが良いのでしょうね。