« "Sum and Product Puzzle"をPythonで解いてみた | メイン | 角換わり棒銀の定跡形で悩む »

 CppUTestでテスト駆動開発(1)

図書館で、「テスト駆動開発による組み込みプログラミング」という本に目を引かれ、流し読みしたら結構面白かったので、借りて帰ったのだが、筆者はこの所公私共に多忙で、ほとんど読めないまま返却期限を迎えてしまった。
とりあえず、その本で推薦されていた、CppUTestというユニットテストフレームワークを使ってみた。

筆者は、凄まじく高コストで生産性の低い日本企業のソフトウェア開発を数年間目の当たりにした後、21世紀に入った頃にテストファーストを含むExtreme Programmingの考え方に全面的に共感し、その時にCUnitというユニットテストフレームワークを使ったことがあるのだが、その価値がわからなかった。わざわざそんなのを使わなくても、ユニットテストを駆動する短いテストドライバを自分で書けば事足りると思ったし、実際、これまでそのようにしてきて困ったことが無い。

しかし、今回、この本でUnityやCppUTestの話を読むと、ユニットテストフレームワークには最先端の考え方が反映されており、この枠に嵌められてユニットテストを書いたりTDDをすれば、手軽に世界のトップクラスのインテリジェンスに触れられるような気がして、面白そうだと思った。

そこで、CppUTestを使ってみることにした。

書籍の第3章のLEDドライバの例を題材に、TDD(テスト駆動開発)方式で、まずは未実装のエラーが出る実行可能なテストを作ってみる。

LedDriver/LedDriverTest.cpp
#include "CppUTest/TestHarness.h"

TEST_GROUP(LedDriver)
{
  void setup()
  {
  }
  void teardown()
  {
  }
};

TEST(LedDriver, LedsOffAfterCreate)
{
  FAIL("Start here");
}
test_main.cpp
//from README.md
#include <CppUTest/CommandLineTestRunner.h>

int main(int ac, char** av)
{
   return CommandLineTestRunner::RunAllTests(ac, av);
}
Makefile
CXX = g++
CPPUTEST_HOME := $(HOME)/tmp/cpputest

#from README.md
CPPFLAGS += -I$(CPPUTEST_HOME)/include
CXXFLAGS += -include $(CPPUTEST_HOME)/include/CppUTest/MemoryLeakDetectorNewMacros.h
CFLAGS += -include $(CPPUTEST_HOME)/include/CppUTest/MemoryLeakDetectorMallocMacros.h
LD_LIBRARIES = -L$(CPPUTEST_HOME)/lib -lCppUTest -lCppUTestExt

TARGET = test_main
SRCS = test_main.cpp LedDriver/LedDriverTest.cpp
OBJS = $(SRCS:.cpp=.o)

all: $(TARGET)

$(TARGET): $(OBJS)
	$(CXX) -o $@ $^ $(CXXFLAGS) $(LD_LIBRARIES)

%.o: %.cpp
#	GNU make implicit rule + "-o $@"
	$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@

.PHONY: check
check: $(TARGET)
	./$(TARGET) -v

.PHONY: clean
clean:
	rm -f $(TARGET) $(OBJS) *.gcno *.gcov *~ */*~
	find . -name "*.gcda" | xargs rm -f

これでmake checkとすると、1つのテストが失敗し、"Start here"と表示される。
筆者は普段、テストの実行はmake testであるが、make checkが多数派らしいので、今後はそれに倣うことにした。(make testはPerlの世界に多い?)

MakefileにLedDriver.cのコンパイルが無いのは、TDDの、エラーにならない限りは作らない原則に従っているからである。

次に、LedDriver/LedDriverTest.cppにテストを追加し、それがpassするようにLedDriver.cを開発するのを繰り返す。
途中と詳細説明を省略するが、例えば途中段階では次のようになった。

LedDriver/LedDriverTest.cpp
#include "CppUTest/TestHarness.h"
extern "C" {
#include "LedDriver.h"
}

static uint16_t virtualLeds;

TEST_GROUP(LedDriver)
{
  void setup()
  {
    LedDriver_Create(&virtualLeds);
  }
  void teardown()
  {
  }
};

TEST(LedDriver, LedsOffAfterCreate)
{
	uint16_t virtualLeds = 0xffff;
	LedDriver_Create(&virtualLeds);
	LONGS_EQUAL(0, virtualLeds);
}

TEST(LedDriver, TurnOnLedOne)
{
	LedDriver_TurnOn(1);
	LONGS_EQUAL(1, virtualLeds);
}

TEST(LedDriver, TurnOffLedOne)
{
	LedDriver_TurnOn(1);
	LedDriver_TurnOff(1);
	LONGS_EQUAL(0, virtualLeds);
}

TEST(LedDriver, TurnOnMultipleLeds)
{
	LedDriver_TurnOn(9);
	LedDriver_TurnOn(8);
	LONGS_EQUAL(0x0180, virtualLeds);
}

TEST(LedDriver, AllOn)
{
	LedDriver_TurnAllOn();
	LONGS_EQUAL(0xffff, virtualLeds);
}
	
TEST(LedDriver, TurnOffAnyLed)
{
	LedDriver_TurnAllOn();
	LedDriver_TurnOff(8);
	LONGS_EQUAL(0xff7f, virtualLeds);
}

IGNORE_TEST(LedDriver, LedMemoryIsNotReadable)
{
	// TODO: let LED state read-only
}
LedDriver/LedDriver.h
#pragma once
#include <stdint.h>

extern void LedDriver_Create(uint16_t *address);
extern void LedDriver_Destroy(void);
extern void LedDriver_TurnOn(int ledNumber);
extern void LedDriver_TurnOff(int ledNumber);
extern void LedDriver_TurnAllOn(void);
LedDriver/LedDriver.c
#include "LedDriver.h"

enum {ALL_LEDS_ON = ~0, ALL_LEDS_OFF = ~ALL_LEDS_ON};

static uint16_t *ledsAddress;

static uint16_t convertLedNumberToBit(int ledNumber)
{
	return 1 << (ledNumber - 1);
}

void LedDriver_Create(uint16_t *address)
{
	ledsAddress = address;
	*ledsAddress = ALL_LEDS_OFF;
}

void LedDriver_Destroy(void)
{
}

void LedDriver_TurnOn(int ledNumber)
{
	*ledsAddress |= convertLedNumberToBit(ledNumber);
}

void LedDriver_TurnOff(int ledNumber)
{
	*ledsAddress &= ~convertLedNumberToBit(ledNumber);
}

void LedDriver_TurnAllOn(void)
{
	*ledsAddress = ALL_LEDS_ON;
}
Makefile
CXX = g++
CC = gcc
CPPUTEST_HOME := $(HOME)/tmp/cpputest

#from README.md
CPPFLAGS += -I$(CPPUTEST_HOME)/include
CXXFLAGS += -include $(CPPUTEST_HOME)/include/CppUTest/MemoryLeakDetectorNewMacros.h
CFLAGS += -include $(CPPUTEST_HOME)/include/CppUTest/MemoryLeakDetectorMallocMacros.h
LD_LIBRARIES = -L$(CPPUTEST_HOME)/lib -lCppUTest -lCppUTestExt

TARGET = test_main
SRCS = test_main.cpp LedDriver/LedDriverTest.cpp
CSRCS = LedDriver/LedDriver.c
OBJS = $(SRCS:.cpp=.o) $(CSRCS:.c=.o)

all: $(TARGET)

$(TARGET): $(OBJS)
	$(CXX) -o $@ $^ $(CXXFLAGS) $(LD_LIBRARIES)

%.o: %.cpp
#	GNU make implicit rule + "-o $@"
	$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@

%.o: %.c
#	GNU make implicit rule + "-o $@"
	$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

.PHONY: check
check: $(TARGET)
	./$(TARGET) -v

.PHONY: clean
clean:
	rm -f $(TARGET) $(OBJS) *.gcno *.gcov *~ */*~
	find . -name "*.gcda" | xargs rm -f

make checkすると、"OK (7 tests, 6 ran, 6 checks, 1 ignored, ...)"となる。
ignoredなのは「実行可能なリマインダ」である。

ところで、CppUTestには簡単なメモリリーク検出機能がある。各TEST()の実行前にTEST_GROUPのsetup()が、実行後にteardown()が呼び出されるが、そのsetup()前とteardown()後とでメモリ確保状態を比較するようである。
試しに、LedDriver/LedDriver.cのLedDriver_TurnAllOn()を

#include <stdlib.h>
void LedDriver_TurnAllOn(void)
{
	void *p = malloc(1);
	*ledsAddress = ALL_LEDS_ON;
}
に変えてmake checkとすると、次のエラーメッセージが出た。
TEST(LedDriver, AllOn)
LedDriver/LedDriverTest.cpp:46: error: Failure in TEST(LedDriver, AllOn)
	Memory leak(s) found.
Alloc num (6) Leak size: 1 Allocated at: LedDriver/LedDriver.c and line: 35. Type: "malloc"

うまく使えば便利そうである。
CppUTestに触れてみて、TDDをきちんと勉強しようと思った。

CUnitは価値がわからなかったと書いたが、後で考えると、見よう見まねで使ってみただけで、その背景となる考え方や、上手な使い方を読まなかったからなのだろう。

5/22に、http://cpputest.github.ioから当時の最新版である3.7.1をダウンロードして、Mac OS X 10.7.5で
./configure
make
make check
とやると、make checkでFAILが出て、出鼻をくじかれてしまった。
ログを見ると、TEST(UtestShell, TestDefaultCrashMethodInSeparateProcessTest)が"killed by signal 4"で失敗したようだった。
./configureをする前にautogen.shを実行しても同じだった。
3.7.2(3.8dev)でも同じだった。

HomebrewというMacのパッケージ管理システムを使えば簡単にインストールできそうであるが、Homebrewはパッケージを/usr/local/にインストールするので、筆者にはハードルが高い。筆者はMacPortsを使っているが、何かおかしな状態になって管理不能なものが発生すれば、インストールディレクトリである/opt/local/を丸ごと消して、全てを再インストールしている。/usr/local/にインストールされたものが管理不能になったら大変である。それに、Rやmaximaなど、筆者がMacPortsでインストールしている多くのパッケージがHomebrewに無いので、乗り換える気にはなれない。
しかし、CppUTestのインストーラーはMacPortsには用意されなさそうである。このように、今後、Homebrewにしかインストーラーが用意されないパッケージがいくつか出てきそうだと考えると、MacPortsとHomebrewを併用すべきなのかも知れないが、筆者のMacbook AirのSSDには空き容量が無いので、今回は断念した。

結局、
git clone git://github.com/cpputest/cpputest.git
とやってソースコードを取得すると、同じ手順でmake checkが通った。
しかも、5/25に3.8がリリースされたようで、これをダウンロードすると、あっさりmake checkが通った。何とタイミングの悪かったことか。

トラックバック

このエントリーのトラックバックURL:
http://ynomura.dip.jp/cgi-bin/mt/mt-tb.cgi/234

コメント投稿フォーム

※投稿されたコメントはオーナーが承認するまで表示されません。


Powered by Movable Type 3.35