Mais conteúdo relacionado Semelhante a 継続的にテスト可能な設計を考える (20) Mais de Atsushi Nakamura (16) 継続的にテスト可能な設計を考える2. About Me
Copyright 2017 @nuits_jp Slide 2
中村 充志 / Atsushi Nakamura
• リコージャパン株式会社 金融事業部所属
• Enterprise系SIerのITアーキテクト
• JavaからC#へ渡り歩く
• 趣味はXamarin?
• Blog http://www.nuits.jp
• Blog(英語) https://blog.nuits.jp
• Twitter @nuits_jp
6. Today’s Goal
Slide 6Copyright 2017 @nuits_jp
継続的にテスト可能な設計を実現するには多くのエッセンスが必要です。
今日はそのうち、つぎの3つについてお話します。
1. 制御の流れと依存方向の分離
2. 依存方向の制御と、安定性と柔軟性の管理
3. 現実的なテスト戦略を考える(結論はこの場ではでない)
「継続的なテストの維持」に必要な一部のエッセンスですが、非常に重要なことです。
9. • 対象システムは次のような特徴をもちます
• プロダクト別の総売上をCSV出力するコンソール アプリを想定
• 同一のデータベースを他の機能からも利用している
Overview
Copyright 2017 @nuits_jp Slide 9
SQL Server 2017
AdventureWorks2017
Other Functions
※英語含む、対象コードへのマサカリは「そっと」
Pull Requestを投げるというソフト対応をお願いします
https://github.com/nuitsjp/Continuous-Testable-Design
16. No. 方式 代表的なデザインパターン
1 Controllerが能動的に取得する Service Locator パターン
2 Controllerに外部から注入する Dependency Injection パターン
インスタンス生成を取り除く二つの方式
Copyright 2017 @nuits_jp Slide 16
基本的にいずれかに類似した方式をとります。
ここではDependency Injectionパターンを利用します。
Service Locator is an Anti-Pattern by Mark Seemann
http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/
• Service Locatorはstaticなレジストリなので並行テストが困難
• ControllerからBusinessLogicへ依存がなくなる代わりに、Service Locatorへ依存が増える
19. テスト ダブルの利用
Copyright 2017 @nuits_jp Slide 19
IBusinessLogic businessLogic =
newTestDouble();
var controller =
new Controller(businessLogic);
controller.Execute("output.csv");
22. 結果、こんな感じでテストできます
Copyright 2017 @nuits_jp Slide 22
単体テスト用DB
class Testable Models
Repositoryの単体テスト
BusinessLogicの単体テスト
Controllerの単体テスト
Controller «interface»
IBusinessLogic
BusinessLogic «interface»
IRepository
Repository
ControllerFixture
BusinessLogicMock
BusinessLogicFixture
RepositoryFixture
RepositoryMock
30. class Testable Models
Repositoryの単体テスト
BusinessLogicの単体テスト
Controllerの単体テスト
Controller «interface»
IBusinessLogic
BusinessLogic «interface»
IRepository
Repository
ControllerFixture
BusinessLogicMock
BusinessLogicFixture
RepositoryFixture
RepositoryMock
つまり参照してるテーブルが変更されると…
Copyright 2017 @nuits_jp Slide 30
単体テスト用DB
Breaking
Change!
Breaking
Change!
Breaking
Change!
Breaking
Change!
Breaking
Change!
Breaking
Change!
Breaking
Change!
Breaking
Change!
Breaking
Change!
Breaking
Change!
Breaking
Change!
32. 何が悪いのか?
Copyright 2017 @nuits_jp Slide 32
class BusinessLogic and Repository
BusinessLogics Repositories
BusinessLogic «interface»
IRepository
Repository
制御の流れに
引きずられて
安定させたい
モジュール
不安定な
モジュール
依存して
しまっている
33. 解決策
Copyright 2017 @nuits_jp Slide 33
class BusinessLogic and Repository
BusinessLogics Repositories
BusinessLogic «interface»
IRepository
Repository
制御の流れから
依存方向を分離し
逆方向へ依存させる
37. IRepositoryのクラス図とER図
Copyright 2017 @nuits_jp Slide 37
class Repository Details
«interface»
IRepository
+ GetProducts(): IEnumerable<Product>
+ GetSalesOrderDetail(): IEnumerable<SalesOrderDetail>
SalesOrderDetail
«property»
+ CarrierTrackingNumber(): string
+ LineTotal(): decimal
+ ModifiedDate(): DateTime
+ OrderQty(): short
+ ProductID(): int
+ rowguid(): Guid
+ SalesOrderDetailID(): int
+ SalesOrderID(): int
+ SpecialOfferID(): int
+ UnitPrice(): decimal
+ UnitPriceDiscount(): decimal
Product
«property»
+ Class(): string
+ Color(): string
+ DaysToManufacture(): int
+ DiscontinuedDate(): DateTime?
+ FinishedGoodsFlag(): bool
+ ListPrice(): decimal
+ MakeFlag(): bool
+ ModifiedDate(): DateTime
+ Name(): string
+ ProductID(): int
+ ProductLine(): string
+ ProductModelID(): int?
+ ProductNumber(): string
+ ProductSubcategoryID(): int?
+ ReorderPoint(): short
+ rowguid(): Guid
+ SafetyStockLevel(): short
+ SellEndDate(): DateTime?
+ SellStartDate(): DateTime
+ Size(): string
+ SizeUnitMeasureCode(): string
+ StandardCost(): decimal
+ Style(): string
+ Weight(): decimal?
+ WeightUnitMeasureCode(): string
dm SalesOrderDetail and Product
Product
«column»
*PK ProductID: int
* Name: nvarchar(50)
* ProductNumber: nvarchar(25)
* MakeFlag: bit = 1
* FinishedGoodsFlag: bit = 1
Color: nvarchar(15)
* SafetyStockLevel: smallint
* ReorderPoint: smallint
* StandardCost: money
* ListPrice: money
Size: nvarchar(5)
FK SizeUnitMeasureCode: nchar(3)
FK WeightUnitMeasureCode: nchar(3)
Weight: decimal(8,2)
* DaysToManufacture: int
ProductLine: nchar(2)
Class: nchar(2)
Style: nchar(2)
FK ProductSubcategoryID: int
FK ProductModelID: int
* SellStartDate: datetime
SellEndDate: datetime
DiscontinuedDate: datetime
* rowguid: uniqueidentifier = newid()
* ModifiedDate: datetime = getdate()
SalesOrderDetail
«column»
*pfK SalesOrderID: int
*PK SalesOrderDetailID: int
CarrierTrackingNumber: nvarchar(25)
* OrderQty: smallint
*FK ProductID: int
*FK SpecialOfferID: int
* UnitPrice: money
* UnitPriceDiscount: money = 0.0
* LineTotal: numeric(38,6)
* rowguid: uniqueidentifier = newid()
* ModifiedDate: datetime = getdate()
クラス図 ER図
IRepositoryが完全にデータベースの文脈で記述されているのが見て取れる
38. BusinessLogicのインターフェースと比較する
Copyright 2017 @nuits_jp Slide 38
dm BusinessLogic
«interface»
IBusinessLogic
+ GetProductSalesList(): IEnumerable<ProductSales>
ProductSales
«property»
+ Name(): string
+ Sales(): decimal
プロダクト別の総売上額が欲しいだけ
class Repository Details
«interface»
IRepository
+ GetProducts(): IEnumerable<Product>
+ GetSalesOrderDetail(): IEnumerable<SalesOrderDetail>
SalesOrderDetail
«property»
+ CarrierTrackingNumber(): string
+ LineTotal(): decimal
+ ModifiedDate(): DateTime
+ OrderQty(): short
+ ProductID(): int
+ rowguid(): Guid
+ SalesOrderDetailID(): int
+ SalesOrderID(): int
+ SpecialOfferID(): int
+ UnitPrice(): decimal
+ UnitPriceDiscount(): decimal
Product
«property»
+ Class(): string
+ Color(): string
+ DaysToManufacture(): int
+ DiscontinuedDate(): DateTime?
+ FinishedGoodsFlag(): bool
+ ListPrice(): decimal
+ MakeFlag(): bool
+ ModifiedDate(): DateTime
+ Name(): string
+ ProductID(): int
+ ProductLine(): string
+ ProductModelID(): int?
+ ProductNumber(): string
+ ProductSubcategoryID(): int?
+ ReorderPoint(): short
+ rowguid(): Guid
+ SafetyStockLevel(): short
+ SellEndDate(): DateTime?
+ SellStartDate(): DateTime
+ Size(): string
+ SizeUnitMeasureCode(): string
+ StandardCost(): decimal
+ Style(): string
+ Weight(): decimal?
+ WeightUnitMeasureCode(): string
40. リファクタリング後のIRepository
Copyright 2017 @nuits_jp Slide 40
dm Repositories
ProductName
«property»
+ Name(): string
+ ProductId(): int?
SalesLineTotal
«property»
+ LineTotal(): double
+ ProductId(): int
«interface»
IRepository
+ GetProductNames(): IEnumerable<ProductName>
+ GetSalesLineTotal(): IEnumerable<SalesLineTotal>
43. class BusinessLogics and Re...
Repositories
BusinessLogics
BusinessLogic
«interface»
IRepository
Repository
依存方向によって決まる、安定性と柔軟性
Copyright 2017 @nuits_jp Slide 43
Repository変更の影響を受けない ⇒ 安定性が高い
変更がRepositoryへ影響を与える ⇒ 柔軟性が低い
BusinessLogic変更の影響を受ける ⇒ 安定性が低い
変更がBusinessLogicへ影響を与えない ⇒ 柔軟性が高い
安定性と柔軟性は設計上トレードオフにある
55. ソフトウェア エンティティ テスト価値 テスト難易度 障害発見
システム 高 高 遅い
サブシステム
コンポーネント
クラス 低 低 早い
ソフトウェア エンティティ別のテスト価値とテスト難易度
Copyright 2017 @nuits_jp Slide 55
59. What システム サブシステム コンポーネント クラス
Why
Who
When
Where
Hoe
How many
How Much
テスト戦略の立案
Copyright 2017 @nuits_jp Slide 59
先ほどのソフトウェア エンティティに対してmWnHで立てます
60. What システム
Why ビジネス シナリオ(業務)の実現性評価
Who 開発サイドのテストチーム
When 評価可能になり次第随時
Where システムテスト環境(Not顧客環境)
How ビジネス シナリオに則って手動で
How many 想定されるすべてのビジネスシナリオ
How Much 全ビジネスシナリオで〇〇人月
テスト戦略:システム
Copyright 2017 @nuits_jp Slide 60
62. まとめ
Slide 62Copyright 2017 @nuits_jp
1. 制御の流れに流されず、適切な依存方向にコントロールする
2. 依存方向によって安定性と柔軟性を、計画的に制御しましょう
3. 多面的な要素を考慮し、現実的なテスト戦略を立てましょう
Notas do Editor みなさんこんにちは。
ご紹介いただきました。ニュイこと中村です。
今日は
継続的にテスト可能な設計を考える
というTitleでお話しさせていただこうと思います。
よろしくお願いいたします。 まずは自己紹介から
中村充志と申します。
リコージャパン株式会社の金融ソリューション開発部
というところに所属しています。
Enterprise系SIerでITアーキテクトをやらせてもらっています。
ところで皆さん、日常的に自動テスト書いてますか?
私は結構書いてます。
SIerのテストというと、Excel!スクショ!エビデンス!的なイメージがあるかも知れませんが
たまたま巡り合わせが良くて、2000年からJUnitを触っていたと思います。
SIerって、プロダクトチームの維持が難しくて、半年ごとに8割のメンバーが入れ替わってる
みたいな地獄みたいな事が良くあるので、自動テストが無いと怖くて生きていけない
というのが、私のテストのモチベーションなんだと思います。
ただ、テストを書くのは良いんですが、本当に難しいのはプロダクトの改修に追随して
テストを維持していくことだと、私は思っています。 テストを維持し続けるのは本当に難しいです。
プロジェクトのスケジュールが厳しくなると、すぐ悪魔が囁き始めますし
テストを維持する事自体が、ソフトウェアの変更を阻害するようなこともあります。
実際に、テストをいくらか破棄して泣く泣く規模を縮小したりしたこともあります。 という訳で、本日のゴールです。 今日は、そういった茨の道で試行錯誤してきたベストプラクティスの中から
特に大切だと思っている、三つのエッセンスについてお話ししたいと思います。
①一つは制御の流れと依存性の分離について
②二つ目は依存方向の制御と、安定性と柔軟性の管理について
③そして最後に現実的なテスト戦略について
です。当然これだけで継続的なテストの維持ができるようになる訳ではありませんが、特に重要なことだと私は思っていますのでお付き合いください。
このセッションの概要ですが 実際のコードを見ながら、最初はテスト不可能なコードからスタートして
リファクタリングしながら、継続的にテスト可能な設計を目指したいと思います。
なお、テストの対象は今回はクラス単位のテストをする前提とさせていただきます。 実際の例に用いるプログラムですが、SQL ServerのサンプルDBのAdventureWorksを
利用させてもらって、プロダクト別の総売上をCSV出力するような
コンソールアプリを想定します。
そのアプリから利用するデータベースは、同一システムの別機能からも利用されるものとします。
コードはこちらのURLに公開しています。
なおコードや、コードの英語に不備不満のある方は、そっとプルリクを送っていただけると助かります。
まえに「その単語、不可算名詞だからsつくのおかしくね?」みたいなマサカリをぶん投げてきた人がいるんですが
なるべく、そっとやさしくお願いします。
今日は強そうな人が多いので、重々よろしくお願いいたします。 では早速、本題に入りたいと思います。 まずはコードを見てください。 ①
さて、今見ていただいたコードですが
クラス図に落とすと、こんな感じです。
今日のテスト対象はクラスですから
②
この辺のクラス間が直接依存してるせいで
上流のクラスが単体テストできない状態になっています。 例えばControllerとBusinessLogicの関係の詳細を見てみると
その二つの間には、オブジェクトの生成と利用という2種類の依存関係があります。
これらが、Controllerをテストダブルを利用して単体テストすることを阻んでいます。
という訳で、これらのクラス間の直接的な依存関係を取り除いていきます。 まずはインターフェースを抽出します。
ここからは、またコードに戻ります。 さて、こうしたことで、BusinessLogicを利用している箇所の依存関係は
クラスからインターフェース移すことができました。
ただし、インスタンスを生成する個所に依存がまだ残っています。 インスタンス生成の依存関係を取り除くには、基本的にはインスタンスの生成を
つぎの二つの何れかの方法で解決する必要があります。
一つはControllerが能動的に、インスタンスをいずれかのレジストリーに取得しに行く方法。
もう一つは依存オブジェクトをControllerの外から、誰かに注入してもらう方法です。
それぞれ代表的な実現方法として、ServiceLocatorパターンとDependency Injectionパターンがあります。
今回はDependency Injectionパターン、つまりDIを利用します。
なぜDIを利用するかは、ざっくりいうと、ServiceLocatorには大きな問題が二つあって
一つはテストをマルチスレッドで実装しにくいということと
ServiceLocatorパターンにするとBusinessLogicへの依存は減るけど、ServiceLocator
への依存が増えて、依存数が減らないという欠点があるためです。
という訳でBusinessLogicをインジェクションするように修正します。 というわけで、無事にControllerからインスタンス生成のロジックも除去出来て
完全にクラス間の依存関係を無くせました。 こうする事で、簡単にBusinessLogicをテストダブルに置き換えが可能になって
テスタブルになります。
ちなみにDIパターンとDIコンテナが混同されているケースがありますが
DIコンテナはあくまでDIパターンを便利に実現するためのツールなので
必ずしも使わなくてもDIパターンは実現できます。普通は使いますけど。 では他の部分もテスタブルにしましょう! はいできました!すいません、時間がないので3分間クッキング方式を
取らせていただきます。
View、BusinessLogic、Repositoryの全てに
インターフェースを導出し、DIを適用します。
こうする事で こんな感じで、全てのクラスにたいしてクラス単位のテストが実施できるようになりました。 ところでこのクラス図
良く見ると不吉なにおいがしますね? 特にこの、ControllerとViewの間です。 ①ControllerがViewに依存している部分です。
②一般的にViewは最も変化が多いと言われていますよね。
それが正しいとすると
③Viewに依存しているControllerはViewに引きずられて
頻繁に変更する必要がでてきます
④結果、プロダクションコードだけでなく、テストコードもテストダブルも
頻繁に変更しなくてはならなくなります
⑤これは辛いです そしてもう一つ
個人的にはこっちの方が嫌な予感がします。 このBusinessLogicがRepositoryに依存しているところです。 最初にお話しした通り、このシステムで利用するデータベースは
他の機能からも利用されます。
ということは、このシステムではない、別の機能起因で変更が入る可能性があります。
そもそもデータが安定的であるというのも、私は懐疑的です。
現代において企業は、顧客に対して新しい価値を次々提供し続けなくてはなりません。
新しい価値を提供するためには、往々にして新しいデータが発生します。
データの取り扱いが無くなる事は少なくても、追加変更はそれなりに発生するのが現実だと
私は思っています。 ①つまり、何らかの要因で参照しているテーブルに変更が発生すると
②リポジトリが変更されます
③当然そうなるとテストケースの修正が入って
④高い確率でリポジトリのインターフェースが変更されます
⑤するとビジネスロジックが影響を受けて
⑥リポジトリのモックとビジネスロジックのテストががががが
全部なおさなきゃ!
最悪だ!と、なってしまうかもしれないです。
実際稀に良くありますし。 つまり何が悪いかというと
①安定させたいビジネスロジックが
②不安定なモジュールにたいして
③制御の流れが原因で引きずられて
④依存してしまっているのが、この問題の根幹にあります
これを解決するには 制御の流れから依存方向を分離して
①逆方向に依存させれば解決できます もちろん実現可能です 具体的には、今こうなっているのを
①こうします。重要なのは、インターフェースを移動することではなくて
②リポジトリをビジネスロジックの文脈で定義することです リポジトリの詳細を見てみましょう 現在の実装は、リポジトリが完全にデータベースの文脈で記述されているのが見て取れます。
しかし、ビジネスロジックで実現したいのは、プロダクト別の総売上額です。
①つまりビジネスロジックに必要なのはこの4項目だけです。
という訳で、リポジトリインターフェースを
ビジネスロジックの文脈へリファクタリングします 結果、こうなりました。
データベースの詳細が隠蔽されているのが
分かるかと思います。 一旦整理しましょう。 制御の流れと、依存方向は、必ずしも一致させる必要はありません。
クラスとクラスの直接依存を避けて、インターフェースを定義し、疎結合にした上で
インターフェースを依存させたい側の文脈で定義することで
依存方向は制御の流れから分離して、自由にコントロールできます。 そして、依存方向によって安定性と柔軟性が変化します。
リポジトリインターフェースは、ビジネスロジックの文脈で記述されています。
①つまり、データベースの影響を受けにくく、安定性が高くなります。
しかし逆にいうと、ビジネスロジックを変更すると、リポジトリの実装が影響を受ける可能性があるため
ビジネスロジックの柔軟性は低くなります。
②逆にリポジトリの実装はビジネスロジックの変更の影響を受ける為、安定性は低くなります。
しかし、リポジトリの変更はビジネスロジックへ影響を与えにくいため、変更しやすく、柔軟性が高いといえます。
③つまり、安定性と柔軟性は設計上のトレードオフにあるわけです。
あらためて全体を見てみましょう。 クラスを書くと煩雑なので、パッケージだけ記述しました。
最初の設計では制御の流れと依存関係が一致していたため
それぞれのパッケージの安定性と柔軟性は、この図のような
状態にあります。
①ビューとリポジトリが一番安定している。つまり変更しにくい状態にあり
それらを修正すると、システム全体への影響が大きい状態に
なってしまっています。 実際には安定性と柔軟性は、だいたいこんな感じにしたいとします。
ビジネスロジックを最も安定させ
ビューとレポジトリーは柔軟性を高めたい、つまり変更しやすくしたいとします。 その為にはコントローラーとビュー、ビジネスロジックとリポジトリの間の
依存関係を逆転してあげれば、望む通りの安定性と柔軟性を
得ることができるわけです。 これで変更を受けやすい部分の柔軟性が高くなり、それ以外への影響が
およびにくくなりました。
つまり、テストコードへの影響も局所化できて、継続的にテストをしやすい状態に
なったはずです。
なったんですが、そこで疑問が発生します。 全部テストできるからと、すべてのクラスを同じレベルでテストする必要があるんでしょうか? 当たり前のことですが、重要なクラスの安定性が高くなるようにコントロールすることで
重要なクラスへのテストコードの寿命は延びます。
逆に言うと、安定性の低いクラスのテストコードの寿命は短くなります。 また、この表はあくまで例ですが、テスト価値と、テストの難易度には一貫性がないのではないかと感じています。
重要な部分はテストしにくい部分から切り離す努力はしますが、かならずしもできるとは限りません。
例えばViewなんかは安定性が低いわりに、テスト難易度が高かったりして、きっと皆さん思いますよね? Viewのテストしたくねえなあって。
また別の視点もあります
そもそも、クラス単体でテストする価値ってどのくらいあるんでしょうか? ①ソフトウェアはクラスとクラスがまとまってコンポーネントとなり
②コンポーネントとコンポーネントがまとまってサブシステムになります。そして
③サブシステムとサブシステムがまとまってシステムになります。
④ここではクラス・コンポーネント・サブシステム・システムを、ソフトウェア エンティティと呼ぶことにします。
⑤ソフトウェアエンティティのすべてにおいて、ここまでした話の内容は適用できます。
もちろんサブシステム間は例えばWeb APIになったりするでしょうけど
どの要素間であっても、インターフェースの文脈を管理する事で
制御の流れと依存関係は制御することが可能です。
そして依存関係を制御することで、安定性と柔軟性をコントロールすることが可能となります そしてソフトウェア エンティティのレベルが高くなれば高くなるほど
それらに対するテストの価値は高まります。
ただし、難易度も併せて高くなります。
クラスが動いても、システムが動かなければ価値を生まないので当たり前ですよね。
ただし、テストの比率を上位レイヤーに置くと、障害の発見が遅れます。
さらにここでジレンマが発生します。 ①ソフトウェアの品質を上げたいという要求があったら
②通常、テストを増やして品質をあげますよね。ところがテストを増やすと
③当然ながら、テスト維持コストが上昇します。その結果
④テストが維持できなくなります。その結果
⑤品質が下がり
そして最初にもどるという。
つまり継続的にテストをするには、テスト技術だけじゃどうにもなんないわけです。 我々には計画的なテスト戦略が絶対に必要です。
ではテスト戦略ってどうたてればいいのか。
ここからは完全にオレオレ方式というか、弊社方式オレ編みたいなもんですが テスト戦略の立案には
①プロジェクト計画や②ビジネスユースケースだったり③システムユースケース④機能要件や⑤非機能要件⑥システムアーキテクチャ
⑦などなどをインプットにして立案していきます
で、どんな内容を計画していくかというと
インプットになった情報をもとに、どの要件をどのレイヤーで、どうテストするか
この表みたいな感じで例えばここでは5W3Hなどで整理していくと
うまく行くことが多いです。 もうちょっと掘り下げてみてみましょう。
例えばシステムテストであれば、こんな感じです。
システムテストは、ビジネスシナリオの実現性を評価します。
評価は開発サイドのテストチームが、評価可能になったビジネスシナリオから逐次テストします。
システムテスト用の環境でビジネスシナリオに則って手動でテストし、
想定されうるビジネスシナリオすべてに対して行います。
全ビジネスシナリオで〇〇人月投入します。
みたいな感じです。
もちろん、これは方針レベルでしかないものですが、ここから初めて詳細に落としていくやり方をとっています。 とうわけで、最後のテスト戦略に関してはふわっとした物になってしまって申しえ訳ありませんが、私からお伝えしたいことは以上です。
最後に改めてまとめたいと思います。 継続的にテスト可能な設計を目指すには、次の三つのエッセンスが重要だと、私は考えています。
①制御の流れ流されず、適切に依存方向をコントロールする必要があります
②依存方向によって安定性と柔軟性を、計画的に制御しましょう
③そして、そういった技術論だけでは継続的なテストの維持は困難なので
多面的な要素を考慮した、現実的なテスト戦略を立てる必要があるかと思います。
テスト戦略の話は、抽象的な話に終始してしまいましたが
先に話した二つのエッセンスを 以上で私の発表を終わります。
ご清聴ありがとうございました。