ユニットテストの保守性を作りこむ, xpjugkansai2011

16,123 views

Published on

Published in: Technology
2 Comments
68 Likes
Statistics
Notes
No Downloads
Views
Total views
16,123
On SlideShare
0
From Embeds
0
Number of Embeds
481
Actions
Shares
0
Downloads
36
Comments
2
Likes
68
Embeds 0
No embeds

No notes for slide

ユニットテストの保守性を作りこむ, xpjugkansai2011

  1. 1. ユニットテストの保守性を作りこむ ~設計・実装の工夫で支える ユニットテストの継続的活用~ rev.1.1 2011/1/29 @XP祭り関西2011 井芹 洋輝
  2. 2. 謝辞• 登壇者として声をかけていただいた細谷さん、• 機会を提供して頂いたXPJUG関西の皆様• 素晴らしい場を作り上げている登壇者・参加 者・運営者の方々 深くお礼申し上げます。
  3. 3. 自己紹介• 井芹洋輝(いせりひろき)• 扱っているもの – 組込み開発/ソフトウェアテスト/開発者テスト• 所属 – WACATE実行委員/TDD研究会/ATECなど• 対外活動 – JaSST’11 Tokyo/WACATE2011冬/Androidテスト祭り等 – ソフトウェアテストPRESS総集編/Ultimate agile Stories
  4. 4. 概要• ユニットテストの保守性を作りこむアプ ローチを、いくつかの具体例を交えて俯 瞰的に見ていきます
  5. 5. 概要1. 背景 – ユニットテストの現状と課題2. 目標 – ユニットテスト継続活用における目標3. ユニットテストの保守性向上アプローチ – テストコードの実装の工夫 – テストコードの構造設計の工夫 – テスト設計の工夫4. まとめ
  6. 6. 背景
  7. 7. ユニットテストの用途 【現在編】
  8. 8. 軽快なチェック
  9. 9. //コードから無視する文字を削除するstring reformat(string line){ ...(複雑なロジック)...} 複雑なメソッドを書いた きちんと動くか不安だ
  10. 10. //コードから無視する文字を削除するstring reformat(string line){ ...(複雑なロジック)...} 安心できるまでテストを書くTEST(CodeAnalyzer, reformatContainComment){ EXPECT_EQ("#hoge", reformat("#hoge /* fuga */"));}TEST(CodeAnalyzer, reformatContainSpace){ EXPECT_EQ("void function()", reformat("void function ()"));}TEST(CodeAnalyzer, reformatContainEmptyLine){ EXPECT_EQ("test;¥n", reformat("test;¥n¥n¥n¥n"));}…
  11. 11. //コードから無視する文字を削除するstring reformat(string line){ ...(複雑なロジック)...} 安心できるまでテストを書くTEST(CodeAnalyzer, reformatContainComment){ EXPECT_EQ("#hoge", reformat("#hoge /* fuga */"));}TEST(CodeAnalyzer, reformatContainSpace){ EXPECT_EQ("void function()", reformat("void function ()"));}TEST(CodeAnalyzer, reformatContainEmptyLine){ EXPECT_EQ("test;¥n", reformat("test;¥n¥n¥n¥n"));}… リスクや不安を即時に解消して前進できる
  12. 12. リファクタリング
  13. 13. //うるう年か判定するbool isLeapYear(unsigned int year){ if (year % 4 == 0) { if (year % 100 == 0) { 実装が汚い if (year % 400 == 0) { return true; } return false; } return true; } return false;}
  14. 14. 1. テストで保護する TEST(TestYear, isLeapYearDivisibleBy4)//うるう年か判定する {bool isLeapYear(unsigned int year) EXPECT_EQ(true, isLeapYear(8));{ EXPECT_EQ(false, isLeapYear(9)); if (year % 4 == 0) EXPECT_EQ(true, isLeapYear(0)); { } if (year % 100 == 0) { TEST(TestYear, isLeapYearDivisibleBy400) if (year % 400 == 0) { { EXPECT_EQ(true, isLeapYear(800)); return true; EXPECT_EQ(false, isLeapYear(900)); } } return false; } TEST(TestYear, isLeapYearDivisibleBy100) return true; { } EXPECT_EQ(false, isLeapYear(500)); return false; }}
  15. 15. 1. テストで保護する TEST(TestYear, isLeapYearDivisibleBy4) { EXPECT_EQ(true, isLeapYear(8));//うるう年か判定する EXPECT_EQ(false, isLeapYear(9));bool isLeapYear(unsigned int year) EXPECT_EQ(true, isLeapYear(0));{ } if (year % 400 == 0) { TEST(TestYear, isLeapYearDivisibleBy400) return true; { } EXPECT_EQ(true, isLeapYear(800)); if ((year % 4 == 0) && (year % 100 != 0)) EXPECT_EQ(false, isLeapYear(900)); { } return true; } TEST(TestYear, isLeapYearDivisibleBy100) return false; {} EXPECT_EQ(false, isLeapYear(500));2. テストでチェックしつつ }リファクタリング
  16. 16. 1. テストで保護する TEST(TestYear, isLeapYearDivisibleBy4) { EXPECT_EQ(true, isLeapYear(8));//うるう年か判定する EXPECT_EQ(false, isLeapYear(9));bool isLeapYear(unsigned int year) EXPECT_EQ(true, isLeapYear(0));{ } if (year % 400 == 0) { TEST(TestYear, isLeapYearDivisibleBy400) return true; { } EXPECT_EQ(true, isLeapYear(800)); if ((year % 4 == 0) && (year % 100 != 0)) EXPECT_EQ(false, isLeapYear(900)); { } return true; } TEST(TestYear, isLeapYearDivisibleBy100) return false; {} EXPECT_EQ(false, isLeapYear(500));2. テストでチェックしつつ }リファクタリング 安全に記述改善
  17. 17. 学習テスト
  18. 18. boost::xpressive 初使用で よくわからない
  19. 19. boost::xpressiveテストで動作を確認する。不安がなくなるまで試すTEST(regex, regexFileseek){ namespace xp = boost::xpressive; string target; xp::smatch match; xp::sregex rex = xp::sregex::compile ("(¥¥.c|¥¥.h)$"); target = "test"; EXPECT_EQ(false, xp::regex_search(target, match, rex)); target = "test.c"; EXPECT_EQ(true, xp::regex_search(target, match, rex)); target = "test.h"; EXPECT_EQ(true, xp::regex_search(target, match, rex)); target = "./../source/_svn/text-base/main.c.svn-base"; EXPECT_EQ(false, xp::regex_search(target, match, rex)); target = "test.cpp"; EXPECT_EQ(false, xp::regex_search(target, match, rex));}
  20. 20. boost::xpressiveテストで動作を確認する。不安がなくなるまで試すTEST(regex, regexFileseek){ namespace xp = boost::xpressive; string target; xp::smatch match; xp::sregex rex = xp::sregex::compile ("(¥¥.c|¥¥.h)$"); target = "test"; EXPECT_EQ(false, xp::regex_search(target, match, rex)); target = "test.c"; EXPECT_EQ(true, xp::regex_search(target, match, rex)); target = "test.h"; EXPECT_EQ(true, xp::regex_search(target, match, rex)); target = "./../source/_svn/text-base/main.c.svn-base"; EXPECT_EQ(false, xp::regex_search(target, match, rex)); 動作の勉強とチェック target = "test.cpp"; EXPECT_EQ(false, xp::regex_search(target, match, rex));} コードのドキュメントとして活用
  21. 21. デバッギングテスト
  22. 22. class MacroWord バグがでた
  23. 23. class MacroWord1. テスト上でバグを再現するTEST_F(TestMacroWord, macroDataMacroWordBug21){ MacroData macroData; macroData.push_back("AUTO_DEBUG_1"); macroData.push_back("AUTO_DEBUG_2"); macroData.push_back("AUTO_DEBUG_2"); macroData.push_back("AUTO_DEBUG_2"); macroData.push_back(“BB_H_"); MacroWord macroWord(macroData); EXPECT_EQ("AUTO_DEBUG_1", macroWord.data_[0].macroName_); EXPECT_EQ(1, analyzer.data_[0].number); EXPECT_EQ("AUTO_DEBUG_2", macroWord.data_[1].macroName_) EXPECT_EQ(3, analyzer.data_[1].number); EXPECT_EQ(“BB_H_", macroWord.data_[2].macroName_); EXPECT_EQ(1, analyzer.data_[2].number);}
  24. 24. class MacroWord1. テスト上でバグを再現する2. テストでバグを絞込み特定するTEST_F(TestMacroWord, MacroWordBug21){ MacroData macroData; macroData.push_back("AUTO_DEBUG_2"); macroData.push_back("AUTO_DEBUG_2"); macroData.push_back("AUTO_DEBUG_2"); MacroWordProxy::wordCount(macroData); EXPECT_EQ(3, MacroWordProxy::getSize(0));}
  25. 25. class MacroWord1. テスト上でバグを再現する2. テストでバグを絞込み特定するTEST_F(TestMacroWord, MacroWordBug21){ MacroData macroData; macroData.push_back("AUTO_DEBUG_2"); macroData.push_back("AUTO_DEBUG_2"); macroData.push_back("AUTO_DEBUG_2"); MacroWordProxy::wordCount(macroData); EXPECT_EQ(3, MacroWordProxy::getSize(0));}3. 修正後テストがパスすることを確認する
  26. 26. class MacroWord1. テスト上でバグを再現する2. テストでバグを絞込み特定するTEST_F(TestMacroWord, macroWordBug21){ MacroData macroData; macroData.push_back("AUTO_DEBUG_2"); macroData.push_back("AUTO_DEBUG_2"); macroData.push_back("AUTO_DEBUG_2"); MacroWordProxy::wordCount(macroData); EXPECT_EQ(3, MacroWordProxy::getSize(0));}3. 修正後テストがパスすることを確認するバグを再現 バグを特定 バグ修正のチェック
  27. 27. その他ユニットテストの活用 テスト駆動開発 CIによる自動回帰テスト REDREFACT GREEN OR
  28. 28. ユニットテストの効能• バグを検出する• バグ発生箇所を絞り込む• バグ混入を監視する• 機能的な保証を行う• 設計支援を行う• ドキュメントとなる
  29. 29. ツール・方法論が発達した今ではプログラミングとユニットテストが一体となり、プログラマはプログラミングの最初から最後まで継続的にユニットテストのサポートを受けられるようになっている
  30. 30. ユニットテストの継続活用 における課題
  31. 31. ユニットテストの継続活用においては保守性がクリティカルな問題となりえる保守性に問題を持つユニットテストは、再利用の度に冗長なコストを累積させる
  32. 32. 可読性の悪いテスト
  33. 33. TEST(TestHoge, Hoge){ stringstream ss; int i, j; Inspector inspector("hoge", "fuga"); EXPECT_EQ(false, inspector.isEmpty()); inspector.pushLine("this is test."); inspector.pushLine("this is test."); EXPECT_EQ(6, inspector.getWords()); for (j = 0; j < 100; j++) { for (i = 0; i < j; i++) { ss << j; ss << " "; ss << i; inspector.pushLine(ss.str()); EXPECT_EQ(INFO, inspector.getState()); } inspector.addSection(); EXPECT_EQ(j, inspector.getSection()); } EXPECT_EQ(0, inspector.runInspection());}
  34. 34. TEST(TestHoge, Hoge){ stringstream ss; int i, j; Inspector inspector("hoge", "fuga"); EXPECT_EQ(false, inspector.isEmpty()); inspector.pushLine("this is test."); 何を検証している inspector.pushLine("this is test."); のか分からない EXPECT_EQ(6, inspector.getWords()); for (j = 0; j < 100; j++) { for (i = 0; i < j; i++) { ss << j; バグはどこにある ss << " "; のか分からない ss << i; inspector.pushLine(ss.str()); EXPECT_EQ(INFO, inspector.getState()); 正しいのかどうか } わからない inspector.addSection(); EXPECT_EQ(j, inspector.getSection()); } EXPECT_EQ(0, inspector.runInspection());} Assertion Roulette
  35. 35. TEST(TestHoge, Hoge){ stringstream ss; int i, j; Inspector inspector("hoge", "fuga"); EXPECT_EQ(false, inspector.isEmpty()); inspector.pushLine("this is test."); 何を検証している inspector.pushLine("this is test."); のか分からない EXPECT_EQ(6, inspector.getWords()); for (j = 0; j < 100; j++) { for (i = 0; i < j; i++) { ss << j; バグはどこにある ss << " "; のか分からない ss << i; inspector.pushLine(ss.str()); EXPECT_EQ(INFO, inspector.getState()); 正しいのかどうか } わからない inspector.addSection(); EXPECT_EQ(j, inspector.getSection()); } EXPECT_EQ(0, inspector.runInspection());} Assertion Rouletteテストの再利用コストを増大させる
  36. 36. 変更性の悪いテスト(テストコードの重複/ Fragile Test)
  37. 37. TEST(testHoge, InspectionFugaPiyo){ MotorStatus motorStatus(0, 0); MaintenanceData mtData; MaintenanceType mtType(createRegionID(EU)); setInitialData(mtData, mtType); InspectionFuga inspector; inspector.set(createMaintenanceInfo(mtData, mtType), motorStatus); …}TEST(testHoge, InspectionFugaFuga){ MotorStatus motorStatus(-1, 2); MaintenanceData mtData; MaintenanceType mtType(createRegionID(ASIA)); setInitialData(mtData, mtType); InspectionFuga inspector; inspector.set(createMaintenanceInfo(mtData, mtType), motorStatus); …}
  38. 38. TEST(testHoge, InspectionFugaHoge){ MotorStatus motorStatus(133, 232); MaintenanceData mtData; MaintenanceType mtType(createRegionID(JAPAN)); setInitialData(mtData, mtType); InspectionFuga inspector; inspector.set(createMaintenanceInfo(mtData, mtType), motorStatus); …} 似たような記述が多数存在・・
  39. 39. TEST(testHoge, InspectionFugaHoge){ MotorStatus motorStatus(133, 232); MaintenanceData mtData; MaintenanceType mtType(createRegionID(JAPAN)); setInitialData(mtData, mtType); InspectionFuga inspector; inspector.set(createMaintenanceInfo(mtData, mtType), motorStatus); …} Fragile Test テストが製品コードの変更 テストコードの変更箇所 も特定困難 コストを増大させる テストがテストコードの変更 製品コードの変更コスト が大きい コストを増大させる
  40. 40. 補足(Fragile Test)• 製品コードとテストコードの過度な依存 関係によって、製品コード/テストコード の変更コストが増大してしまう問題を Fragile Testsという• Fragile Tests問題は継続的なユニットテ ストやTDDが抱える大きな問題であり、常 に対策し続けなくてはならない
  41. 41. テストコードもレガシーコード化するユニットテストを継続活用する環境では、レガシーコード化したユニットテストは最悪テストの効果をマイナスに反転させる
  42. 42. ユニットテストのコストモデル (継続的にテストを作る場合) ユニットテスト の利益ユニットテスト規模 時間 ユニットテスト 保守コスト 時間 時間
  43. 43. ユニットテストのコストモデル (継続的にテストを作る場合) ユニットテスト の利益ユニットテスト規模 レガシーコード化 時間 ユニットテスト 保守コスト レガシーコード化 時間 時間
  44. 44. ユニットテストのコストモデル (継続的にテストを作る場合) ユニットテスト の利益ユニットテスト規模 レガシーコード化 時間 ユニットテスト 保守コスト レガシーコード化 時間 使えないテストの放棄とい う手段に・・・ 時間
  45. 45. ユニットテストはプログラミングを強力に支援し得るが、一方でプログラミングを強力に阻害し得るユニットテストはソフトウェアの保守性を支えるが、一方でソフトウェアの保守性を阻害し得る
  46. 46. ユニットテストの継続活用 のための目標
  47. 47. 目標 ユニットテスト の利益ユニットテスト規模 時間 ユニットテスト 運用コスト 時間 保守性改善 時間
  48. 48. 目標 ユニットテスト の利益ユニットテスト規模 時間 ユニットテスト 運用コスト 時間 保守性改善 保守性改善により 保守コストの増大を抑える 時間
  49. 49. テストとプロダクトの扱いに違いはない。テストもプロセスを持ち構造的設計、機能的設計を行う必要がある。テストはプロダクトと同じように保守性を作りこむ余地がある。
  50. 50. テスト構造設計 テスト設計テストコードの実装テストコードの テストの 構造設計 上位設計テストコードの テスト設計 実装 テスト実装
  51. 51. ユニットテストの 保守性を支える原則• 変更に対する堅牢性に優れる• 可読性に優れる• 独立性に優れる• 自己完結している• 完全自動化している• 細粒度なテストケース
  52. 52. ユニットテストの 保守性を支える原則• 変更に対する堅牢性に優れる• 可読性に優れる テスト対象・テスト設計が変更 されても、テストコードの変更• 独立性に優れる が小さく済む• 自己完結している• 完全自動化している• 細粒度なテストケース
  53. 53. ユニットテストの 保守性を支える原則• 変更に対する堅牢性に優れる• 可読性に優れる テストコードからテストの設計 や意図を容易に読み取れる• 独立性に優れる• 自己完結している• 完全自動化している• 細粒度なテストケース
  54. 54. ユニットテストの 保守性を支える原則• 変更に対する堅牢性に優れる• 可読性に優れる 構造軸:他のテストメソッド、テ• 独立性に優れる ストクラスの影響を受けない 時間軸:テストの実行順序の影• 自己完結している 響を受けない• 完全自動化している• 細粒度なテストケース
  55. 55. ユニットテストの 保守性を支える原則• 変更に対する堅牢性に優れる• 可読性に優れる Setup、テスト実行、結果の検証、• 独立性に優れる Teardownの一連の処理が細か• 自己完結している 粒度で完結している• 完全自動化している• 細粒度なテストケース
  56. 56. ユニットテストの 保守性を支える原則• 変更に対する堅牢性に優れる• 可読性に優れる• 独立性に優れる• 自己完結している テストを実行する処理を完全に• 完全自動化している 自動化できる• 細粒度なテストケース
  57. 57. ユニットテストの 保守性を支える原則• 変更に対する堅牢性に優れる• 可読性に優れる• 独立性に優れる• 自己完結している• 完全自動化している• 細粒度なテストケース テストメソッドが十分に細分化さ れている
  58. 58. ユニットテストの 保守性作りこみアプローチ1. 実装の工夫2. 構造設計の工夫3. 設計の工夫
  59. 59. ユニットテストの保守性作りこみ アプローチ1 実装の工夫
  60. 60. ユニットテストの実装はプログラミング行為そのものである保守性を高めるパターンを適用し継続的に記述改善する必要がある
  61. 61. コードの共通化• 重複する記述は共通化する• 便利なロジックは汎用化する
  62. 62. TEST(Buyer, addSameStatus){ Buyer buyer; Customer customer1("Taro", "Yamada", 15, 2, "HOGE|FUGA"); customer1.addCategory(STATE_ACTIVE); Customer customer2("Taro", "Yamada", 15, 2, "HOGE|FUGA|HOGEHOGE"); customer2.addCategory(STATE_ACTIVE); buyer.add(customer1); buyer.add(customer2); 似た記述が大量に散在 EXPECT_EQ(0, buyer.getSection());}
  63. 63. TEST(Buyer, addSameStatus){ Buyer buyer; Customer customer1("Taro", "Yamada", 15, 2, "HOGE|FUGA"); customer1.addCategory(STATE_ACTIVE); Customer customer2("Taro", "Yamada", 15, 2, "HOGE|FUGA|HOGEHOGE"); customer2.addCategory(STATE_ACTIVE); buyer.add(customer1); buyer.add(customer2); 似た記述が大量に散在 EXPECT_EQ(0, buyer.getSection());}Parameterized Creation MethodCustomer createCustomer(string status){ Customer customer("Taro", "Yamada", 15, 2, status); customer.addCategory(STATE_ACTIVE); return customer;}
  64. 64. TEST(Buyer, addSameStatus){ Buyer buyer; Customer customer1 = createCustomer("HOGE|FUGA"); Customer customer2 = createCustomer("HOGE|FUGA|HOGEHOGE"); buyer.add(customer1); 重複部分をCreation buyer.add(customer2); Methodで共通化 EXPECT_EQ(0, buyer.getSection());}Parameterized Creation MethodCustomer createCustomer(string status){ Customer customer("Taro", "Yamada", 15, 2, status); customer.addCategory(STATE_ACTIVE); return customer;}
  65. 65. TEST(TestItemList, ItemPush){ … EXPECT_EQ(expectItem.getName(), item.getName()); EXPECT_EQ(expectItem.getID(), item.getID()); EXPECT_EQ(expectItem.getSize(), item.getSize());}TEST(TestItemList, itemPush){ 似た記述が大量に散在 … EXPECT_EQ(expectItem2.getName(), item2.getName()); EXPECT_EQ(expectItem2.getID(), item2.getID()); EXPECT_EQ(expectItem2.getSize(), item2.getSize());}
  66. 66. TEST(TestItemList, itemPush){ … EXPECT_EQ(expectItem.getName(), item.getName()); EXPECT_EQ(expectItem.getID(), item.getID()); EXPECT_EQ(expectItem.getSize(), item.getSize());}TEST(TestItemList, itemPush){ 似た記述が大量に散在 … EXPECT_EQ(expectItem2.getName(), item2.getName()); EXPECT_EQ(expectItem2.getID(), item2.getID()); EXPECT_EQ(expectItem2.getSize(), item2.getSize());}Custom Assertion#define EXPECT_ITEM_EQ(expect, actual) ¥EXPECT_EQ((expect).getName(), (actual).getName()); ¥EXPECT_EQ((expect).getID(), (actual).getID()+1); ¥EXPECT_EQ((expect).getSize(), (actual).getSize());
  67. 67. TEST(TestItemList, itemPush){ … EXPECT_ITEM_EQ(expectItem, item);} 重複部分を拡張Assertionで共通化TEST(TestItemList, test_itemPush){ … EXPECT_ITEM_EQ(expectItem2, item2);}Custom Assertion#define EXPECT_ITEM_EQ(expect, actual) ¥EXPECT_EQ((expect).getName(), (actual).getName()); ¥EXPECT_EQ((expect).getID(), (actual).getID()+1); ¥EXPECT_EQ((expect).getSize(), (actual).getSize());
  68. 68. 可読性の向上• 重要なものを目立たせる• 適切な名前に置き換える• 小さくする
  69. 69. Customer customer2("Taro", "Yamada", 15, 2, "HOGE|FUGA|HOGEHOGE"); 何でもいいダミー値 テストしたい値
  70. 70. Customer customer2("Taro", "Yamada", 15, 2, "HOGE|FUGA|HOGEHOGE"); 何でもいいダミー値 テストしたい値Parameterized Creation MethodCustomer customer2 = createCustomer("HOGE|FUGA|HOGEHOGE"); ダミー値を隠し、重要なパラメータを強調する 重要なものを目立たせる ※隠ぺいはトレードオフなので要注意
  71. 71. TEST(testHoge, Fuga){ MotorStatus motorStatus(133, 232); InspectionFuga inspector; inspector.set(createMaintenanceInfo(motorStatus); EXPECT_EQ(START, inspector.getState()); EXPECT_EQ(true, inspector.isEmpty()); inspector.initialize(); EXPECT_EQ(INFO, inspector.getState()); EXPECT_EQ(false, inspector.isEmpty());}
  72. 72. TEST(testHoge, Fuga){ MotorStatus motorStatus(133, 232); InspectionFuga inspector; inspector.set(createMaintenanceInfo(motorStatus); EXPECT_EQ(START, inspector.getState()); EXPECT_EQ(true, inspector.isEmpty()); inspector.initialize(); EXPECT_EQ(INFO, inspector.getState()); EXPECT_EQ(false, inspector.isEmpty());} 1つのテストメソッドで色々検証しているため少し読みにくい 分割で改善する
  73. 73. TEST(testHoge, Fuga){ InspectionFuga inspector = createInspectionFugaDummy(); EXPECT_EQ(START, inspector.getState()); EXPECT_EQ(true, inspector.isEmpty()); inspector.initialize(); EXPECT_EQ(INFO, inspector.getState()); EXPECT_EQ(false, inspector.isEmpty());} 分割時に重複するメソッドをあらかじめ抜き出す
  74. 74. TEST(testHoge, fugaConstractor){ InspectionFuga inspector = createInspectionFugaDummy(); EXPECT_EQ(START, inspector.getState()); EXPECT_EQ(true, inspector.isEmpty());}TEST(testHoge, fugaInitialize){ InspectionFuga inspector = createInspectionFugaDummy(); inspector.initialize(); EXPECT_EQ(INFO, inspector.getState()); EXPECT_EQ(false, inspector.isEmpty());} テストメソッドを小さくする 意図を読み取りやすい名前をつける
  75. 75. その他工夫• テストメソッドをコンパクトに記述する• テストコードの影響範囲を限定する – 時間軸、構造軸• 危ういコードを分離する – 変更頻度の高いインターフェースをラッピン グする – テスト用インタフェースを用意する
  76. 76. その他工夫• テストコードの保守性は製品コード・テ ストコード両方から支えられる• 堅牢なユニットテストには堅牢な製品コ ードのインターフェースが不可欠である テスト 製品 コード コード テストが内部メンバを細かく参照する形は Fragile Testsになりえる
  77. 77. その他工夫• テストコードの保守性は製品コード・テ ストコード両方から支えられる• 堅牢なユニットテストには堅牢な製品コ ードのインターフェースが不可欠である 堅牢な インターフェース テスト 製品 コード コード 堅牢なインターフェースを製品コードが提供することで テストコードの堅牢性を高められる
  78. 78. ユニットテストの実装はプログラミング行為そのものである製品コード・テストコード両方に対して、保守性を高めるパターンを適用し継続的に記述改善する必要がある
  79. 79. ユニットテストの保守性作りこみ アプローチ2 構造設計の工夫
  80. 80. ユニットテストのコードはAssertionの羅列ではないテストコードも構造を持ち、それがユニットテストの保守性に影響を与えるため、構造設計の視点が必要になる
  81. 81. 内部設計
  82. 82. テストのツリー構造 Assertion Test Case Assertion Test Suite Test Caseユニットテ スト Test Suite … …
  83. 83. テストのツリー構造 細粒度・可読性 Assertion Test Case Assertion Test Suiteユニットテ 少数のAssertionでTestCaseを構成する Test Case スト (1条件/1テストケースなど Test Suite テスト設計の最小構成単位を反映) … … テスト設計やテストの意図が 分かりやすくなる
  84. 84. テストのツリー構造 独立性、自己完結性の確保 Assertion Test Case Assertion Test Suite 独立・無干渉 Test Caseユニットテ 独立・無干渉 スト Test Suite … 変更や流用の影響を局所化できる …
  85. 85. テストのツリー構造 可読性などTest Suite:テスト対象Class = n : 1 Assertion ※Fixture/外部コンポーネント等で分離 Test Case 通常は1:1 Assertion Test Suit Test Case ユニットテ スト Test Suit … 構造に対する網羅を… チェックしやすくする
  86. 86. ユニットテストの内部構造は一般的にツリー構造をとるそのため葉のコンパクト化、枝の独立性向上、適切な分割といった構造化設計の工夫が重要となるそれによりモジュール性や可読性が向上し、ユニットテストの保守性が作りこまれる
  87. 87. 外部インターフェース設計
  88. 88. 外部インターフェース• 外部コンポーネントは保守性を低下させ るリスクになる テスト テスト テスト対象 外部IF ブラックボックス
  89. 89. 外部インターフェース• 外部コンポーネントは保守性を低下させ ブラックボックスを介してテストが結合 るリスクになる テスト テスト テスト対象 外部IF ブラックボックス 状態を持つ
  90. 90. ユニットテストの保守性を損なう外部コンポーネントは、外部インターフェースの改善で分離する必要がある 外部コンポー ブラックボック ブラックボック StubやMock ネントを使用 スを使用する ス など しないテスト テスト テスト対象
  91. 91. 構造設計による改善
  92. 92. ユニットテストの内部構造や外部インターフェースはその保守性に影響するため、構造設計・アーキテクチャ設計を俯瞰的に洗練する視点が要求されるただし留意点が2つある
  93. 93. 1:柔軟なズームアウト/ ズームインが必要• ユニットテストを継続活用する場合、ユ ニットテストはしばしばボトムアップに 実装されるため、構造設計は実装状況に 応じて柔軟に行わなければならない 構造設計 ズームアウト ズームイン 実装
  94. 94. 2:改善対象として製品コード/ テストコードを区別しない• ユニットテストの障害は、製品コード/テ ストコード両方から除去する• 両者のインターフェースを統合的に洗練 させる
  95. 95. class CameraCtrlvoid CameraCtrl::hoge(){ _motorCtrl.run(); テスト対象が … 外部コンポーネントに} 依存しているvoid CameraCtrl::fuga() 構造設計{ _motorCtrl.stop(); … 外部コンポーネントを制御} 実装
  96. 96. Test Suite テストが 外部コンポーネントを 共有している CameraCtrl 構造設計 ズームアウト 実装 MotorCtrl(外部コンポーネント)
  97. 97. 外部コンポーネ ントTest TestSuite Suite MockやStub等 テスタビリティの作りこみ CameraCtrl 外部コンポーネントを 置換可能にする 構造設計 実装
  98. 98. Test Test MotorCtrl Suite Suite Dependency Injection void CameraCtrl::CameraCtrl() CameraCtrl { _motorCtrl.open(...): ... } 構造設計 void CameraCtrl::CameraCtrl(MemoryCtrl memoryCtrl)ズームイン { _motorCtrl = motorCtrl; _motorCtrl.open(...): 実装 ... }
  99. 99. Mock、 外部コンポー右以外のテス ト ネントを用い MotorCtrl Stub等 るテスト Dependency Injection CameraCtrl 実装 構造設計
  100. 100. ユニットテストのテストコードはAssertionのリストではないテストコードも構造を持ち、それがユニットテストの保守性に影響を与える継続的な構造設計の改善で、テストの保守性を高めよう
  101. 101. 実装・構造設計の参考図書• xUnit Test Patterns – –Refactoring Test Code• 読書会サイトに翻訳情報 あり – <http://www.fieldnotes.jp/xutp/> – 「xutp読書会」で検索
  102. 102. ユニットテストの保守性作りこみ アプローチ3 設計の工夫
  103. 103. 設計の工夫1. テスト設計を洗練する2. トップダウンのテスト設計で保守性を作 りこむ
  104. 104. テスト設計の洗練
  105. 105. ユニットテストの継続的活用 においての留意点 • 作りすぎたテストを継続的に保守しようとす ると保守性に悪影響を与える テストコード TEST(…) { 製品コード … EXPECT_EQ(…, Hoge()); void Hoge() } { TEST(…) … { } … EXPECT_EQ(…, Hoge()); }製品コードを変更する TEST(…) {と大量のテストがビルドエラー … EXPECT_EQ(…, Hoge()); } テスト設計を変更する と修正が複数にまたがる
  106. 106. 保守性を高めるにはコンパクトなテストで十分な網羅性を確保する必要があるその実現にテスト設計技法やテスティングリテラシーの素養は有効である
  107. 107. 境界値分析• 入出力値をグルーピング(例えば同値分 割)して、グループの境界の値を抽出す る• 欠陥が境界付近に偏在するという経験則 に立脚
  108. 108. 境界値分析• 「6歳未満は無料。6歳以上12歳以下は半 額。13以上は定額」
  109. 109. 境界値分析• 「6歳未満は無料。6歳以上12歳以下は半 額。13以上は定額」-∞ +∞ -1 ありえない 0 5 無料 6 12 半額 13 定額
  110. 110. カバレッジ分析• 制御フローの網羅度を基準にテスト設計 を行う
  111. 111. カバレッジ分析start 入力:ループ回数 任意の回数 ループend
  112. 112. カバレッジ分析start 入力:ループ回数 カバレッジレベル6(C6) テストで用いるループ回数: 0回 任意の回数 1回 ループ 頻出する代表値 最大回end
  113. 113. テストの目的に応じて柔軟に 設計技法を使い分ける 仕様テスト設計 その他コード構造
  114. 114. テストの目的に応じて柔軟に 設計技法を使い分ける 4で割り切れる N Y Y Y 機能的な保証 仕様 100で割り切れる N N Y Y テストファースト 400で割り切れる N N N Y etc.. うるうどし N Y N Yテスト設計 その他 //うるう年か判定する bool isLeapYear(unsigned int year) { if (year % 400 == 0) 機械的なリファクタリング { return true; テスタビリティの評価 } etc.. if ((year % 4 == 0) && (year % 100 != 0))コード構造 { return true; } return false; }
  115. 115. テストの目的に応じて柔軟に設計技法や考え方を応用する TEST(…)任意の値でいいけど {取り合えず境界値を … 指定しよう EXPECT_EQ(?, ?); } /**仕様が厳密なので仕様ベー * @brief …詳細な関数仕様…スのテスト設計ですまそう */ void fuga(…) { } bool hoge(unsigned int year)仕様が不明だけど単純な分 { if (year % 400 == 0)岐構造なのでC3のテストを用 {意してリファクタリングしよう return true; } if ((year % 4 == 0) && (year % 100 != 0)) { return true; } return false; }
  116. 116. テスト設計の技法や考え方のレパートリーが増えればより洗練されたテストが書けるようになり、ユニットテストの保守性向上につながる
  117. 117. テスト設計技法 参考図書
  118. 118. 2.トップダウンのテスト設計で 保守性を作りこむ
  119. 119. テスト設計フロー テストコード実装フロー テスト設計フロー テストコードの 構造設計 テスト設計 テストコードの テスト実装 実装ユニットテストの継続的活用におけるテスト設計フロー
  120. 120. テスト設計フロー テスト条件、テスト設計技法等を整理する テスト設計 テストケースを求める、等 テスト実装 テストケースの選定を行う テストに用いる具体値を求める、等
  121. 121. 俯瞰的なテスト設計もユニットテストの保守性の作りこみにとって必須であるテストコードの構造設計と同様、柔軟なズームアウト/ズームインで洗練されたテスト設計を実現しよう
  122. 122. テスト条件の抽出による外部インターフェースの洗練 テストコード実装フロー テスト設計フロー テストコードの 構造設計 テスト設計 テストコードの テスト実装 実装 ユニットテストの継続的活用におけるテスト設計フロー
  123. 123. 2. トップダウンでインターフェース設計 1. テスト条件を抽出 DB UART ●テスト条件リスト 通常テスト テスト テスト ・DB ・タッチパネル … ・メカニカルボタン ・UART通信 DB UART … DB UART ….スタブ スタブ ユニットテストの制約となる テストコードの実装 テスト設計プロセス 外部コンポーネントを抽出し、構造設計に反映させる テストコードの テスト設計 構造設計 テストコードの 実装 テスト実装 全体整合のとれた外部インターフェースを構築
  124. 124. テスト対象の安定度分析による ユニットテストの作りこみ テストコード実装フロー テスト設計フロー テストコードの テスト設計 構造設計 テストコードの テスト実装 実装 ユニットテストの継続的活用におけるテスト設計フロー
  125. 125. 2. 変更リスクの少ないテストを作りこみ 1. テスト対象の安定度評価テストの品質 Hoge Fuga Piyo … ファイル 変更step/week ゴールとなる水準 1 ○ ○ Hoge.cpp 0 2 ○ ○ Fuga.cpp 23 PIyo.cpp 133 3 … … … 開発時間 安定したプログラムユニットを抽出し、イテレーティブな テストコードの実装 テスト設計プロセス ユニットテストの作りこみに反映する テストコードの テスト設計 構造設計 テストコードの テスト実装 実装 テストの作りすぎによるデメリットを軽減する
  126. 126. テスト計画/テストの上位設計 の重要性• 開発中継続的にユニットテストを構築し ていく場合
  127. 127. 開発中継続的にユニットテストを構築していく場合、テスト計画・テスト上位設計は特に重要である望ましいテストは何か、いつどのようなテストを確保すべきか、どのようにテストを管理するかトップダウンで決めることで、柔軟かつ洗練されたテストを確保できる
  128. 128. テスト計画・テスト上位設計• テスト目的、テスト対象• 望ましいテストの構造 – テストクラスの粒度 • Feature/Class/Fixture/Method/Story – 望ましいAssertionの数 – テストスイートの粒度• テストをどのように配置するか – テストレベル/テストタイプ/テストフェーズ/テストの目的• 期待するテストの網羅性 – ソフトウェアのどこをテストするか • コンポーネント/パッケージ/外部コンポーネント – カバレッジはどの程度か • 構造的カバレッジ/機能的カバレッジ
  129. 129. テスト計画・テスト上位設計• テストの品質の管理・保証方法 – カバレッジやテストコード等の管理 • どう維持するか/いつどのように保証するか• テスト環境の整備 – Test Doubleの開発管理 – ツールや環境の管理• フェーズや計画 – どの段階でどの程度のテストを確保するか
  130. 130. テストコードと同じく、テスト設計も上位構造を持つ保守性の障害となるテスト条件や制約、テスト設計の抽象構造を抽出し、下位の設計に反映させていこう
  131. 131. まとめ
  132. 132. ユニットテストの実装はプログラミング行為そのもの保守性を高めるパターンを適用し、継続的に記述改善しよう
  133. 133. テストコードはAssertの羅列ではない保守性に優れたアーキテクチャ・構造をテストコードに持たせようまた構造のズームアウト/ズームインを柔軟に行って、継続的にテストコードの設計を改善させていこう
  134. 134. テスト設計はテストコードの保守性を左右する保守性に優れたテストを書くためテスト設計技法、リテラシーをどんどん身につけようテスト設計でもトップダウンでテスト設計の改善をすすめようテスト計画・上位設計でより良いテストを効率的に確保しよう
  135. 135. ご清聴ありがとうございました

×