[Japanese|English]

Table of contents

  1. Introduction
  2. Specification of fuel efficiency calculation program
  3. Preparation
  4. Failure Test
  5. Currency Class
  6. Extended Currency Class
  7. Form implementation
 Introduction

This page is planned to explain the usage of PalmUnit by playing with it in practice.

I wish automatic unit test would enable developers to lessen debugging labor, and efficiently implement application. This might be too much to say :-)

Please let me know if you want to say, "We have much better way!", or "No so good." Feel free to send email to catoo.

Here, you will implement "Fuel efficiency calculation program" as an exercise.

 
 Specification of fuel efficiency calculation program

Because easy example is enough here, following use-cases are the exercise here.

  • Data input

    On 2/12/2001, you go shopping, and find that gasoline remains little. So, you refill at a gas station. It costs \3,412 to fully refill by cash. The receipt tells unit price is \104 per liter and the amount of gasoline is 31.33 liter. The consumer tax is \163. The trip meter shows exact 300 km. Then, you use Palm to input this information. Calculated fuel efficiency is 9.58 km/liter.

  • Data reference

    Nowadays fuel efficiency seems to be not good, so you check recent fuel efficiency in a look. No volatility is observed.

Based on the above description, we need detail editing and list display windows for the program. I always make a prototype using text data in order to make it concrete. I think text-based prototype is very convenient, because it is very easy to move text back and forth, and add some comments. Sounds good?

[Fuel display window]

+------+
|Refill|             ▼ My Car  <- Select car type
+------+----------------------+
|    Date: [2/12/01]          | <- Select date
| Mileage: [0][0][0][3][0][0] | <- Count-up buttons to add number
|  Vendor: ▼xxxx stand______ | <- Save 10 recently visited gas stations
|   Grade: ▼Regular          | <- Select gasoline grade (e.g. regular, plus) from pre-registered list
|  Amount:__31.33 ▼Liter     | <- Enable to select liter/gallon (2 unit decimal)
|    Cost:___3412             | <- Input total cost
|    Note:___________________ |
|         ___________________ |
|         ___________________ |
|         ___________________ |
|         ___________________ |
|         ___________________ |
|                             |
|(Done)                       | <- Move to list display window
+-----------------------------+

[List window]

+---+
|MPL|                ▼ My Car  <- Select car type
+---+-------------------------+
|Date  Vendor       ▼MPL     | <- Enable to select various data types
|2/12  xxxx sta...     9.58 ↑|    (e.g. MPL, MPG, cost, unit price, etc.)
|                             |
|                             |
|                             |
|                             |
|                             |
|                             |
|                             |
|                             |
|                             |
|                             |
|                           ↓|
|(Refill)                     | <- Move to refill window
+-----------------------------+

[Configuration window]

+-----------------------------+
|        Preferences          | 
+-----------------------------+
|Currency Decimal Places: ▼2 | <- Input currency decimal places
|Default Unit: ▼Liter        | <- Select default unit
|Sort by : ▼Date Des.        | <- Select sort sequence
|                             |
|(OK)(Cancel)                 |
+-----------------------------+

By the way, let us call our program as MPL, meaning Mileage Per Liter.

 
 Preparation

Here, let's prepare for our implementation. There are so many things to do, but please stay with me. First, we need to make working directory. Following shows the directory structure holding PalmUnit header files and libraries.

~/                          home directory
 |
 +-- prog/                  directory for programs
     |
     +-- Palm               directory for shared programs
     |   |
     |   +-- include        directory for header files (CatooCraftLibrary heade files)
     |   |
     |   +-- lib            directory for libraries (CatooCraftLibrary library file)
     |
     +-- PalmUnit           directory for PalmUnit
     |   |
     |   +-- doc
     |   |
     |   +-- framework
     |   |
     |   +-- src
     |
     +-- MPL                directory for fuel efficiency program
         |
         +-- Palm           directory for Palm (Conduit directory may be created in the future extension)
             |
             +-- test       directory for Unit Test

Next, let's install PalmUnit header files and libraries! Please modify INSTALLDIR macro in ~/prog/PalmUnit/framework/Makefile as follows. After that, just make it.

## Makefile for PalmUnit Framework

TARGET = PalmUnit.a
SDK = -palmos3.5

INSTALLDIR = ../../Palm          # <- relative directory pass of ~/prog/Palm

After editing, please install PalmUnit as follows:

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

Now, necessary files for Unit Test are saved in ~/prog/Palm/include and ~/prog/Palm/lib. Then, let's move on to preparation of Unit Test for MPL.

Please copy all files in ~/prog/PalmUnit/src to ~/prog/MPL/Palm/test. Since sample files are unnecessary, please delete them. Moreover, please delete AllTests.*, because we will create them again.

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.*

Next, we need to edit Makefile. OBJ and PALMHOME macro should be edited as follows:

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

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

Finally, please edit PalmUnit.cc. Please delete such lines calling sample test files, which were deleted before.

<<Top of file>>
#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");
<<Around line #110>>
		 _Runner.addTest("All Tests", new AllTests);
//		 _Runner.addTest("Sample Test 1", testSample1.suite());
//		 _Runner.addTest("Sample Test 2", testSample2.suite());

We have finished preparation. Good job!

 
 Failure Test

Since preparation is over, let's make unit test cases. First of all, Currency class will be tested. This class parses the cost from strings, and formats the cost into strings. Please prepare following seven files in ~/prog/MPL/Palm. These files(currency1.tar.gz) can be downloaded here

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

Let's compile it! Ops! Before compilation, please set POSEAUTOLOADPATH as directory of POSE. If you want more detail information, please refer to here.

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

Can you successfully compile it? If the answer is "yes", let's start POSE. If following window is displayed, you have successfully finished. Now, tap PalmUnit icon, and start unit test by selecting AllTests and tapping run button.

launcher launched result

Because this test case should cause failure, you will see the above display.

 
 Currency Class

Now, let's write-up Unit Test. It was a long distance up to here. :-)

Currency is supposed to be used as follows:

// Transformation from numeric to text
Int32 amount = Currency::Parse("1,980");

Char buff[16];
// Transformation from text to numerics
Char* text = Currency::Format(buff, 2980);

This can be specified in Unit Test as follows: Do not forget to make testParse() and testFormat() in the place of testFail() in CurrencyTest.cc. Please do not forget to add declaration of method in 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));
}
		 

Let's compile now! However, you would say compilation could fail. That's my intention. In XP, we begin with describing test cases before testing. Then, encountering failures by executing program is our starting point. Failures let us to remove the cause. This simplest style is the implementation of XP.

Here, compilation error happens, because Parse() and Format() are missing in Currency. Therefore, let's add necessary methods in Currency.h. We need to solve in simplest way, so the methods only return 0 or buffer given as arguments.

#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__

What happens this time? Compilation should successfully finish. How about executing POSE? Please reset POSE. If you compile by executing make test, then new PalmUnit.prc should be copied in POSEAUTOLOADPATH. Thus, reset will load program again.

This time, PalmUnit will produce two failures. Please tap list. You will see failure results as follows:

failure 1 failure 2

Like this, PalmUnit can report the name of the test(parse), the expected value (expected:<1980>), the actual value (but was:<0>), and the place (CurrencyTest.cc:14) of source.

Cool!? Do you want to try it? Or, not sure?

Anyway, let's move on. We need to modify them in order to succeed in these testing. Thus, we shall implement Currency class. As simple as possible...

#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 will be implemented like this. (looks complicated?) The compressed file (currency2.tar.gz) can be downloaded here. Note that Makefile in test directory is also changed.

After compiling again, please start POSE. Now, you should be able to pass all tests. We make certain that the current program can operate under the test cases of Unit Test. Then, how about other test cases? Why not add more test cases!

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));
}

How about the results of test cases? Last test case of testFormat should be failed. It seems that ::MemMove() cannot correctly process the number of bytes moved. Please modify it by yourself. If you can correct it, please modify source code in order to calculate negative cost.

Are you keeping up with us? By iterating these "Implement and test" cycles step by step, you can easily follow "where is the problem", and "what is wrong."

Correct Currency source code and test cases (currency3.tar.gz) can be downloaded here.

 
 Extended Currency Class

Palm can specify numeric format at Formats in Prefs. Following shows a list of possible formats.

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

These formats are defined in the SDK header files as follows:

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

In order to enable MPL to be used all over the world, Currency class should correspond with these all formats. However, I hesitate to change my completed source codes... No problem! Unit Test ensures that no bug will be injected, because source codes will be continuously checked against the existing specifications. Then, let's go on to extend them.

First of all, let's consider test cases. Because Parse() and Format() are Currency class's methods, numerical format and currency decimal places can be set through methods. Since we do not have to know the current value, only methods to set will be necessary. Necessary test cases should as follows: Without doubt, boundary conditions should be covered, but further test cases may be added in the future, not now. (It depends on you)

#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;
}

// ... snip ...

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));
}

Let's compile it! It should fail. Then, modify source codes, compile again, and test it, and so on. Please modify a little and test it soon. By doing so, it will be easy for us to understand when errors happen.

My source codes and test cases (currency4.tar.gz) can be downloaded here . If it is helpful, it will be happy. Please let me know further improvements. Thanks.

 
 Form implementation

Sorry, I am exhausted. Writing documents is pretty hard. As a substitute for further documents, all source codes implementing GUI can be downloaded in Download.