[Japanese|English]

目次

  1. はじめに
  2. 燃費計算ソフトの仕様
  3. 準備
  4. 失敗するテスト
  5. 通貨クラス
  6. 通貨クラスの機能拡張
  7. フォームの実装
 はじめに

ここは PalmUnit を使って実際にアプリケーションを作ることでその使い方を説明していくページにしていく予定です.

単体テストを自動化することで,すこしでもデバッグ作業を減らし,効率よくアプリを作成することができるようになるお手伝いができればと願っております. 余計なお世話かもしれませんが... ^^;

「もっといい方法がある!」とか「そんなことしてたらダメ!」とかご指摘くださいますとありがたいです. catoo までお気軽にメールしてください.

なお例題として作成するのは「燃費計算ソフト」です.

 
 燃費計算ソフトの仕様

最初は簡単なものでいいので,以下のようなユースケースを考えています.

  • データの入力

    2001 年 2 月 12 日,車で買い物にいくとガソリンが残り少なくなってきたので○×ガソリンスタンドで給油しました. 現金でレギュラー満タンにしてもらうと 3,412 円でした. 明細をみてみると値段は 104 円/リットルで 31.33 リットル給油していました. 消費税は 163 円でした. トリップメータをみるとちょうど 300 km でした. そこで Palm をとりだしこれらの情報を入力しました. 燃費は 9.58 km/リットル でした.

  • データの参照

    最近どうも燃費が悪いような気がするので,一覧でここ何回かの燃費を確かめてみました. そんなに変動はしていないようです.

以上から,燃費計算ソフトには詳細編集画面と一覧画面があればよいようです. 私はより具体的にイメージしやすいようにいつもテキストデータでプロトタイプを作成しています. テキストなのですぐに前後を入れ替えたりコメントを記しておくにも便利だとおもうのですがいかがでしょう?

【給油画面】
+------+
|Refill|             ▼ My Car  ← 車種を選択
+------+----------------------+
|    Date: [01/02/12]         | ← 日付を選択
| Mileage: [0][0][0][3][0][0] | ← 各数値がボタンでタップするとその桁がカウントアップする
|  Vendor: ▼○×スタンド____ | ← 最近行ったガソリンスタンドを 10 個所までおぼえておく
|   Grade: ▼Regular          | ← レギュラー,ハイオクなどをリストから選択
|  Amount:__31.33 ▼Liter     | ← リットルとガロンを選択可能とする(小数は 2 桁)
|    Cost:___3412             | ← トータルの金額を入力
|    Note:___________________ |
|         ___________________ |
|         ___________________ |
|         ___________________ |
|         ___________________ |
|         ___________________ |
|                             |
|(Done)                       | ← 一覧画面に遷移
+-----------------------------+

【一覧画面】
+---+
|MPL|                ▼ My Car  ← 車種を選択
+---+-------------------------+
|Date  Vendor       ▼MPL     | ← MPL,MPG(ガロン)のほか金額や単価(Cost÷Amount)を
|2/12  ○×スタ...     9.58 ↑|    選択可能とする
|                             |
|                             |
|                             |
|                             |
|                             |
|                             |
|                             |
|                             |
|                             |
|                             |
|                           ↓|
|(Refill)                     | ← 給油画面に遷移
+-----------------------------+

【設定画面】
+-----------------------------+
|        Preferences          | 
+-----------------------------+
|Currency Decimal Places: ▼2 | ← 通貨の小数点以下の桁数
|Default Unit: ▼Liter        | ← デフォルトの単位
|Sort by : ▼Date Des.        | ← 一覧のソート順
|                             |
|(OK)(Cancel)                 |
+-----------------------------+

そうそうアプリケーションの名前は mileage per liter を略して MPL としましょう.

 
 準備

それではアプリケーション作成の準備にかかりましょう. やらなければならない準備が多いので大変ですが,辛抱ください. まず作業ディレクトリを作成します. PalmUnit のヘッダファイルやライブラリとあわせて次のような構成にしてみます.

~/                          ホームディレクトリ
 |
 +-- prog/                  プログラム置き場
     |
     +-- Palm               プログラミングで共通に使うもの置き場
     |   |
     |   +-- include        ヘッダ
     |   |
     |   +-- lib            ライブラリ
     |
     +-- PalmUnit           PalmUnit を展開したディレクトリをここに置く
     |   |
     |   +-- doc
     |   |
     |   +-- framework
     |   |
     |   +-- src
     |
     +-- MPL                燃費計算ソフトのディレクトリ
         |
         +-- Palm           もしかして Conduit を作るかもしれないので Palm サブディレクトリとしておく
             |
             +-- test       Unit Test 用ディレクトリ

では PalmUnit のヘッダファイルとライブラリをインストールしましょう. ~/prog/PalmUnit/framework/MakefileINSTALLDIR マクロを次のように変更してから make します.

## Makefile for PalmUnit Framework

TARGET = PalmUnit.a
SDK = -palmos3.5

INSTALLDIR = ../../Palm          # ← ~/prog/Palm の相対パス

編集ができたら PalmUnit をインストールします.

hoge [~] $ cd prog/PalmUnit/framework
hoge [~/prog/PalmUnit/framework] $ make
hoge [~/prog/PalmUnit/framework] $ make install

これで ~/prog/Palm/include~/prog/Palm/lib にUnit Test に必要なファイルがそろいました. では MPL 用の Unit Test の準備に移りましょう.

~/prog/PalmUnit/src にあるファイルをすべて ~/prog/MPL/Palm/test にコピーしてください. サンプルは今回不要ですので削除しておきます. また AllTests.* も作り直しますので削除しておいてください.

hoge [~] $ cd prog/PalmUnit/src
hoge [~/prog/PalmUnit/src] $ cp * ~/prog/MPL/Palm/test
hoge [~/prog/PalmUnit/src] $ cd ~/prog/MPL/Palm/test
hoge [~/prog/MPL/Palm/test] $ rm Sample*
hoge [~/prog/MPL/Palm/test] $ rm AllTests.*

つぎに Makefile の編集です. OBJ マクロと PALMHOME マクロをつぎのように編集してください.

OBJS = \
	$(TARGET).o  \
	PalmTestResult.o \
	TestRunner.o \
	PalmUnitStopWatch.o \
	../CurrencyTest.o \
	../AllTests.o

PALMHOME = ../../../Palm

最後に PalmUnit.cc を編集します. 先ほど削除したサンプル用のテストを呼んでいるところを削除してください.

<<先頭部分>>
#include "PalmUnit.h"
#include "PalmUnitRsc.h"

//#include "SampleTest.h"
//#include "SampleTest2.h"
#include "../AllTests.h"

//static SampleTest testSample1("Sample Test");
//static SampleTest2 testSample2("Sample Test 2");
<<110 行目あたり>>
		 _Runner.addTest("All Tests", new AllTests);
//		 _Runner.addTest("Sample Test 1", testSample1.suite());
//		 _Runner.addTest("Sample Test 2", testSample2.suite());

これでやっと準備がおわりました. お疲れ様でした. m(__)m

 
 失敗するテスト

準備も終わりましたので早速 Unit Test をつくりましょう. 最初のテスト対象は Currency クラスです. このクラスは文字列から金額をパースしたり,金額を文字列にフォーマットするのに使用します. では次の 7 つファイルを ~/prog/MPL/Palm に用意してください. 各ファイル (currency1.tar.gz) はこちらからダウンロードできます.

  • Makefile
  • AllTests.cc
  • AllTests.h
  • Currency.h
  • Currency.cc
  • CurrencyTest.h
  • CurrencyTest.cc

ではコンパイルしてみましょう. おっとその前に POSEAUTOLOADPATHPOSEAutoload ディレクトリを設定しておいてください. 詳細についてはこちらをどうぞ.

hoge [~] $ cd prog/MPL/Palm
hoge [~/prog/MPL/Palm] make test

うまくコンパイルできましたでしょうか? うまくできたら POSE を起動してみましょう. 次のような画面が表示されれば OK です. PalmUnit アイコンをタップし, AllTests を選択してテストを実行してみてください.

ランチャー 起動後 テスト結果

今回は失敗するようにテストを作成しているので上記のような画面になります.

 
 通貨クラス

いよいよ Unit Test の記述です. ここまで長い道のりでした.^^;

さて, Currency はこんな感じで使うことを想定しています.

// テキストから数値へ変換
Int32 amount = Currency::Parse("1,980");

Char buff[16];
// 数値からテキストへ変換
Char* text = Currency::Format(buff, 2980);

これを Unit Test で書くと次のようになります. CurrencyTest.cctestFail() の代わりに testParse()testFormat() を作成してください. CurrencyTest.h にメソッドの宣言を追加することも忘れないようにお願いします.

#include <TestSuite.h>
#include <TestCaller.h>
#include "CurrencyTest.h"

Test* CurrencyTest::suite() {
    TestSuite* suite = new TestSuite("CurrencyTest");
    suite->addTest(new TestCaller("parse", &CurrencyTest::testParse));
    suite->addTest(new TestCaller("format", &CurrencyTest::testFormat));

    return suite;
}

void CurrencyTest::testParse() {
   AssertEquals(1980, Currency::Parse("1,980"));
}

void CurrencyTest::testFormat() {
   Char buff[16];
   AssertEquals("2,980", Currency::Format(buff, 2980));
}
		 

ではコンパイルしてみましょう. え?,失敗するに決まってるって? それが狙いなのです. XP というプログラミングスタイルでは,まずテストを記述します. そしてそれを実行して失敗するところから始めるのです. 失敗するとその原因を取り除くもっともシンプルな解法を実装していきます.

ここでは CurrencyParse()Format() メソッドがないのでコンパイルエラーが発生しました. では Currency.h にメソッドを追加しましょう. もっともシンプルな解法ということですので,ただ単に 0 や引数で与えられたバッファをそのまま返すことにします.

#ifndef __CURRENCY_H__
#define __CURRENCY_H__

#include <PalmOS.h>

class Currency
{
   public:
      static Int32 Parse(const Char *str) {
         return 0;
      }

      static Char *Format(Char *buff, Int32 amount) {
         return buff;
      }
};

#endif __CURRENCY_H__

さて今度はどうでしょうか. コンパイルはちゃんととおりました. POSE で実行してみるとどうでしょう. POSE をリセットしてみてください. make test でコンパイルしていれば新しい PalmUnit.prcPOSEAUTOLOADPATH にコピーされていますので,リセットするだけ再ロードできます.

PalmUnit を実行すると Failures が 2 個になるはずです. 一覧をタップしてみてください. 次のようにその失敗の内容がポップアップします.

失敗1 失敗2

このように PalmUnit はなんと言う名前のテスト (parse) のどういう期待値 (expected:<1980>) が実際にはどういう値 (but was:<0>)だったのかを,そのテスト個所(CurrencyTest.cc:14) とともに報告してくれます.

どうです. ちょっと便利なような気がしませんか. それとも「なんかピンとこない」といったところでしょうか.

まぁそれはさておき,先に進みましょうか. これらはテストですからパスするようにしなければなりません. ということで Currency クラスを実装します. できるだけシンプルに...

#include "Currency.h"

Int32 Currency::Parse(const Char *str) {
   Int16 l = ::StrLen(str);
   Char *p = (Char *)::MemPtrNew(l + 1);
   Int16 j = 0;
   for (Int16 i = 0; i < l; i++) {
      if (str[i] >= '0' && str[i] <= '9') {
         p[j] = str[i];
         j++;
      }
   }
   p[j] = '\0';
   Int32 amount = ::StrAToI(p);
   ::MemPtrFree(p);
   return amount;
}

Char *Currency::Format(Char *buff, Int32 amount) {
   ::StrIToA(buff, amount);
   Int16 i = ::StrLen(buff);
   while (i > 3) {
      Int16 c = i;
      i -= 3;
      ::MemMove(&buff[i+1], &buff[i], c);
      buff[i] = ',';
   }
   return buff;
}

Currency.ccの実装はこんな感じでしょうか. (ぜんぜんシンプルじゃない?) 各ファイル (currency2.tar.gz) はこちらからダウンロードできます. test ディレクトリの Makefile も変更しているので注意してください.

コンパイルして POSE で動かしてみてください. 今度はテストをパスするはずです. これで少なくとも Unit Test に記述したテストケースについては正しく動くことが確かめられました. では他のケースではどうでしょうか. もっといろいろなテストを追加してみましょう.

void CurrencyTest::testParse() {
   AssertEquals(1980, Currency::Parse("1,980"));
   AssertEquals(0, Currency::Parse("0"));
   AssertEquals(10, Currency::Parse("10"));
   AssertEquals(1980, Currency::Parse("1980"));
   AssertEquals(10000000, Currency::Parse("10,000,000"));
}

void CurrencyTest::testFormat() {
   Char buff[16];
   AssertEquals("2,980", Currency::Format(buff, 2980));
   AssertEquals("0", Currency::Format(buff, 0));
   AssertEquals("10", Currency::Format(buff, 10));
   AssertEquals("10,000,000", Currency::Format(buff, 10000000));
}

テスト結果はいかがでしたか? testFormat の最後のテストが失敗してしまったと思います. どうやら ::MemMove() で移動するバイト数に問題がありそうです. ぜひご自分で直してみてください. テストがパスするようになったら,さらに負の金額もちゃんと処理できるように修正してみてください.

どうでしょうか. こんな感じでちょっとずつ「実装してはテストを行う」というサイクルを繰り返すことで,どこでおかしくなったのか,なにがおかしいのか,ということを追いやすくなります.

負の金額を処理できるようになった Currency のコードとテスト (currency3.tar.gz) はこちらからダウンロードできます.

 
 通貨クラスの機能拡張

ところで, Palm では PrefsFormats で数値のフォーマットを指定することができるようになっています. ここには次のような候補がリストされます.

  • 1,000.00
  • 1.000,00
  • 1 000,00
  • 1'000.00
  • 1'000,00

これらの数値フォーマットは SDK ヘッダファイルの中で,次のように定義されています.

typedef enum {
    nfCommaPeriod,
    nfPeriodComma,
    nfSpaceComma,
    nfApostrophePeriod,
    nfApostropheComma
    } NumberFormatType;

MPL を海外のユーザにも使ってもらうためには, Currency クラスをこれら全てのフォーマットに対応させたいところです. でも完成したコードには手を出したくないし...     大丈夫! Unit Test があれば,絶えず拡張前の仕様を満たしているかチェックしながら作業が進められるのでバグの入り込む余地はありません. では作業にとりかかりましょう.

まず最初にテストケースから考えましょう. Currency クラスの Parse()Format() はクラスメソッドですので,数値フォーマット指定と小数以下の桁数指定もクラスメソッドにします. 現在の設定値を知る必要は今のところないので指定メソッドだけで取得メソッドは必要ないですね. 新しく追加されるテストは以下のようなものでしょう. もちろんもっと境界条件をカバーしておくべきですが,今回はこんなところで許してください.

#include <TestSuite.h>
#include <TestCaller.h>
#include "CurrencyTest.h"

Test* CurrencyTest::suite() {
    TestSuite* suite = new TestSuite("CurrencyTest");
	suite->addTest(new TestCaller("parse", &CurrencyTest::testParse));
	suite->addTest(new TestCaller("format", &CurrencyTest::testFormat));
	suite->addTest(new TestCaller("parse2", &CurrencyTest::testParse_periodComma));
	suite->addTest(new TestCaller("format2", &CurrencyTest::testFormat_periodComma));

    return suite;
}

// 途中 省略 ...

void CurrencyTest::testParse_periodComma() {
   Currency::SetNumberFormatType(nfPeriodComma);
   Currency::SetDecimalPlaces(2);
   AssertEquals(1980, Currency::Parse("19,80"));
   AssertEquals(0, Currency::Parse("0"));
   AssertEquals(1000, Currency::Parse("10"));
   AssertEquals(198000, Currency::Parse("1980"));
   AssertEquals(10000000, Currency::Parse("100.000,00"));
   AssertEquals(-1000, Currency::Parse("-10"));
   AssertEquals(-1980, Currency::Parse("-19,80"));
   AssertEquals(-10000000, Currency::Parse("-100.000,00"));
}

void CurrencyTest::testFormat_periodComma() {
   Char buff[16];
   Currency::SetNumberFormatType(nfPeriodComma);
   Currency::SetDecimalPlaces(2);
   AssertEquals("29,80", Currency::Format(buff, 2980));
   AssertEquals("0,00", Currency::Format(buff, 0));
   AssertEquals("0,10", Currency::Format(buff, 10));
   AssertEquals("100.000,00", Currency::Format(buff, 10000000));
   AssertEquals("-0,10", Currency::Format(buff, -10));
   AssertEquals("-29,80", Currency::Format(buff, -2980));
   AssertEquals("-100.000,00", Currency::Format(buff, -10000000));
}

さぁコンパイルしてみましょう. 失敗します. じゃぁソースを修正して,コンパイルを通して,テストしてみて,と作業を進めてみてください. ちょっと直してはすぐテストをするようにしてくださいね. そうすることでどの時点でおかしくなったのかが把握しやすくなります.

私の場合のコードとテスト (currency4.tar.gz) がこちらからダウンロードできます. 参考になれば幸いです. 「こうすればもっとシンプルになる」などありましたらどしどしお知らせください. よろしくお願いします.

 
 フォームの実装

すみません,力つきました. 文章を書くのは結構たいへんなのですね. 代わりといってはなんですが GUI を実装した全てのソースを Download のページで公開しておりますので参考にしてください.