sync.Poolの挙動について(Golang)
概要
標準パッケージのsync.Pool
を利用して、ファイルの中身を常にメモリ上(=キャッシュ)におきたかったのですが、動作が不定のためうまく利用できなかった話です。
Go version
% go version go version go1.8.1 darwin/amd64
sync.Pool概要
概要としては、以下の認識です
- オブジェクトを一時的に、保持するための機能
- 再利用されるオブジェクトをキャッシュする利用法
- 複数のgoroutineで安全に利用可能
- copy禁止(
noCopy
structが埋め込まれている)
メソッド
Get
func (p *Pool) Get() interface{}
- Poolよりitemを取り出す
- 取り出されたitemはPoolから削除
- Putで入れた値とGetで取り出す値の関係を考慮すべきでない
Put
func (p *Pool) Put(x interface{})
- Poolに値を追加する
動作確認
下記のコードを利用して、Getした際とPutした際の挙動を確認してみました。
https://github.com/midorigreen/gopool/blob/master/main.go
package main import ( "fmt" "net/http" "sync" ) var pool = sync.Pool{ New: func() interface{} { return "init" }, } func main() { mux := http.NewServeMux() mux.HandleFunc("/pool", poolHandler) mux.HandleFunc("/pool/change", poolChangeHandler) http.ListenAndServe(":8888", mux) } func poolHandler(w http.ResponseWriter, r *http.Request) { fmt.Println("----------------------------------") fmt.Println("/pool") fmt.Println(get().(string)) fmt.Println("----------------------------------") } func poolChangeHandler(w http.ResponseWriter, r *http.Request) { fmt.Println("----------------------------------") s := get().(string) fmt.Println("/pool/change") pool.Put(s) fmt.Println("-- changed --") fmt.Println("----------------------------------") } func get() interface{} { s := pool.Get().(string) cpS := s // Getした値を再度Poolに戻している pool.Put(s) return cpS }
出力例
実際の出力例です。
上記コードより、get()
で呼び出した際は、Get→Put
としているため
同じ値がGet()
で取得されて欲しいところです。
ですが、一度/pool/change
APIを叩いた後も"init"が出力されていることから、
Get→Put
で値を戻したところで、必ず同じ値をGet()
してくれるわけではなさそうです。
---------------------------------- /pool init ---------------------------------- ---------------------------------- /pool/change -- changed -- ---------------------------------- # ↓ これ以降はすべて "change handler" が出力されていて欲しい ---------------------------------- /pool change handler ---------------------------------- ---------------------------------- /pool init ---------------------------------- ---------------------------------- /pool change handler ---------------------------------- ---------------------------------- /pool init ---------------------------------- ---------------------------------- /pool change handler ---------------------------------- ---------------------------------- /pool init ---------------------------------- ---------------------------------- /pool change handler ---------------------------------- ---------------------------------- /pool change handler ---------------------------------- ---------------------------------- /pool init ----------------------------------
感想
Callers should not assume any relation between values passed to Put and the values returned by Get. → Putで入れた値とGetで取り出す値の関係を考慮すべきでない
とあるように、Put()
で入れたからGet()
で必ず入れた値が取得できるわけではないことを念頭に置いて開発しないといけないようです。
ファイルの中身をキャッシュしておいて、書き換え時は更新等を行いたかったのですが、
更新をしたデータをPut()
で突っ込んでも返ってくるとは限らなくなってます。
結論、あまり良い使い方ではなさそうです。(一意にキャッシュを保てないので、実データと差分がでてしまうため)
補足
@nobonoboさんにも指摘頂きました。
Get処理でGet&Putしてるわけですね。Poolは内部にもつデータのどれをGetで返すのかは不定です。Put多数->Get多数と、Get&Putを繰り返す場合の挙動は異なるのでご注意を。
— nobonobo🐦 (@nobonobo) 2017年6月25日
参考
Bayeux プロトコルの仕様について
はじめに
Pub/Sub Messaging
を勉強するにあたって、Bayeuxプロトコルでの実装があるとのことだったので、Bayeux プロトコル日本語訳を読んでメモを取ってみました。
枯れた仕様(検索であまり当たらない)な気もしていますが、せっかく読んだので公開しようかと思います。
Bayeuxプロトコル概要
- Webサーバー間でメッセージを非同期にやりとりするためのプロトコル
- メッセージは名前付きチャネルを経て、ルーティング及び送達される
- サーバ <=> クライアント
- ajaxを利用したサーバープッシュ型のテクニック=「comet」
- 準拠要求
- 全てのMUST及びREQUIREDを満たし、かつ、全てのSHOULDを満たすものは、"完全準拠(unconditionally compliant)"
- 1つでもSHOULDを満たさないモノは「条件付き準拠(conditionally compliant)」
- ワード定義
HTTPプロトコル
- リクエスト/レスポンス型のプロトコル
- クライアント -> サーバー
- サーバー -> クライアント
- ステータスライン
- プロトコルバージョン
- 基本的にサーバー -> クライアントへはクライアントの要求なしに通信を走らせない
- Bayeuxでは、双方向の非同期通信を走らせるためサーバー・クライアント間で複数コネクションをサポートしている
- 通常アクセス用コネクション
- ロングポーリング用コネクション
- (MUST NOT)Bayeuxプロトコルも、サーバーが全てのアプリケーションに対して処理を行うために3本以上のコネクションをはらない
様々なルール
- (MUST) クライアントはブラウザのシングルオンポリシーを尊守しなければならない
- シングルオンポリシー = JSからはそれがダウンロードされたサーバー以外への接続は許可されない
- 2コネクション処理
- (MUST) Bayeux実装はHTTPのパイプライン処理を制御し、リクエスト順を保持して実行しなければならない
- ポーリング
- レスポンスを受け取った後、メッセージをサーバーに対して送信する
- 次のメッセージを受け取れるようにする処理
- Bayeux実装は、ロングポーリングと呼ばれる形式のポーリングをサポートする必要がある
- レスポンスを受け取った後、メッセージをサーバーに対して送信する
- 接続ネゴシエーション
- 接続の際、コネクション/認証/を交換して合意するネゴシエーションが行われる
- その際、ハンドシェークメッセージがかわされる
- クライアントの状態
-------------++------------+-------------+----------- +------------ State/Event || handshake | Timeout | Successful | Disconnect || request | | connect | request || sent | | response | sent -------------++------------+-------------+----------- +------------ UNCONNECTED || CONNECTING | UNCONNECTED | | CONNECTING || | UNCONNECTED | CONNECTED | UNCONNECTED CONNECTED || | UNCONNECTED | | UNCONNECTED -------------++------------+-------------+------------+------------
// BNF記法 alpha = lowalpha | upalpha lowalpha = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" upalpha = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" alphanum = alpha | digit mark = "-" | "_" | "!" | "~" | "(" | ")" | "$" | "@" string = *( alphanum | mark | " " | "/" | "*" | "." ) token = ( alphanum | mark ) *( alphanum | mark ) integer = digit *( digit )
- チャネル
channel_name = "/" channel_segments channel_segments = channel_segment *( "/" channel_segment ) channel_segment = token
メッセージフィールド定義
名 | 説明 |
---|---|
channel | - メッセージの配送先および配送元を示す - リクエスト: 配送先 - レスポンス: 配送元 |
supportedConnectionTypes | - /meta/handshake へ送受信の際に利用 - どの転送タイプを用いるか決める - (MUST) long-polling,callback-polling等が存在 |
clientId | - クライアントをユニークに識別するもの - /meta/handshake ,投稿メッセージ以外の全てのメッセージに付与が必須 |
id | 全てのメッセージに付与可能 |
timestamp | - ISO 8601形式(YYYY-MM-DDThh:mm:ss.ss ) - GMTで表記 |
successful | -リクエストの成否 - /meta/* 系のレスポンスに必須 |
error | エラー |
メタメッセージ定義
handshake
クライアントは、/meta/handshake
channelに対してメッセージを送ることで接続ネゴシエーションを開始する
handshake request (MUST field)
field | 説明 |
---|---|
channel | /meta/handshake という値 |
version | クライアントで処理されるバージョン |
supportedConnectionTypes | 接続タイプの文字列 |
handshake response
field | 説明 |
---|---|
channel | /meta/handshake という値 |
version | クライアントで処理されるバージョン |
supportedConnectionTypes | クライアント・サーバー共にサポートする接続タイプ |
clientId | クライアント固有のID |
successful | true/false |
connect
クライアントは、/meta/connection
channelにメッセージを送ることでコネクションを開始する。
connection request
field | 説明 |
---|---|
channel | /meta/connection という値 |
clientId | クライアント固有のID |
connectionType | 接続タイプ |
connection response
field | 説明 |
---|---|
channel | /meta/connection という値 |
successful | true/false |
clientId | クライアント固有のID |
subscribe
チャネルへの登録を行うために、/meta/subscribe
にリクエストを行い、配信登録を行う。
subscribe request
field | 説明 |
---|---|
channel | /meta/subscribe という値 |
clientId | クライアント固有のID |
subscription | 購読するチェネル名/チャネルパターン/配列 |
subscribe response
field | 説明 |
---|---|
channel | /meta/subscribe という値 |
successful | true/false |
clientId | クライアント固有のID |
subscription | 購読するチェネル名/チャネルパターン/配列 |
イベントメッセージ定義
イベントメッセージ投稿
channelに対して、イベントを投稿する処理のこと
publish request
field | 説明 |
---|---|
channel | 投稿するchannel名 |
data | JSONオブジェクトの形をしたメッセージ |
publish response
field | 説明 |
---|---|
channel | 投稿したchannel名 |
successful | true/false |
イベントメッセージ配信
channelからクライアントへイベントを配信する
field | 説明 |
---|---|
channel | 投稿するchannel名 |
data | JSONオブジェクトの形をしたメッセージ |
ポーリング
long-polling
特徴
- 配送すべきメッセージが届くまでコネクションをオープンにし続ける
- 伝送遅延を最小化
- フェールセーフとして、classicalな数秒ごとのポーリング処理を行う
- POSTメッセージのボディで伝送される
- 形式
application/x-www-form-urlencoded
text/json
参考
GolangのSort処理について
GolangのSort処理
GolangのSort処理について、まとめました。
(// package sort
と記載があるサンプルコードは、Golang本体のソースコードです)
Sort
Sort Interface
Golangでは、struct
のソートを行うため、sort.Interface
を実装する必要があります。
(実際はGo1.8以降、下記の sort.Slice()
を利用して、ソートすることができるようになりました。参考)
// sort.Interface type Interface interface { // Len is the number of elements in the collection. Len() int // Less reports whether the element with // index i should sort before the element with index j. Less(i, j int) bool // Swap swaps the elements with indexes i and j. Swap(i, j int) }
メソッド | 特徴 |
---|---|
Len() int | 配列・リストの要素数 |
Less(i, j int) bool | i < j を満たすかの真偽値 |
Swap(i,j int) | iとjの要素を入れ替える |
Sortの方法
標準パッケージで対応している型は下記になります。
- float64
- int
- string
ここでは、ソートの方法をint配列をもとにみていきます。
sort.Ints(a int[])
Ints()
メソッドは、int[]型をソートするメソッドです。
これは、昇順(increacing)にソートされます。
nums := []int{4, 3, 2, 10, 8} sort.Ints(nums) fmt.Println(nums) // output: [2 3 4 8 10]
内部では、 IntSlice
型のstructにつめかえを行い、Sort関数を呼んでいます。
(sort.Interface
を実装するルールは同じ)
// package sort // Ints sorts a slice of ints in increasing order. func Ints(a []int) { Sort(IntSlice(a)) } // IntSlice // Len,Less,Swapを実装したstruct // IntSlice attaches the methods of Interface to []int, sorting in increasing order. type IntSlice []int func (p IntSlice) Len() int { return len(p) } func (p IntSlice) Less(i, j int) bool { return p[i] < p[j] } func (p IntSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
sort.Reverse(data Interface) Inteface
Reverse(data Interface)
は、sort.Interface型のソート方法を逆順に修正する関数です。
実際に内部で行っていることは、下記の2点です。
\1. reverse struct
にstruct
を詰め替える
reverse struct
は、 sort.Interface
を埋め込んでいます。
// package sort type reverse struct { // This embedded Interface permits Reverse to use the methods of // another Interface implementation. Interface } // Reverse returns the reverse order for data. func Reverse(data Interface) Interface { return &reverse{data} }
\2. Lessメソッドの引数をスワップ
reverse struct
は、実装を参照したところ、
Less(i,j int) bool
のみを再実装しています。
これにより、比較条件を逆にすることで、逆順ソートを実現しています。
// Less returns the opposite of the embedded implementation's Less method. func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) }
sort.Search(n int, f func(int) bool) int
nの長さの配列内でf()=true
となる最小のindexを返却します。
→ 配列内で指定のvalueがあるかどうかを調べる関数です。
特徴
- 条件を満たさなかった場合、nが返却
- 一致を探すことも、ある数値以上の値を探すことも可能
- binary search(二分探索)で検索
- ソートされた配列に対して実行しないと正確な結果が返却されない
- 下記は
Search
のラッパー関数- func SearchFloat64s(a float64, x float64) int
- func SearchInts(a int, x int) int
- func SearchStrings(a []string, x string) int
// package sort // binary sort の実装 func Search(n int, f func(int) bool) int { // Define f(-1) == false and f(n) == true. // Invariant: f(i-1) == false, f(j) == true. i, j := 0, n for i < j { h := i + (j-i)/2 // avoid overflow when computing h // i ≤ h < j if !f(h) { i = h + 1 // preserves f(i-1) == false } else { j = h // preserves f(j) == true } } // i == j, f(i-1) == false, and f(j) (= f(i)) == true => answer is i. return i } // Int ラッパー関数 func SearchInts(a []int, x int) int { return Search(len(a), func(i int) bool { return a[i] >= x }) }
利用サンプル
nums := []int{4, 3, 2, 10, 8} x := 8 // 事前にソートしておく sort.Ints(nums) // search asc index = sort.Search(len(nums), func(i int) bool { return nums[i] >= x }) if index < len(nums) && nums[index] == x { // nums[index] = 8 = x } else { // x は nums[]中に存在しなかった // だが、indexはnums[]の中にxをインサートする位置にある }
sort.Slice(slice interface{}, less func(i, j int) bool)
渡したsliceを、ソートする関数。
less関数で定義された順にソートされる。
特徴
- Go1.8以上
- sort.Interfaceを実装していなくてもソートが可能となっている
- stable sort(安定ソート)を保証していない
- sort.SliceStableを利用することで保証される
(参考)実際のソート処理の中身について
sort.Sort(data sort.Interface) は渡されるデータによってソートの種別変えています。
流れとしては、基本的にクイックソートを利用して、条件を満たした際の他のソート手法に切り替わるといった挙動です。
- ヒープソート
- 条件: log(n+1)の整数値の2乗の深さとなった場合
- 右に1ビットずつシフト演算して0になるまでの回数の2乗が最大の深さ(
maxDepth
) maxDepth
が0となったときに、ヒープソートに切り替わる
- 右に1ビットずつシフト演算して0になるまでの回数の2乗が最大の深さ(
- 条件: log(n+1)の整数値の2乗の深さとなった場合
// package golang // Switch to heapsort if depth of 2*ceil(lg(n+1)) is reached.
- シェルソート
- 条件: スライスが12より短い場合
// package golang // Use ShellSort for slices <= 12 elements
- クイックソート
- 条件: 上記条件に当てはまらない場合
まとめ
Golangのsortパッケージについて、学びました。
実際のソート処理の中身までは、深く追求しませんでしたが、
ソートを行う利用方法については触れられたかなと思います。
参考
golang.tokyo #6 に行ってきました
概要
golang.tokyo #6 に参加しましたので、レポートを記載いたします。
(会場は、DeNAさんでお寿司(食べ損ねた)とお酒をご用意いただいておりました。)
下記は各セッションのまとめです。
Gopher Fest 2017
@tenntenn さん
スライド
概要
サンフランシスコで開催されるGoSFが主催のイベントの参加レポートです。
セッションの中のひとつ The state of Go のお話
The state of Go
Go1.9での仕様変更や標準ライブラリの改善についてのお話
(余談ですが、tip.golang.orgで最新のmasterのドキュメントが見れるそうです)
- Go1.9は5/1にコードフリーズ済み
- 残りはバグフィックスのみ
- リファクタリングを安全にする方法
- Goで安全にリファクタできるのか?
- 定数の場合は問題なし
- 関数はラップして引き継げば問題なし
- 型の場合→ 問題有り
- 型を作る → メソッドが引き継げない
- 埋め込み → キャストができない
- Go1.9でAlias導入
- 言語仕様変更
- 標準ライブラリ変更
- math/bits ビット演算に便利なライブラリ
syc.Map
スレッドセーフなマップhtml.template
がより安全に利用可能に- errorを返却するようになる
- os.Exec
- 環境変数が重複の際、一番後ろのものを優先する
- ソース中で上書き可能
- 環境変数が重複の際、一番後ろのものを優先する
- go testの改善
- vendorを無視するようになった
感想
Go1.9にあたっての改善点のお話がメインとなりました。
@tenntennさんもおっしゃってましたが、 go test
でvendor
配下を
みなくなる修正はなかなか良いですね。
初めてGolangで大規模Microservicesを作り得た教訓
Yuichi Murata さん
スライド未公開(2017/06/01)
概要
GAE/Goで大規模なMicroservices(30くらい)を作った際の、教訓のお話
- いきなりつかってみたため、失敗談が中心
- 構成
- GAE/Go
- Gin/Echo
- Microservices構成
- 結論
- 困ったときはGoの哲学にそったシンプルなアプローチを取る
- Goを過信せずパフォーマンスに気を配る
教訓
1. フレームワークにこだわらない
- Railsの経験から良いフレームワーク探しをこだわって行った
- Gin
- 良い点
- シンプル
- App Engine とのインテグレーション有り
- Framework Context vs App Engine Context
- gin.ContextからApp Engine Contextに派生させられることで解消
- 困った点
- ワイルドカードパスを利用したルーティング
- 良い点
- Echo
- まとめ
2. Interfaceを尊重する
- 独自のエラー型を定義
- こちらの型にerrorを寄せる
- 毎回キャストするのが面倒のため
- こちらの型にerrorを寄せる
- Nilに型がある問題発生
- (参考記事) 絶対ハマる、不思議なnil
- 独自のエラー型を混在させた際に発生
- 独自型とerrorインタフェースでnilの型が異なる
- まとめ
- Interfaceが定義されたものはそのまま利用するほうが自然に実装できる
3. regex compile/ reflectionが遅い
- バリデーション
- JSON Schemaを利用
- GolangでのJSON Schema
- gojsonschema
- go-jsval (たしか..)
- パフォーマンステストで問題発覚
- バリデーションの有無でパフォーマンスに顕著な差が出る
- pprof を利用して実行を解析
- 問題点の解消
- まとめ
4. 非対称暗号は遅い
- 認証・認可処理
- 荒業で解消
感想
GAE/Goでサービスを作る際の問題点がいくつか聞けました。
サービスでの利用はこれからあるかもなので、参考になりました。
またnilに型がある問題は、まだあたったことがないので知れて良かったです。
[LT]ゲーム開発に欠かせないあれをしゅっとみる
ゴリラのアイコンの方(@Konboi)
スライド
概要
- CSVの話
- スマホゲームには欠かせない
- カヤックではデータの受け渡しとしてCSVを利用している
- マスターデータ等
- CSV困る時
- カラムとデータの関連性が見づらい
- 空欄がつらい
- DBにインポートする系はその際にわかる
- 調査系で利用する時つらい
- csviewer
- csvをDBのテーブルを検索するように参照できる
- ライブラリ
- sliceflag
- 同一オプションで複数の値を受け取りたい
- 普段のflagの利用方法と変わらない
- tablewriter
- データをいい感じにテーブル表示してくれる
- sliceflag
[LT] Go Review Commentを翻訳した話
@knsh14 さん
スライド
概要
Go Code Review Commentを読もう
(→ 読みます)
- Go Code Review Commentとは
- コードレビューをする際に見るべき箇所をまとめたもの
- Effective Goの簡略版
- 翻訳してみました(qitta)
- 良かった点
- 正解のパターンを勉強できるところが効率が良い
- 内容
- コードの見た目を改善
- ツールでなんとかしよう
- golintは優秀
- コメントや文章の体裁
- Tips系
- より良いパターンを記載
- 設計の指針
- コードの見た目を改善
- まとめ
- 良いドキュメントの翻訳は英語の勉強に最適
[LT] ScalaからGo
James さん
スライド未公開(2017/06/01)
概要
- 関数型開発はGoでできるか?
- できないですね
- ジェネリクスがないので..
- 関数型開発のコンセプトは利用できるか?
- 利用できる!!
- 関数型開発とは?
- 副作用がない開発
- 副作用あり
- テストしにくい、バグの原因
- 部分適用
- Goは初心者が入りやすく会社での導入がスムーズに行える点が良い
[LT] Crypto in Go
概要
- Goにおける暗号アルゴリズムの利用
- AES
- 固定長でしか利用できない
- AES + Padding + HMac
- 行数があって面倒
- AES + GCM
- 機密モード + 認証モードがひとつに
- とってもシンプル
まとめ
各セッションとLTについて、ざっくりと記載いたしました。
詳細知りたい際は、スライドが公開されるはず(ほぼされてますが)ですのでそちらをご参照ください。
全体的に有意義な発表が聞けて楽しかったです。
開催いただきありがとうございました。(次回も参加したいです)
JUnit4を利用したJavaのテスト概要
概要
JUnitを利用した、Javaのユニットテストを作成する際の基本的な部分について記載しました。
JUnit実践入門の内容 + 経験から記載しました。
テストコード基本
ユニットテスト対象物
テストクラスに対する、テスト対象物はテストクラスのコードのみとします。
そのため、外部オブジェクトの作成等は基本的にモック化します。
(外部クラスのコードのテストは、外部クラスのテスト上で行えば良いとの考え方です。)
またテストメソッドの対象物ですが、基本的に publicメソッド
を対象とします。
privateメソッド
は、publicメソッド
を正確にテストすることで、同時にテストされていなければ、ならないと考えているためです。
各命名と役割
命名 | 役割 | 備考 |
---|---|---|
XXXTest | テストクラス名 | |
xxx_条件_結果 | テストメソッド | - xxx()メソッド - @Test アノテーションを付ける |
sut | テスト対象オブジェクト変数名 | - テスト対象のオブジェクトを明確化するために同一名称で宣言する。 - System Under Testの略 |
setUp() | テストごとの前処理 | - 各テスト実行ごとの前処理を記載 - @Before アノテーションを付ける |
tearDown() | テストごとの後処理 | - 各テスト実行ごとの後処理を記載 - @After アノテーションを付ける |
setUpClass() | テストクラスごとの前処理 | - テストクラス内のテストが一つでも実行される前の処理を記載 - @BeforeClass アノテーション記載 - public static methodで宣言 |
tearDownClass() | テストクラスごとの後処理 | - テストクラス内のテストが全て実行された後の処理を記載 - @AfterClass アノテーション記載 - public static methodで宣言 |
テストのフェーズ
事前準備 =
set up
下記を行います。- テストデータ作成
- DBの接続
- モックの作成
- モックからの返り値の設定
- Testクラスに渡すオブジェクトの作成
- Testクラスのオブジェクト化
- ..etc
実行 =
exercise
テスト対象のメソッドを実行します検証 =
verify
メソッドの実行結果を検証します。
メソッドの返り値が期待値通りかを判定するフェーズです。後処理 =
tear down
テスト実行後の後処理を行います。- テストデータの破棄
- DBの切断
Assert
テスト結果を検証するための方法についてです。
検証とは基本的に、テスト結果が期待値通りかを確かめるフェーズとなります。
- Assertは、
org.junit.Assert.assertThat()
を利用- `assertThat` 一つで基本的に事足りる - static importを行っておく
- Matcher
org.hamcrest.CoreMatchers
is()
equalsメソッドによる比較nullValue()
nullであることを検証する- 利用する際は、
is(nullValue())
と利用する
- 利用する際は、
not()
評価値を反転させる
org.junit.matchers.JUnitMatchers
hasItem()
Iterableインタフェースを実装したクラスに、期待値が含まれているか検証hasItems()
複数指定可能
BaseMatcher<>
を継承することで、カスタムMatcherが作成できる
サンプルコード
上記までの内容を踏まえた、テストサンプルコードです。
public class TargetClassTest { @Before public void setUp() { // テストごとの共通の前(初期化)処理 } @After public void tearDown() { // テストごとの共通の後処理 } @Test public void hasUserName_ユーザー名称をもつ場合_trueを返却() throws Exception { // set up /** テスト対象のオブジェクトの変数名は sut とする **/ TargetClass sut = new TargetClass)(); // exercise boolean actual = sut.hasUserName(); // verify /** 英語の構文 "assert that actual is expected" を意識する**/ asseertThat(actual, is(true)); }
(追記) Exception発生時のテスト
/** * Exception発生時のテスト */ @Test(expected = NullPointerException.class) public void fetchUserName_ユーザー名称を取得に失敗_exception発生() { // set up /** テスト対象のオブジェクトの変数名は sut とする **/ TargetClass sut = new TargetClass)(); // exercise /** 下記メソッド実行時にException発生 **/ Result actual = sut.fetchUserName(); } }
テスト作成にあたっての他用語
Fixture
事前準備にて、設定する情報のことで、下記2パターンのFixtureの設定方法があります。
- inline set up
- 各テストメソッドごとに、fixtureのset upを行う
- simpleに設定を記述すれば良い
- コードが長くなり、可読性が悪くなりがち
- implicit set up
- @Beforeアノテーションをつけたメソッドにて設定を行う手法
パラメータ化テスト
テストに対してパラメータを設定したい際に、利用します。
Theories
を利用して実現します。
- Theories
@RunWith(Theories.class)
をクラス宣言の前に宣言- テストランナーの一つ
@Theory
テストメソッドのアノテーション@DataPoint
パラメータ
// サンプルコード @RunWith(Theories.class) public class TargetClassTest { @DataPoint public static int PARAM_1 = 1; @DataPoint public static int PARAM_2 = 2; public TargetClassTest() { // 初期化処理 } @Theory public void testCase(int x) throws Exception { } }
Rule
テストをプラグイン的に拡張できる機能のことです。
テストに関係なく、初期化+後処理をしたい場合 に便利です。
publicなfiledにアノテーションをつけて利用します。
// サンプル @Rule public Timeout timeout = new Timeout(100);
- 処理はテストごとに実行される
→ テストごとに実行したい共通処理をRuleとしてまとめられる @ClassRule
にてテストClassごとに1回のRuleも作成可能- カスタムルールの作成 方法1
org.junit.rules.TestRule
インターフェイスを継承するStatement apply(final Statement base, Description description)
をOverrideする- 引数で渡される
Statement base
が各テストメソッドのイメージ base.evaluate()
を実行することでテストが実行される
- 引数で渡される
- これを利用することでテストに共通の前後処理を定義することができる
public class HogeRule implements TestRule { private void before() {} private void after() {} @Override public Statement apply(final Statement base, Description description) { // new Statement()することで実際のテストを拡張している return new Statement() { // 前処理 before() // テスト実行 (@Before -> テスト実行 -> @After) base.evaluate(); // 後処理 after() } } }
- カスタムルールの作成 方法2 (← こちらのほうが効率よく作れます)
- 上記の設定を、事前に行っているクラス(
ExternalResource
)を利用 - ExternalResourceを継承して作成
- 利用する際は、
before()
とafter()
をOverrideして、前後処理を記載する - Rule内で利用する変数はprivate fieldに宣言しておき、コンストラクタで受け取る
- 上記の設定を、事前に行っているクラス(
// 上記のサンプルコードをすでに実装した抽象クラス public abstract class ExternalResource implements TestRule { public Statement apply(Statement base, Description description) { return statement(base); } private Statement statement(final Statement base) { return new Statement() { @Override public void evaluate() throws Throwable { before(); try { base.evaluate(); } finally { after(); } } }; } /** * Override to set up your specific external resource. * * @throws if setup fails (which will disable {@code after} */ protected void before() throws Throwable { // do nothing } /** * Override to tear down your specific external resource. */ protected void after() { // do nothing } }
モック
モックとは、テスト対象クラス以外のクラスを擬似的に作成して、対象外クラスのメソッドの
返り値を設定する事ができます。
テスト対象クラス に絞ったテストを行うために重要な要素です。
org.mockito.Mockito
ライブラリを利用する- JavaDoc
- mockの利用方法について詳細が記載されている
- モックの作成
mock(TargetClass.class)
- 返り値の設定
when(sut.exec(anyInt(), any(Date.class))).thenReturn(obj)
- JavaDoc
データベースのテスト
(手法が確立できていないため一時保留)
- H2 Databaseの利用
- DBUnit
- @Ruleとして、作成することでDB接続周りを一手に引き受けられる
org.dbunit.AbstractDatabaseTester
を継承する
コードカバレッジ
カバレッジの種類
- C0(命令網羅)
- C1(分岐網羅)
- すべての分岐(if)を1回以上実行したかを測定
if
条件のtrue
orfalse
のどちらも通す必要がある
- C2(条件網羅)
- すべての条件を1回以上実行したかを測定
if
条件のtrue
になる全ての条件とfalse
になるすべての条件を見る必要がある
カバレッジ測定ツール
- [EclEMMA(http://www.eclemma.org/)
- Jacoco
- カバレッジ測定エンジン
- Mavenにライブラリとして追加 Maven Repository
# テスト実行 + カバレッジ測定 % mvn clean jacoco:prepare-agent test jacoco:report # カバレッジレポート参照 % target/site/jacoco/index.html
おわりに
JUnitを利用した、ユニットテストに関してざっくりとした記事を書きました。
昨今は、マイクロサービス化の利用等でますますユニットテストの価値が上がってきているかと思います。
(テストがないコードはレガシーとまで言われるかもしれないですね。。)
テストを何のために書くのかや、どういったテストが良いテストなのかについては特に記載しませんでした。
そのあたりは自分で考えていただいて、テストの有用性に対して正確な理解を個人でしてもらえればと思います。
また、自分で利用したことがないため、テストランナーについてあまり記載できていませんでしたので、
学んだ際に追記できればと思います。
参考文献
Vagrantに開発環境構築
新規サービスの開発環境を整えるために、新たにnginx+php-fpm環境を構築しました。
参考のリンクまとめ
1.Vagrant構築
2.nginx構築
3.php-fpm構築
centOS7では、
nginxのスタートコマンドは
systemctl start nginx
自動起動コマンドは
systemctl enable nginx
php-fpmも同様に
systemctl start/enable php-fpm
また、開発ディレクトリを変更してPHPを動作させるには、/etc/nginx/conf.d/default.conf内の
location / { root /var/www; index index.php; }
と変更し、以下の部分も
location ~ \.php$ { root /var/www; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME /var/www$fastcgi_script_name; include fastcgi_params; }
と変更することで、動きました。 書いてたら、Qiitaに書けよと言われそうな(もしくは、この記事いる?)記事になってました。