Dependency Injection からモックライブラリまで

83fb9f537f0aedcfbfc22dc395e85c84?s=47 daimatz
September 11, 2014

Dependency Injection からモックライブラリまで

83fb9f537f0aedcfbfc22dc395e85c84?s=128

daimatz

September 11, 2014
Tweet

Transcript

  1. Dependency Injection から モックライブラリまで GolangTestNight (Gunosy.go#10) daimatz 2014-09-11 1 /

    31
  2. 自己紹介 ID: daimatz Scala でアドテクとかやってます あと Haskell とか Ruby とか好きでした

    Go は別にガリガリ使っているわけではない 2 / 31
  3. Go の気に入らないところ 構文 型推論不要派。 変数の同時代入のとき型注釈をつけたい v a r n i

    n t , e r r e r r o r = b u f . R e a d ( b y t e s ) みたいに書きたい 変数と型は : で区切るでしょ普通 m a p [ i n t ] s t r i n g の特別感 型システム ジェネリクスほしいっす ライブラリのバー ジョニングの話 g o g e t するバー ジョンを指定できない 「 後方互換性をなくすような変更するな」 は理想論… それ以外は大体気に入っている 3 / 31
  4. 今日の話 Dependency Injection アプリケー ション初期化と DI コンテナ インター フェー ス実装によるモックとモックライブラリ

    4 / 31
  5. 今日の話 Dependency Injection アプリケー ション初期化と DI コンテナ インター フェー ス実装によるモックとモックライブラリ

    5 / 31
  6. Dependency Injection DI, 依存性の注入 依存しているオブジェクトを直接クラスの中に持っておくのではなく、 コンス トラクタなどで受け取れるようにする インター フェー スに対してプログラミングを心がけられる

    モックしてテストしやすくなる 6 / 31
  7. 例: Twitter ボットアプリケー ション f u n c T w

    e e t ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) { . . . } f u n c G e t T i m e l i n e ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) { . . . } t y p e T w i t t e r B o t s t r u c t { . . . } f u n c ( t * T w i t t e r B o t ) E v e n t L o o p ( ) e r r o r { f o r { s t a t u s I d s , e r r : = t . A c t i o n ( 1 0 ) . . . t i m e . S l e e p ( 6 0 * t i m e . S e c o n d ) } } f u n c ( t * T w i t t e r B o t ) A c t i o n ( c o u n t i n t ) ( [ ] i n t , e r r o r ) { t l , e r r : = G e t T i m e l i n e ( c o u n t ) r e t : = m a k e ( [ ] i n t , 0 ) f o r i : = r a n g e t l { s t a t u s : = t l [ i ] s t a t u s I d , e r r : = T w e e t ( " @ " + s t a t u s . U s e r I d + " G o o d m o r n i n g ! " , s t a t u s . I d ) r e t = a p p e n d ( r e t , s t a t u s I d ) } r e t u r n r e t , n i l } f u n c m a i n ( ) { b o t : = & T w i t t e r B o t { . . . } b o t . E v e n t L o o p ( ) } 7 / 31
  8. 問題点 T w i t t e r B o

    t が直接 T w e e t , G e t T i m e l i n e に依存している モックして一時的に振る舞いを変えることができない T w i t t e r B o t を単体テストしようとしても必然的に T w e e t および G e t T i m e l i n e の 動作を前提としてしまう 粒度の大きいテストしかできない 粒度の大きいテストしかできないと、 問題が起きたときに問題箇所の特定が難 しくなる 世の中綺麗なことばかりではない。 副作用をモックしないと粒度の細かいテス トはできない 8 / 31
  9. 動的言語での例 Ruby での例 d e s c r i b

    e ' a c t i o n ' d o i t ' r e t u r n [ ] i f t i m e l i n e i s e m p t y ' d o # s t u b e x p e c t ( b o t . s e r v i c e ) . t o r e c e i v e ( : g e t _ t i m e l i n e ) . w i t h ( 1 0 ) . a n d _ r e t u r n ( [ ] ) e x p e c t ( b o t . a c t i o n ( 1 0 ) ) . t o e q [ ] e n d e n d 後から簡単に挙動を変えられるが、 静的言語ではそうはいかない 9 / 31
  10. 改善案: メソッドを引数に渡してみる f u n c ( t * T

    w i t t e r B o t ) E v e n t L o o p ( t w e e t f u n c ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) , g e t T i m e l i n e f u n c ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) , ) e r r o r { . . . } f u n c m a i n ( ) { b o t : = & T w i t t e r B o t { . . . } b o t . E v e n t L o o p ( T w e e t , G e t T i m e l i n e ) } こうすれば、 テスト時に t w e e t と g e t T i m e l i n e の挙動を変えることができる モックして単体テストできる 究極的には、 依存しているメソッドを全部引数に渡せばいい でも面倒 この例だと E v e n t L o o p が1 回しか呼ばれてないからいいけど… なんで呼び出し側がモックしたいメソッドを気にかけてやんなきゃいけな いの? 10 / 31
  11. 改善案: メソッドをまとめたオブジェ クトを渡してみる t y p e T w i

    t t e r S e r v i c e s t r u c t { } f u n c ( t * T w i t t e r S e r v i c e ) T w e e t ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) { . . . } f u n c ( t * T w i t t e r S e r v i c e ) G e t T i m e l i n e ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) { . . . } f u n c ( t * T w i t t e r B o t ) E v e n t L o o p ( s e r v i c e * T w i t t e r S e r v i c e ) e r r o r { . . . } f u n c m a i n ( ) { b o t : = & T w i t t e r B o t { . . . } s e r v i c e : = & T w i t t e r S e r v i c e { . . . } b o t . E v e n t L o o p ( s e r v i c e ) } 呼び出しは少し楽になったけど、 モックができない T w i t t e r S e r v i c e という実態に依存してしまっている 差し替えのためには実態でなく型シグネチャ ( インター フェー ス) を指定しなけ ればいけない 11 / 31
  12. 改善案: 依存しているインター フェー スを渡してみる t y p e T w

    i t t e r S e r v i c e i n t e r f a c e { T w e e t ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) G e t T i m e l i n e ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) } t y p e T w i t t e r S e r v i c e I m p l s t r u c t { } f u n c ( t * T w i t t e r S e r v i c e I m p l ) T w e e t ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) { . . . } f u n c ( t * T w i t t e r S e r v i c e I m p l ) G e t T i m e l i n e ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) { . . . } f u n c ( t * T w i t t e r B o t ) E v e n t L o o p ( s e r v i c e T w i t t e r S e r v i c e ) e r r o r { . . . } f u n c m a i n ( ) { b o t : = & T w i t t e r B o t { . . . } s e r v i c e : = & T w i t t e r S e r v i c e I m p l { . . . } b o t . E v e n t L o o p ( s e r v i c e ) } 差し替えがきくようになった 「T w i t t e r S e r v i c e のメソッドを実装しているものをちょうだい」 と言って いるだけで、 具体的な実装は指定していない ついでにこれオブジェクトの初期化時に渡して保持しとけばいいよね 12 / 31
  13. 改善案: 初期化時に渡してみる t y p e T w i t

    t e r S e r v i c e i n t e r f a c e { T w e e t ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) G e t T i m e l i n e ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) } t y p e T w i t t e r S e r v i c e I m p l s t r u c t { } f u n c ( t * T w i t t e r S e r v i c e I m p l ) T w e e t ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) { . . . } f u n c ( t * T w i t t e r S e r v i c e I m p l ) G e t T i m e l i n e ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) { . . . } t y p e T w i t t e r B o t s t r u c t { T w i t t e r S e r v i c e T w i t t e r S e r v i c e , . . . } f u n c ( t * T w i t t e r B o t ) E v e n t L o o p ( ) e r r o r { . . . } f u n c m a i n ( ) { s e r v i c e : = & T w i t t e r S e r v i c e I m p l { . . . } b o t : = & T w i t t e r B o t { s e r v i c e , . . . } b o t . E v e n t L o o p ( ) } 依存オブジェクトの差し替えはきくし、 呼び出しも簡単 このように、 依存しているオブジェクトを具体的に保持せず、 初期化時に注入 する手法を Dependency Injection (DI) という 13 / 31
  14. 改善案: 徹底的にやる 本当は T w i t t e r

    B o t も別のオブジェクトに依存しているかもしれない。 T w i t t e r B o t もインター フェー スと実装を分けておく t y p e T w i t t e r S e r v i c e i n t e r f a c e { T w e e t ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) G e t T i m e l i n e ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) } t y p e T w i t t e r S e r v i c e I m p l s t r u c t { } f u n c ( t * T w i t t e r S e r v i c e I m p l ) T w e e t ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) { . . . } f u n c ( t * T w i t t e r S e r v i c e I m p l ) G e t T i m e l i n e ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) { . . . } t y p e T w i t t e r B o t i n t e r f a c e { . . . } t y p e T w i t t e r B o t I m p l s t r u c t { T w i t t e r S e r v i c e T w i t t e r S e r v i c e , . . . } f u n c ( t * T w i t t e r B o t I m p l ) E v e n t L o o p ( ) e r r o r { . . . } f u n c m a i n ( ) { s e r v i c e : = & T w i t t e r S e r v i c e I m p l { . . . } b o t : = & T w i t t e r B o t I m p l { s e r v i c e , . . . } . . . / / 他の T w i t t e r B o t を使うオブジェクトを生成 b o t . E v e n t L o o p ( ) } 14 / 31
  15. インター フェー スに対するプログラミ ング 依存しているオブジェクトに対してメソッドを呼ぶのではなく、 インター フェ ー スに対してメソッドを呼ぶべき インター

    フェー スに対してメソッドを呼べるような設計を心がける 15 / 31
  16. 今日の話 Dependency Injection アプリケー ション初期化と DI コンテナ インター フェー ス実装によるモックとモックライブラリ

    16 / 31
  17. アプリケー ション初期化問題 各モジュー ルでインター フェー スに対するプログラミングはできたが、 オブジ ェクトの実態はどこにあるのか? アプリケー ション起動時に実際にオブジェクトを生成し、

    順番に依存オブジェ クトを注入してやる必要がある アプリケー ションの初期化 注入されるオブジェクト間の依存性は有向無閉路グラフ (Directed Acyclic Graph, DAG) として表される DAG といえばトポロジカルソー ト UNIX にはトポロジカルソー トするための t s o r t というコマンドがあ ります ( これ書いてて知った!) 17 / 31
  18. 依存グラフとトポロジカルソー ト $ c a t d a g .

    i n T w i t t e r S e r v i c e T w i t t e r B o t A p p l i c a t i o n L o g g e r T w i t t e r S e r v i c e T h r e a d P o o l T w i t t e r B o t U s e r S t r e a m S e r v i c e T w i t t e r S e r v i c e U s e r C o n f i g A p p l i c a t i o n L o g g e r T w i t t e r C r e d e n t i a l T w i t t e r S e r v i c e A p i P a r s e r T w i t t e r S e r v i c e $ t s o r t < d a g . i n A p i P a r s e r T h r e a d P o o l T w i t t e r C r e d e n t i a l U s e r C o n f i g U s e r S t r e a m S e r v i c e A p p l i c a t i o n L o g g e r T w i t t e r S e r v i c e T w i t t e r B o t 18 / 31
  19. 初期化コー ド f u n c m a i n

    ( ) { a p i P a r s e r : = & A p i P a r s e r I m p l { . . . } t h r e a d P o o l : = & T h r e a d P o o l I m p l { . . . } t w i t t e r C r e d e n t i a l : = & T w i t t e r C r e d e n t i a l I m p l { . . . } u s e r C o n f i g : = & U s e r C o n f i g I m p l { . . . } u s e r S t r e a m S e r v i c e : = & U s e r S t r e a m S e r v i c e I m p l { . . . } a p p l i c a t i o n L o g g e r : = & A p p l i c a t i o n L o g g e r I m p l { u s e r C o n f i g , . . . } t w i t t e r S e r v i c e : = & T w i t t e r S e r v i c e I m p l { t w i t t e r C r e d e n t i a l , a p p l i c a t i o n L o g g e r , u s e r S t r e a m S e r v i c e , a p i P a r s e r , . . . } t w i t t e r B o t : = & T w i t t e r B o t I m p l { u s e r C o n f i g , t w i t t e r S e r v i c e , t h r e a d P o o l , . . . } t w i t t e r B o t . E v e n t L o o p ( ) } めんどくさいしミスりそう 19 / 31
  20. DI コンテナ 依存性を管理していい感じに扱うためのライブラリ Google Guice (Java) など https://github.com/google/guice Motivation の記事はとてもよくまとまっているので読むといい

    https://github.com/google/guice/wiki/Motivation p u b l i c c l a s s B i l l i n g M o d u l e e x t e n d s A b s t r a c t M o d u l e { @ O v e r r i d e p r o t e c t e d v o i d c o n f i g u r e ( ) { b i n d ( T r a n s a c t i o n L o g . c l a s s ) . t o ( D a t a b a s e T r a n s a c t i o n L o g . c l a s s ) ; b i n d ( C r e d i t C a r d P r o c e s s o r . c l a s s ) . t o ( P a y p a l C r e d i t C a r d P r o c e s s o r . c l a s s ) ; b i n d ( B i l l i n g S e r v i c e . c l a s s ) . t o ( R e a l B i l l i n g S e r v i c e . c l a s s ) ; } } p u b l i c c l a s s R e a l B i l l i n g S e r v i c e i m p l e m e n t s B i l l i n g S e r v i c e { p r i v a t e f i n a l C r e d i t C a r d P r o c e s s o r p r o c e s s o r ; p r i v a t e f i n a l T r a n s a c t i o n L o g t r a n s a c t i o n L o g ; @ I n j e c t p u b l i c R e a l B i l l i n g S e r v i c e ( C r e d i t C a r d P r o c e s s o r p r o c e s s o r , T r a n s a c t i o n L o g t r a n s a c t i o n L o g ) { t h i s . p r o c e s s o r = p r o c e s s o r ; t h i s . t r a n s a c t i o n L o g = t r a n s a c t i o n L o g ; } } 20 / 31
  21. facebookgo/inject https://github.com/facebookgo/inject Go 用の DI コンテナ タグを抽出して自動で依存グラフを作り、 注入 リフレクションごりごり… 紹介記事

    http://blog.parse.com/2014/05/13/dependency-injection-with-go/ 21 / 31
  22. 私見 implicit なものが大嫌い アプリケー ションの初期化もプログラマが自分でどの順番でやるべきかを 指定するべき 黒魔術的なマクロやシンタックスシュガー など使わず、 明示的に自分で書 いていくほうが読みやすい

    3 万行くらいまでなら実証済み 22 / 31
  23. 今日の話 Dependency Injection アプリケー ション初期化と DI コンテナ インター フェー ス実装によるモックとモックライブラリ

    23 / 31
  24. モック テスト時に依存オブジェクトの挙動を一時的に変更したい モック、 スタブ、 スパイ DI で設計してあれば依存性を後から注入できるので簡単にできる 24 / 31

  25. インター フェー ス実装してモック (Java) @ T e s t p

    u b l i c v o i d a c t i o n W i t h E m p t y T i m e l i n e ( ) { T w i t t e r S e r v i c e s e r v i c e = n e w T w i t t e r S e r v i c e ( ) { L i s t < S t a t u s > g e t T i m e l i n e ( i n t c o u n t ) { r e t u r n n e w L i s t < S t a t u s > ( ) ; } i n t t w e e t ( S t r i n g t e x t , i n t i n R e p l y T o ) { t h r o w n e w R u n t i m e E x c e p t i o n ( ) ; } } ; T w i t t e r B o t b o t = n e w T w i t t e r B o t I m p l ( s e r v i c e ) ; a s s e r t T h a t ( b o t . a c t i o n ( 1 0 ) . s i z e , i s ( 0 ) ) ; } 25 / 31
  26. インター フェー ス実装してモック (Go) t y p e M o

    c k S e r v i c e s t r u c t { } f u n c ( x * M o c k S e r v i c e ) G e t T i m e l i n e ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) { r e t u r n m a k e ( [ ] S t a t u s , 0 ) , n i l } f u n c ( t * M o c k S e r v i c e ) T w e e t ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) { p a n i c ( " " ) } f u n c T e s t A c t i o n ( t * t e s t i n g . T ) { s e r v i c e : = & M o c k S e r v i c e { } b o t : = & T w i t t e r B o t I m p l { s e r v i c e } t l , _ : = b o t . A c t i o n ( 1 0 ) i f l e n ( t l ) ! = 0 { t . E r r o r ( " f a i l " ) } } Go の場合、 どのインター フェー スを実装しているかを明示的に書かない ダックタイピング Structural Subtyping もし Go に匿名インター フェー ス構文があれば次のように書ける s e r v i c e : = & i n t e r f a c e { G e t T i m e l i n e ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) { r e t u r n m a k e ( [ ] S t a t u s , 0 ) , n i l } T w e e t ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) { p a n i c ( " " ) } } 26 / 31
  27. 問題点 「 メソッドの挙動を変える」 以外の細かいところが地味に面倒 1 回目の呼び出しと2 回目の呼び出しで挙動を変える 呼び出し順番チェック 呼び出し回数チェック 使わないメソッドについてもいちいち実装を指定してあげる必要がある

    とりあえず p a n i c ( " " ) でいいんだけど 27 / 31
  28. モックライブラリ gomock https://code.google.com/p/gomock/ 使い方がわかりそうでわかりづらいライブラリ 完全にドキュメント不足 使い方の例: https://github.com/daimatz/gomock_example 各コミットで動くようになっているので試してみてください withmock というやつもある

    https://github.com/qur/withmock gomock のラッパー っぽい 試せていない 28 / 31
  29. gomock 使い方 $ m k d i r $ G

    O P A T H / s r c / g i t h u b . c o m / d a i m a t z / g o m o c k _ u s a g e # リポジトリ名。 なんでもいい $ c d $ G O P A T H / s r c / g i t h u b . c o m / d a i m a t z / g o m o c k _ u s a g e $ m k d i r a # ディレクトリ名。 なんでもいい $ c a t > a / b . g o # ファイル名。 なんでもいい p a c k a g e a / / ディレクトリ名と同じ t y p e I i n t e r f a c e { F ( ) s t r i n g } / / 名前に制限あり? X だとダメだった $ m k d i r m o c k _ a # m o c k _ ディレクトリ名 $ m o c k g e n g i t h u b . c o m / d a i m a t z / g o m o c k _ u s a g e / a \ # リポジトリ名/ ディレクトリ名 I > m o c k _ a / x . g o # インター フェー ス名 > m o c k _ ディレクトリ名/ なんでもいい $ c a t > a / b _ t e s t . g o # ファイル名_ t e s t . g o p a c k a g e a _ t e s t / / ディレクトリ名_ t e s t i m p o r t ( " t e s t i n g " " c o d e . g o o g l e . c o m / p / g o m o c k / g o m o c k " " g i t h u b . c o m / d a i m a t z / g o m o c k _ u s a g e / m o c k _ a " / / リポジトリ名/ m o c k _ ディレクトリ名 ) f u n c T e s t I ( t * t e s t i n g . T ) { c t r l : = g o m o c k . N e w C o n t r o l l e r ( t ) d e f e r c t r l . F i n i s h ( ) i : = m o c k _ a . N e w M o c k I ( c t r l ) / / N e w M o c k インター フェー ス名 i . E X P E C T ( ) . F ( ) . R e t u r n ( " m o c k e d ! " ) i f i . F ( ) ! = " m o c k e d ! " { t . E r r o r ( " f a i l e d " ) } } $ c d a ; g o t e s t 29 / 31
  30. 他言語のモックライブラリとの比較 Java などのモックライブラリと比べるとかなり面倒 Mockito の例 T w i t t

    e r S e r v i c e s e r v i c e = M o c k i t o . m o c k ( T w i t t e r S e r v i c e . c l a s s ) ; M o c k i t o . w h e n ( s e r v i c e . g e t T i m e l i n e ( 1 0 ) ) . t h e n R e t u r n ( n e w L i s t ( ) ) ; Go にはジェネリクスがなく、 インター フェー スが第一級でないため、 ソー ス コー ド内で簡単にモックを生成することができない? s e r v i c e : = g o m o c k . m o c k [ T w i t t e r S e r v i c e ] ( ) / / ジェネリクスがない s e r v i c e : = g o m o c k . m o c k ( T w i t t e r S e r v i c e ) / / インター フェー ス名を渡せない 30 / 31
  31. まとめ Dependency Injection からモックライブラリの嬉しさまで一周りしました テスタブルな設計を目指しましょう gomock は難しい 31 / 31