2013年7月18日木曜日

[iutest] TDDBC の課題の経過報告(その1)

ちょこちょこ進めていた TDDBC の課題ですが、ステップ0が終わったので経過報告でもしておこうと思います。

環境のおさらい
家の PC が壊れたこともあって、タブレットのみで作業できるように環境を整えました。
ビルドとテストは CloudBees の Jenkins を使用しました。
コミットは Red/Green/Refactoring ごとになるべく細かく行うようにしました。

解説
svn のリビジョンにそって解説していきます。
r1~r4
このへんは最初の環境セットアップをしています。
Makefile の準備やファイルの追加を行なっています。

r5
Index: test/VendingMachineTest.cpp
===================================================================
--- test/VendingMachineTest.cpp (revision 4)
+++ test/VendingMachineTest.cpp (revision 5)
@@ -1,3 +1,13 @@
 #include "gtest/iutest_switch.hpp"
+#include "VendingMachine.h"

+class VMTest : public ::iutest::Test
+{
+public:
+    VendingMachine vm;
+};

+IUTEST_F(VMTest, totalAmount)
+{
+    IUTEST_ASSERT_EQ(0, vm.GetTotalAmount());
+}

まずは、投入金額の取得関数のテストを書きました。
この時点ではビルドは通りません。
テストフィクスチャのことは知っているので、初めからフィクスチャを使っています。
テストフィクスチャを使うことで VendingMachine の構築などテストで共通の処理をまとめて書くことができます。

r6
Makefile を修正しました。
r7
Index: src/VendingMachine.cpp
===================================================================
--- src/VendingMachine.cpp      (revision 6)
+++ src/VendingMachine.cpp      (revision 7)
@@ -1,2 +1,6 @@
 #include "VendingMachine.h"

+int VendingMachine::GetTotalAmount(void)
+{
+    return -1;
+}
Index: src/VendingMachine.h
===================================================================
--- src/VendingMachine.h        (revision 6)
+++ src/VendingMachine.h        (revision 7)
@@ -1,4 +1,10 @@
 #ifndef INCG_VendingMachine_H_
 #define INCG_VendingMachine_H_

+class VendingMachine
+{
+public:
+    int GetTotalAmount(void);
+};
+
 #endif
GetTotalAmount 関数を実装しました。
ここではテストが失敗するように実装をします。
テストが正しく失敗することを確認することで、テスト自体が正しく書けているかがわかります。
ここで仮にテストに成功した場合、テストの方が間違っているはずです。

こちらが Jenkins の結果。


r8
Index: src/VendingMachine.cpp
===================================================================
--- src/VendingMachine.cpp      (revision 7)
+++ src/VendingMachine.cpp      (revision 8)
@@ -2,5 +2,5 @@

 int VendingMachine::GetTotalAmount(void)
 {
-    return -1;
+    return 0;
 }

テストが通るように実装しました。

ここで重要なのが、テストが通る最小の実装に留めることです。
分かっていてもついつい実装が進み過ぎてしまうことがあります。
今回もやってしまったので、これについては後ほど解説します。


r9
Index: test/VendingMachineTest.cpp
===================================================================
--- test/VendingMachineTest.cpp (revision 8)
+++ test/VendingMachineTest.cpp (revision 9)
@@ -11,3 +11,9 @@
 {
     IUTEST_ASSERT_EQ(0, vm.GetTotalAmount());
 }
+
+IUTEST_F(VMTest, insert)
+{
+    vm.Insert(100);
+    IUTEST_ASSERT_EQ(100, vm.GetTotalAmount());
+}
特にリファクタリングすることもないので、次のテストを書きました。
次はお金の投入のテストを書きました。

r10
Index: src/VendingMachine.cpp
===================================================================
--- src/VendingMachine.cpp      (revision 9)
+++ src/VendingMachine.cpp      (revision 10)
@@ -4,3 +4,7 @@
 {
     return 0;
 }
+
+void VendingMachine::Insert(int money)
+{
+}
Index: src/VendingMachine.h
===================================================================
--- src/VendingMachine.h        (revision 9)
+++ src/VendingMachine.h        (revision 10)
@@ -5,6 +5,7 @@
 {
 public:
     int GetTotalAmount(void);
+    void Insert(int money);
 };

 #endif
テストが失敗する最小の Insert 関数を実装しました。

Jekins で失敗を確認。

r11~r13
Index: src/VendingMachine.cpp
===================================================================
--- src/VendingMachine.cpp      (revision 10)
+++ src/VendingMachine.cpp      (revision 11)
@@ -2,9 +2,10 @@

 int VendingMachine::GetTotalAmount(void)
 {
-    return 0;
+    return m_TotalAmount;
 }

 void VendingMachine::Insert(int money)
 {
+    m_TotalAmount += money;
 }
Index: src/VendingMachine.h
===================================================================
--- src/VendingMachine.h        (revision 10)
+++ src/VendingMachine.h        (revision 11)
@@ -6,6 +6,8 @@
 public:
     int GetTotalAmount(void);
     void Insert(int money);
+private:
+    int m_TotalAmount;
 };

 #endif

テストが通るように実装しました。
が、実はここでミスをしました。
これではテストが通るための最小の実装でないからです。

r12 で意気揚々とリファクタリングしてますが、次のテストを考えた時に間違いに気づきました。
次のテストとして、100円以外のお金が投入された場合のテストを書こうと思ったのですが、
r11 の実装では、テストを失敗させることができないのです。

テストを失敗させることができないとテストの正当性が確認できなくなってしまい、
たとえそのテストが成功したとしても、誤ったテストで誤った結果を確認しているだけになってしまう恐れがあります。

というわけで、r13 で最小の実装に退行させています。
Index: src/VendingMachine.cpp
===================================================================
--- src/VendingMachine.cpp      (revision 12)
+++ src/VendingMachine.cpp      (revision 13)
@@ -12,5 +12,5 @@

 void VendingMachine::Insert(int money)
 {
-    m_TotalAmount += money;
+    m_TotalAmount = 100;
 }

Jenkins の結果も確認しておきます。

r14~r17
Index: test/VendingMachineTest.cpp
===================================================================
--- test/VendingMachineTest.cpp (revision 13)
+++ test/VendingMachineTest.cpp (revision 14)
@@ -7,13 +7,20 @@
     VendingMachine vm;
 };

+class VMParamTest : public VMTest, public ::iutest::TestWithParam<int>
+{};
+
 IUTEST_F(VMTest, totalAmount)
 {
     IUTEST_ASSERT_EQ(0, vm.GetTotalAmount());
 }

-IUTEST_F(VMTest, insert)
+IUTEST_P(VMParamTest, insert)
 {
-    vm.Insert(100);
-    IUTEST_ASSERT_EQ(100, vm.GetTotalAmount());
+    int money=GetParam();
+    vm.Insert(money);
+    IUTEST_ASSERT_EQ(money, vm.GetTotalAmount());
 }
+
+IUTEST_INSTANTIATE_TEST_CASE_P(Accept, VMParamTest
+    , ::iutest::Values(10, 50, 100, 500));
100円以外のお金の投入テストに対応するため、値のパラメータ化テストを使用しました。(1000円抜けてますが…)

Insert の実装は既にあるのでビルドは通る状態のはずでしたが、
テストフレームワークの使い方を間違えたため、ビルドエラーとなってしまいました。
r15,r16 とで修正を試みていますが、そもそもテストフレームワーク側がテストフィクスチャのダイヤモンド継承に対応していませんでした。

というわけで、テストフレームワーク側に手を加えて解決しました。
Jenkins でテストが失敗することも確認。

r18
Index: src/VendingMachine.cpp
===================================================================
--- src/VendingMachine.cpp      (revision 17)
+++ src/VendingMachine.cpp      (revision 18)
@@ -12,5 +12,5 @@

 void VendingMachine::Insert(int money)
 {
-    m_TotalAmount = 100;
+    m_TotalAmount = money;
 }
テストが通るように修正しました。
この実装では不十分なことは分かりきっていますが、ここでも最小の実装に留めています。

r19,r20
Index: test/VendingMachineTest.cpp
===================================================================
--- test/VendingMachineTest.cpp (revision 18)
+++ test/VendingMachineTest.cpp (revision 20)
@@ -1,26 +1,23 @@
 #include "gtest/iutest_switch.hpp"
 #include "VendingMachine.h"

-class VMTest : virtual public ::iutest::Test
+class VMTest : public ::iutest::TestWithParam<int>
 {
 public:
     VendingMachine vm;
 };

-class VMParamTest : public VMTest, public ::iutest::TestWithParam<int>
-{};
-
 IUTEST_F(VMTest, totalAmount)
 {
     IUTEST_ASSERT_EQ(0, vm.GetTotalAmount());
 }

-IUTEST_P(VMParamTest, insert)
+IUTEST_P(VMTest, insert)
 {
     int money=GetParam();
     vm.Insert(money);
     IUTEST_ASSERT_EQ(money, vm.GetTotalAmount());
 }

-IUTEST_INSTANTIATE_TEST_CASE_P(Accept, VMParamTest
+IUTEST_INSTANTIATE_TEST_CASE_P(Accept, VMTest
     , ::iutest::Values(10, 50, 100, 500));

テストのリファクタリングをしました。
ダイヤモンド継承の問題をテストフレームワーク側に解決させましたが、
そもそもそんなことをしなくてもテストフィクスチャの再利用は可能でした。
※1 その辺は後日まとめます。
※2 Google Test でもダイヤモンド継承できなかったので、移植性も考慮。

r21
Index: test/VendingMachineTest.cpp
===================================================================
--- test/VendingMachineTest.cpp (revision 20)
+++ test/VendingMachineTest.cpp (revision 21)
@@ -14,10 +14,19 @@

 IUTEST_P(VMTest, insert)
 {
-    int money=GetParam();
+    const int money=GetParam();
     vm.Insert(money);
     IUTEST_ASSERT_EQ(money, vm.GetTotalAmount());
 }

+IUTEST_P(VMTest, insert2)
+{
+    const int money=GetParam();
+    vm.Insert(money);
+    IUTEST_ASSERT_EQ(money, vm.GetTotalAmount());
+    vm.Insert(money);
+    IUTEST_ASSERT_EQ(money*2, vm.GetTotalAmount());
+}
+
 IUTEST_INSTANTIATE_TEST_CASE_P(Accept, VMTest
     , ::iutest::Values(10, 50, 100, 500));

続いて、2枚お金を投入したときのテストを作成しました。
(※ 1000円が抜けているのはミスです。)

テストが失敗することを確認。

r22
Index: src/VendingMachine.cpp
===================================================================
--- src/VendingMachine.cpp      (revision 21)
+++ src/VendingMachine.cpp      (revision 22)
@@ -12,5 +12,5 @@

 void VendingMachine::Insert(int money)
 {
-    m_TotalAmount = money;
+    m_TotalAmount += money;
 }

テストが通るように修正します。
ここでようやくそれっぽくなりましたね。

r23
Index: test/VendingMachineTest.cpp
===================================================================
--- test/VendingMachineTest.cpp (revision 22)
+++ test/VendingMachineTest.cpp (revision 23)
@@ -29,4 +29,4 @@
 }

 IUTEST_INSTANTIATE_TEST_CASE_P(Accept, VMTest
-    , ::iutest::Values(10, 50, 100, 500));
+    , ::iutest::Values(10, 50, 100, 500, 1000));

1000円のテストが抜けていたので追加しました。

r24
Index: test/VendingMachineTest.cpp
===================================================================
--- test/VendingMachineTest.cpp (revision 23)
+++ test/VendingMachineTest.cpp (revision 24)
@@ -12,6 +12,11 @@
     IUTEST_ASSERT_EQ(0, vm.GetTotalAmount());
 }

+IUTEST_F(VMTest, refund)
+{
+    IUTEST_ASSERT_EQ(0, vm.Refund());
+}
+
 IUTEST_P(VMTest, insert)
 {
     const int money=GetParam();

払い戻しのテストを追加しました。
r25
Index: src/VendingMachine.cpp
===================================================================
--- src/VendingMachine.cpp      (revision 24)
+++ src/VendingMachine.cpp      (revision 25)
@@ -14,3 +14,8 @@
 {
     m_TotalAmount += money;
 }
+
+int VendingMachine::Refund(void)
+{
+    return -1;
+}
Index: src/VendingMachine.h
===================================================================
--- src/VendingMachine.h        (revision 24)
+++ src/VendingMachine.h        (revision 25)
@@ -9,6 +9,7 @@
 public:
     int GetTotalAmount(void);
     void Insert(int money);
+    int Refund(void);
 private:
     int m_TotalAmount;
 };

払い戻しの実装をしました。

もちろんテストが失敗するように実装しています。

r26
Index: src/VendingMachine.cpp
===================================================================
--- src/VendingMachine.cpp      (revision 25)
+++ src/VendingMachine.cpp      (revision 26)
@@ -17,5 +17,5 @@

 int VendingMachine::Refund(void)
 {
-    return -1;
+    return 0;
 }

テストが通る最小の実装です。

r27
Index: test/VendingMachineTest.cpp
===================================================================
--- test/VendingMachineTest.cpp (revision 26)
+++ test/VendingMachineTest.cpp (revision 27)
@@ -12,9 +12,13 @@
     IUTEST_ASSERT_EQ(0, vm.GetTotalAmount());
 }

-IUTEST_F(VMTest, refund)
+IUTEST_P(VMTest, refund)
 {
     IUTEST_ASSERT_EQ(0, vm.Refund());
+
+   const int money=GetParam();
+    vm.Insert(money);
+    IUTEST_ASSERT_EQ(money, vm.Refund());
 }

 IUTEST_P(VMTest, insert)

続いてお金を投入した後の払い戻しテストを追加しました。

実装は既にあるので、テストが失敗することを確認します。

r28
Index: src/VendingMachine.cpp
===================================================================
--- src/VendingMachine.cpp      (revision 27)
+++ src/VendingMachine.cpp      (revision 28)
@@ -17,5 +17,5 @@

 int VendingMachine::Refund(void)
 {
-    return 0;
+    return m_TotalAmount;
 }
テストが通るように実装。

r29
Index: test/VendingMachineTest.cpp
===================================================================
--- test/VendingMachineTest.cpp (revision 28)
+++ test/VendingMachineTest.cpp (revision 29)
@@ -15,10 +15,12 @@
 IUTEST_P(VMTest, refund)
 {
     IUTEST_ASSERT_EQ(0, vm.Refund());
+    IUTEST_ASSERT_EQ(0, vm.GetTotalAmount());

    const int money=GetParam();
     vm.Insert(money);
     IUTEST_ASSERT_EQ(money, vm.Refund());
+    IUTEST_ASSERT_EQ(0, vm.GetTotalAmount());
 }

 IUTEST_P(VMTest, insert)

最後に払い戻しした後の投入金額のテストを追加しました。
払い戻しをしたあとなので、当然金額は 0円になります。

ここでも、実装は既にありますのでテストが失敗することを確認します。


r30
Index: src/VendingMachine.cpp
===================================================================
--- src/VendingMachine.cpp      (revision 29)
+++ src/VendingMachine.cpp      (revision 30)
@@ -17,5 +17,7 @@

 int VendingMachine::Refund(void)
 {
-    return m_TotalAmount;
+    const int refund = m_TotalAmount;
+    m_TotalAmount = 0;
+    return refund;
 }

払い戻し後の投入金額の処理を実装しました。

テストも通りました。

r31
Index: test/VendingMachineTest.cpp
===================================================================
--- test/VendingMachineTest.cpp (revision 30)
+++ test/VendingMachineTest.cpp (revision 31)
@@ -12,12 +12,15 @@
     IUTEST_ASSERT_EQ(0, vm.GetTotalAmount());
 }

-IUTEST_P(VMTest, refund)
+IUTEST_F(VMTest, refund)
 {
     IUTEST_ASSERT_EQ(0, vm.Refund());
     IUTEST_ASSERT_EQ(0, vm.GetTotalAmount());
+}

-   const int money=GetParam();
+IUTEST_P(VMTest, insertAndRefund)
+{
+    const int money=GetParam();
     vm.Insert(money);
     IUTEST_ASSERT_EQ(money, vm.Refund());
     IUTEST_ASSERT_EQ(0, vm.GetTotalAmount());

最後にリファクタリングをしています。
投入をしていない場合の払い戻しのテストを別のテストに分けました。


次へ
以上で、ステップ0が完了しました。
テストの推移ばこんな感じになりました。

オーバーフローなど考慮しないといけない点がまだありますが、次のステップ1に進みたいと思います。


まとめ
ここまでは TDDBC でやったことのおさらいなのでスムーズにいきました。
大事なことは「テストを書いたら失敗させる」・「テストを成功させるときは最小の実装で」。
TDDBC の時にも言われましたが、慣れたら一気に数回転進めるのもいいらしいです。

私はまだまだ初心者なので1つ1つやっていきたいと思います。

最後に、ここに書いた手順が正解ではありません。
むしろ間違っていたらツッコミいれてください m(__)m



0 件のコメント:

コメントを投稿