M5Stackと温湿度センサDHT12で始めるTDDと同値分割・境界値分析

##1. はじめに
以下のような偶然が重なりこれはもうやってみるしかないなと。

- [ソフトウェアテスト技法練習帳](https://gihyo.jp/book/2020/978-4-297-11061-1)を買って読んだら1問目が室温計
- [M5Stack](https://www.switch-science.com/catalog/3648/)が手元にある
- 温湿度センサ[DHT12](https://www.switch-science.com/catalog/3651/)([データシート](https://docid81hrs3j1.cloudfront.net/medialibrary/2018/02/DHT12_Datasheet.pdf))も手元にある
- t_wadaさんの[動作するきれいなコード: SeleniumConf Tokyo 2019 基調講演文字起こし+α](https://t-wada.hatenablog.jp/entry/clean-code-that-works)を読んでTDDを試したくなった
- AVRに加えESP32でもArduino用のUnit Testフレームワークである[ArduinoUnit](https://github.com/mmurdoch/arduinounit/)や[AUnit](https://github.com/bxparks/AUnit)が使えることが分かった
  - Arduino UNOとArduinoUnitでUnit Testを行う記事は[こちら](https://qiita.com/pbjpkas/items/d10a2d0a1af68aa6efdc)です

DHT12の[サンプルコード](https://github.com/m5stack/M5Stack/tree/master/examples/Modules/DHT12)をベースにTDDを取り入れて室温計を開発します。

##2. 仕様
[ソフトウェアテスト技法練習帳](https://gihyo.jp/book/2020/978-4-297-11061-1)の一番最初のお題である「温度によって表示を変えるペット用室温計」をアレンジします。

- 室温計のディスプレイに1.室温、2.メッセージ表示、を行う
- ディスプレイは2秒ごとに更新する
- 室温は0.1℃刻みとし以下の表示に従う
  - 温湿度センサDHT12の計測範囲外(-20℃未満および60℃を超える温度)は考慮不要とする

|No.|室温|表示|
|----|----|----|
| 1 | -10.0℃未満 | under -10.0 |
| 2 | -10.0℃以上40.0℃以下 | -10.0～40.0 |
| 3 | 40.0℃を超える| over 40.0 |

- メッセージ表示は以下の表に従う

|No.|室温|表示メッセージ
|----|----|----|
| 1 | 20.0℃未満 | cold |
| 2 | 20.0℃以上25.0℃以下 | comfortable |
| 3 | 25.0℃を超える | hot |

##3. 一つの関数にすべてを詰め込んだ実装
ひとまずTDDは脇に置き、実現可能性の調査や仕上がりの大まかな把握といった目的でプロトタイプ開発を行うテイでmonolithic_implementation()にすべてを詰め込んだ実装例を以下に示します[^1]。

半日程度の開発で以下の成果を得ることができました。

- 室温およびメッセージの画面レイアウトはほぼ決定
- 室温のフォントを7セグ風のものとすることでほぼ決定
- <strike>0℃未満の環境で筆者の持っているDHT12から温度を読み出すと意図しない値を返すことが判明</strike>
  - <strike>温度を下げてゆくと … 0.2 → 0.1 → 0.0 → 12.9 → 13.0 → 13.1 … という値が返ってくる</strike>
  - <strike>たまたま(故障などで)このような症状を呈するのかソフトの不具合なのかの切り分けはまだ行っていません</strike>
  - <strike>DHT12::readTemperature()のバグ(DHT12のByte Addr 0x03(Temperature scale)のBit8(温度の正負を示すビット)の処理漏れ)と判明し、付録X.2に修正版を掲載しました(2020/1/12)</strike>
  - 最新版のDHT12.cppをご使用ください(修正版を[M5Stack](https://github.com/m5stack/M5Stack)にプルリクエストしマージ済みです)(2020/1/13)

![MyM5StackThermo.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/578d17ee-304d-4b72-b043-adb452ec9c31.jpeg)

```c:MyM5StackThermometer.ino
/*
  This code is based on
  https://github.com/m5stack/M5Stack/tree/master/examples/Modules/DHT12
 */
/*
For configuration library:
DHT12 dht12("Scale temperature","ID device for I2C");
On "Scale temperature" you can select the preset scale:
CELSIUS, FAHRENHEIT or KELVIN.
And on "ID device", you can put ID sensor, on DHT12
normally is 0x5c.
Examples:
  DHT12 dht12;
The preset scale is CELSIUS and ID is 0x5c.
  DHT12 dht12(KELVIN);
the preset scale is KELVIN and ID is 0x5c.
  DHT12 dht12(FAHRENHEIT,0x53);
The preset scale is FAHRENHEIT and ID is 0x53.
*/
#include <stdio.h>  //sprintf
#include <string.h> //strcpy
#include <M5Stack.h>
#include "DHT12.h"
#include <Wire.h>     //The DHT12 uses I2C comunication.
DHT12 dht12;          //Preset scale CELSIUS and ID 0x5c.
//#line 27 "MyM5StackThermometer.ino"
//#include <AUnit.h>

#define STRLEN  32

//On-Screen Display Layout
#define OSD_X    0
#define OSD_Y   60
#define OSD_W  320
#define OSD_H  120

#define OSD_BG_COLOR TFT_WHITE
#define OSD_FG_COLOR TFT_GREEN

//message strings
char msg_01[] = "under -10.0";
char msg_02[] = "over 40.0";
char msg_11[] = "cold";
char msg_12[] = "comfortable";
char msg_13[] = "hot";

void monolithic_implementation()
{
  char str[STRLEN] = {'\0'};

  // M5.Lcd.drawCentreString(const char *string, int dX, int poY, int font);
  // Only font numbers 2,4,6,7 are valid. Font 6 only contains characters [space] 0 1 2 3 4 5 6 7 8 9 : . - a p m
  // Font 7 is a 7 segment font and only contains characters [space] 0 1 2 3 4 5 6 7 8 9 : .
  // see: https://github.com/m5stack/M5Stack/blob/master/examples/Advanced/Display/TFT_Clock/TFT_Clock.ino
  int font = 4;

  float temperature = dht12.readTemperature();

  Serial.print("Temperature: ");
  Serial.println(temperature);
  
  M5.Lcd.fillRect(OSD_X, OSD_Y, OSD_W, OSD_H, OSD_BG_COLOR);
  M5.Lcd.setTextColor(OSD_FG_COLOR);
  
  if(temperature < -10.0)
  {
    strcpy(str, msg_01);
    font = 4;
  }
  else if(40.0 < temperature)
  {
    strcpy(str, msg_02);
    font = 4;
  }
  else
  {
    sprintf(str, "%.1f", temperature);
    font = 7;
  }
  M5.Lcd.drawCentreString(str, 160, 80, font);
  
  if(temperature < 20.0)
  {
    strcpy(str, msg_11);
  }
  else if(25.0 < temperature)
  {
    strcpy(str, msg_13);
  }
  else
  {
    strcpy(str, msg_12);
  }
  M5.Lcd.drawCentreString(str, 160, 144, 4);
}

void setup() {
  M5.begin();
  Wire.begin();
  Serial.begin(115200);
}

void loop() {
  monolithic_implementation();
  delay(2000);
}
```

##4. テストで開発を駆動する
3章のプログラムはプロトタイプ評価に役立った実際に動くものですが量産を前提としたソフトウェア設計という視点で眺めると以下の理由でとってもテストがしづらいです。

- 温湿度センサの値の取得、ディスプレイに表示する文字列の生成、ディスプレイへの表示が一体化していて機能ごとのチェックができない
- テストを行うには室温計を恒温槽に入れてセンサが所定の温度を返すのを待つ必要がある
- システムテストにテストが集中していて不具合摘出のフロントローディングができない

そもそもシステムテストの工程ではシステムテストでないとできないテストを実施したいわけで、設計を工夫することでシステムテストで行っているテストをUnit Testや結合テストに前倒しできるならそうしたいです。そこで、1.テストすべき事柄を挙げる、2.フロントローディングできるよう設計を工夫する、3.実装する、4.テストする、という手順で開発します。

TDDというと1.Unit Testを書く→2.Unit Testがグリーンになるプロダクトのコードを書く→3.リファクタリングするというサイクルを回すことを思い浮かべますが、テストが開発を駆動するという点でこれもまたTDD[^2]です。

###4.1 テスト観点(テストすべき事柄)の洗い出し
以下にテスト観点を抜粋します[^3]：

1. 表示される室温が規定の誤差の範囲内に収まっていること
2. センサの出力を正しくデコードしていること
3. 室温やメッセージの表示が仕様通りであること
4. 室温やメッセージが欠けたり余計なものが表示されないこと

###4.2 どの工程でどのようなテストを行うかを設計する

|No.|観点|どの工程でテストしたいか|
|----|----|----|
|1|表示される室温が規定の誤差の範囲内に収まっていること|ソフトウェアというよりハードウェアのテストに近い気がしますが室温計の実機でシステム動作を行ってテストしたい。
|2|センサの出力を正しくデコードしていること|本物のセンサを使って結合レベルでチェックするか、センサをエミュレーションしてUnit Testでチェックしたい。
|3|室温やメッセージの表示が仕様通りであること|出力(室温やメッセージの表示内容)が入力(室温)によって一意に決まるのであれば結合テストではなくUnit Testでチェックしたい。
|4|室温やメッセージが欠けたり余計なものが表示されないこと|結合テストの段階で不具合を捕まえたい。

もっともいくら「センサをエミュレーションしてUnit Testでチェックしたい」といっても実現する技術や手段を持ち合わせていなければできないのでNo.2は結合レベルでチェックすることとします(本稿では割愛します)。これらをテストコンテナにまとめます[^4]。

![MyM5StackThermometer-test-container.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/1e91b809-3eae-ff69-26c4-a7756357adb1.png)

###4.3 ソフトウェアの構造を設計する
####4.3.1 要件

- (プロトタイプの評価を踏まえ)起動時に画面を白で塗りつぶすなど何かしらの初期化を行い、画面に黒帯がないこと
- 室温やメッセージの文字列を生成するプログラムはUnit Testができること
- 任意の文字列を画面に表示する機能を提供し、表示の目視確認ができること
- 2章の仕様を満たすこと

####4.3.2 起動時処理
setup()からinitDisplay()を呼び出して初期画面を描画します。

![MyM5Stack Thermometer setup.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/a29727cd-12de-b00d-9b16-9a64d7d77feb.png)

※それっぽいシーケンス図を描いていますが実際はただのC言語の関数呼び出しです。

####4.3.3 室温計プログラム

- 室温計のプログラムはloop()からmyM5StackThermometer()を2秒ごとに呼び出して実行します。
- UARTからコマンド(文字'd')が入力されたらデバッグ機能を呼び出します。
- 任意の文字列を画面に表示する機能をデバッグ機能に備えます。

![MyM5Stack Thermometer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/4e4c9006-be10-8276-96c2-026748caba5f.png)

※それっぽいシーケンス図を描いていますが実際はただのC言語の関数呼び出しです。

##5. initDisplay()の実装
以下にinitDisplay()を示します。

- 画面を白で塗りつぶす
- 画面下部にプログラムおよび開発者の名前を表示する
- 室温とメッセージのテキストの色をグリーンに設定する

```c:initDisplay()
void initDisplay()
{
  M5.Lcd.fillRect(0, 0, 320, 240, OSD_BG_COLOR);
  M5.Lcd.setTextColor(TFT_BLUE);
  M5.Lcd.drawCentreString("MyM5Stack Thermometer by ka's", 160, 220, 2);
  M5.Lcd.setTextColor(TFT_GREEN);
}
```

![init_s.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/14cf7497-e306-4c4d-c556-99b83e1e5960.jpeg)

##6. genTemperatureStr()とgenMessageStr()の実装
###6.1 同値分割・境界値分析でテストを設計する
4.3節に登場する関数のうちUnit Testの対象となるのはgenTemperatureStr()とgenMessageStr()です。float型の引数を受け取り文字列を返します。Unit Testを実装するために同値分割や境界値分析を行ってテスト値を決めます。

####6.1.1 genMessageStr()
室温をfloat型で受け取り、表示メッセージの文字列を返します。

#####6.1.1.1 仕様

|No.|室温|表示メッセージ
|----|----|----|
| 1 | 20.0℃未満 | cold |
| 2 | 20.0℃以上25.0℃以下 | comfortable |
| 3 | 25.0℃を超える | hot |

- 室温は0.1℃刻み

#####6.1.1.2 室温と表示メッセージの対応
![MyM5StackThermometer-Spec-Message.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/654a7879-7f1b-a560-cb92-5a45386806cc.png)

#####6.1.1.3 テストケース
同値分割・境界値分析を用いたテストケースを以下に示します。

|No.|テスト値|期待結果(表示メッセージ)|テスト値の根拠
|----|----|----|----
|1|15.0|cold|coldの代表値
|2|19.9|cold|coldの境界値(上限)
|3|20.0|comfortable|comfortableの境界値(下限)
|4|22.5|comfortable|comfortableの代表値
|5|25.0|comfortable|comfortableの境界値(上限)
|6|25.1|hot|hotの境界値(下限)
|7|30.0|hot|hotの代表値

####6.1.2 genTemperatureStr()
室温をfloat型で受け取り、室温の文字列を返します。

#####6.1.2.1 仕様

|No.|室温|表示|
|----|----|----|
| 1 | -10.0℃未満 | under -10.0 |
| 2 | -10.0℃以上40.0℃以下 | -10.0～40.0 |
| 3 | 40.0℃を超える| over 40.0 |

- 室温は0.1℃刻み
- 温湿度センサDHT12の計測範囲外(-20℃未満および60℃を超える温度)は考慮不要

#####6.1.2.2 室温と室温の文字列の対応
仕様には明示されていませんがNo.2は-10.0～-0.1と0.0～40.0に分けて確認することとします。

![MyM5StackThermometer-Spec-Temperature.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/d09aa319-d1a0-d407-908e-0db53162355f.png)

#####6.1.2.3 テストケース

同値分割・境界値分析を用いたテストケースを以下に示します。

|No.|テスト値|期待結果(室温の文字列)|テスト値の根拠
|----|----|----------|-----------------------
| 1|-20.0|under -10.0|under -10.0の境界値(下限)
| 2|-15.0|under -10.0|under -10.0の代表値
| 3|-10.1|under -10.0|under -10.0の境界値(上限)
| 4|-10.0|-10.0      |-10.0～-0.1の境界値(下限)
| 5| -5.0| -5.0      |-10.0～-0.1の代表値
| 6| -0.1| -0.1      |-10.0～-0.1の境界値(上限)
| 7|  0.0|  0.0      |  0.0～40.0の境界値(下限)
| 8| 22.5| 22.5      |  0.0～40.0の代表値
| 9| 40.0| 40.0      |  0.0～40.0の境界値(上限)
|10| 40.1|over 40.0  |  over 40.0の境界値(下限)
|11| 50.0|over 40.0  |  over 40.0の代表値
|12| 60.0|over 40.0  |  over 40.0の境界値(上限)

###6.2 TDDの実践
Unit Testのフレームワークとして[AUnit](https://github.com/bxparks/AUnit)を使用します。Arduino IDEにAUnitをインストールする手順は[こちら](https://github.com/bxparks/AUnit#installation)をご覧ください。

```c:AUnitの準備
#define ENABLE_UT 1
#if ENABLE_UT
  #line 30 "MyM5StackThermometer.ino"
  #include <AUnit.h>
#endif

void setup() {
  ...
#if ENABLE_UT
  aunit::TestRunner::setTimeout(0); // A timeout value of 0 means an infinite timeout.
#endif
  ...
}

void loop() {
#if ENABLE_UT
  aunit::TestRunner::run();
#else
  ...
#endif
}
```

####6.2.1 genMessageStr()
#####6.2.1.1 テストコードの作成
6.1.1.3節のテストケースを実装します。msg_XXは3章のプログラムをご確認ください。

```cpp:テストコード作成
char *genMessageStr(float temperature)
{
  return msg_01;
}

test(genMessageStr_150)
{
  assertEqual(0, strcmp(genMessageStr(15.0f), msg_11));
}

test(genMessageStr_199)
{
  assertEqual(0, strcmp(genMessageStr(19.9f), msg_11));
}

test(genMessageStr_200)
{
  assertEqual(0, strcmp(genMessageStr(20.0f), msg_12));
}

test(genMessageStr_225)
{
  assertEqual(0, strcmp(genMessageStr(22.5f), msg_12));
}

test(genMessageStr_250)
{
  assertEqual(0, strcmp(genMessageStr(25.0f), msg_12));
}

test(genMessageStr_251)
{
  assertEqual(0, strcmp(genMessageStr(25.1f), msg_13));
}

test(genMessageStr_300)
{
  assertEqual(0, strcmp(genMessageStr(30.0f), msg_13));
}
```

genMessageStr()は常にmsg_01の文字列を返すためテストはすべてfailします。

```txt:実行結果
TestRunner started on 7 test(s).
Assertion failed: (0) == (1), file MyM5StackThermometer.ino, line 128.
Test genMessageStr_150 failed.
Assertion failed: (0) == (1), file MyM5StackThermometer.ino, line 133.
Test genMessageStr_199 failed.
Assertion failed: (0) == (1), file MyM5StackThermometer.ino, line 138.
Test genMessageStr_200 failed.
Assertion failed: (0) == (1), file MyM5StackThermometer.ino, line 143.
Test genMessageStr_225 failed.
Assertion failed: (0) == (1), file MyM5StackThermometer.ino, line 148.
Test genMessageStr_250 failed.
Assertion failed: (0) == (1), file MyM5StackThermometer.ino, line 153.
Test genMessageStr_251 failed.
Assertion failed: (0) == (1), file MyM5StackThermometer.ino, line 158.
Test genMessageStr_300 failed.
TestRunner duration: 0.058 seconds.
TestRunner summary: 0 passed, 7 failed, 0 skipped, 0 timed out, out of 7 test(s).
```

#####6.2.1.2 genMessageStr()の実装
テストがグリーンになるようにgenMessageStr()を実装します。

```c
char *genMessageStr(float fTemperature)
{
  if(fTemperature < 20.0)
  {
    return msg_11;
  }
  else if(25.0 < fTemperature)
  {
    return msg_13;
  }
  else
  {
    return msg_12;
  }
}
```

```txt:実行結果
TestRunner started on 7 test(s).
Test genMessageStr_150 passed.
Test genMessageStr_199 passed.
Test genMessageStr_200 passed.
Test genMessageStr_225 passed.
Test genMessageStr_250 passed.
Test genMessageStr_251 passed.
Test genMessageStr_300 passed.
TestRunner duration: 0.013 seconds.
TestRunner summary: 7 passed, 0 failed, 0 skipped, 0 timed out, out of 7 test(s).
```

すべてのテストがpassする実装ができました。

####6.2.2 genTemperatureStr()
#####6.2.2.1 テストコードの作成
6.1.2.3節のテストケースを実装します。

```cpp:テストコード作成
char *genTemperatureStr(float fTemperature)
{
  return msg_11;
}

test(genTemperatureStr_m200)
{
  assertEqual(0, strcmp(genTemperatureStr(-20.0f), msg_01));
}

test(genTemperatureStr_m150)
{
  assertEqual(0, strcmp(genTemperatureStr(-15.0f), msg_01));
}

test(genTemperatureStr_m101)
{
  assertEqual(0, strcmp(genTemperatureStr(-10.1f), msg_01));
}

test(genTemperatureStr_m100)
{
  assertEqual(0, strcmp(genTemperatureStr(-10.0f), "-10.0"));
}

test(genTemperatureStr_m050)
{
  assertEqual(0, strcmp(genTemperatureStr(-5.0f), "-5.0"));
}

test(genTemperatureStr_m001)
{
  assertEqual(0, strcmp(genTemperatureStr(-0.1f), "-0.1"));
}

test(genTemperatureStr_000)
{
  assertEqual(0, strcmp(genTemperatureStr(0.0f), "0.0"));
}

test(genTemperatureStr_225)
{
  assertEqual(0, strcmp(genTemperatureStr(22.5f), "22.5"));
}

test(genTemperatureStr_400)
{
  assertEqual(0, strcmp(genTemperatureStr(40.0f), "40.0"));
}

test(genTemperatureStr_401)
{
  assertEqual(0, strcmp(genTemperatureStr(40.1f), msg_02));
}

test(genTemperatureStr_500)
{
  assertEqual(0, strcmp(genTemperatureStr(50.0f), msg_02));
}

test(genTemperatureStr_600)
{
  assertEqual(0, strcmp(genTemperatureStr(60.0f), msg_02));
}
```
genTemperatureStr()は常にmsg_11の文字列を返すためgenemperatureStrのテストはすべてfailします。

```txt:実行結果
TestRunner started on 19 test(s).
Test genMessageStr_150 passed.
Test genMessageStr_199 passed.
Test genMessageStr_200 passed.
Test genMessageStr_225 passed.
Test genMessageStr_250 passed.
Test genMessageStr_251 passed.
Test genMessageStr_300 passed.
Assertion failed: (0) == (1), file MyM5StackThermometer.ino, line 210.
Test genTemperatureStr_000 failed.
Assertion failed: (0) == (1), file MyM5StackThermometer.ino, line 215.
Test genTemperatureStr_225 failed.
Assertion failed: (0) == (1), file MyM5StackThermometer.ino, line 220.
Test genTemperatureStr_400 failed.
Assertion failed: (0) == (-1), file MyM5StackThermometer.ino, line 225.
Test genTemperatureStr_401 failed.
Assertion failed: (0) == (-1), file MyM5StackThermometer.ino, line 230.
Test genTemperatureStr_500 failed.
Assertion failed: (0) == (-1), file MyM5StackThermometer.ino, line 235.
Test genTemperatureStr_600 failed.
Assertion failed: (0) == (1), file MyM5StackThermometer.ino, line 205.
Test genTemperatureStr_m001 failed.
Assertion failed: (0) == (1), file MyM5StackThermometer.ino, line 200.
Test genTemperatureStr_m050 failed.
Assertion failed: (0) == (1), file MyM5StackThermometer.ino, line 195.
Test genTemperatureStr_m100 failed.
Assertion failed: (0) == (-1), file MyM5StackThermometer.ino, line 190.
Test genTemperatureStr_m101 failed.
Assertion failed: (0) == (-1), file MyM5StackThermometer.ino, line 185.
Test genTemperatureStr_m150 failed.
Assertion failed: (0) == (-1), file MyM5StackThermometer.ino, line 180.
Test genTemperatureStr_m200 failed.
TestRunner duration: 0.127 seconds.
TestRunner summary: 7 passed, 12 failed, 0 skipped, 0 timed out, out of 19 test(s).
```

#####6.2.2.2 genTemperatureStr()の実装

テストがグリーンになるようにgenTemperatureStr()を実装します。

```c:genTemperatureStr()
char *genTemperatureStr(float fTemperature)
{
  static char str[STRLEN] = {'\0'};

  if(fTemperature < -10.0)
  {
    return msg_01;
  }
  else if(40.0 < fTemperature)
  {
    return msg_02;
  }
  else
  {
    sprintf(str, "%.1f", fTemperature);
    return str;
  }
}
```

```txt:実行結果
TestRunner started on 19 test(s).
Test genMessageStr_150 passed.
Test genMessageStr_199 passed.
Test genMessageStr_200 passed.
Test genMessageStr_225 passed.
Test genMessageStr_250 passed.
Test genMessageStr_251 passed.
Test genMessageStr_300 passed.
Test genTemperatureStr_000 passed.
Test genTemperatureStr_225 passed.
Test genTemperatureStr_400 passed.
Test genTemperatureStr_401 passed.
Test genTemperatureStr_500 passed.
Test genTemperatureStr_600 passed.
Test genTemperatureStr_m001 passed.
Test genTemperatureStr_m050 passed.
Test genTemperatureStr_m100 passed.
Test genTemperatureStr_m101 passed.
Test genTemperatureStr_m150 passed.
Test genTemperatureStr_m200 passed.
TestRunner duration: 0.051 seconds.
TestRunner summary: 19 passed, 0 failed, 0 skipped, 0 timed out, out of 19 test(s).
```

すべてのテストがpassする実装ができました。

##7. drawStr()の実装
drawStr()と任意の文字列を画面に表示するデバッグ機能のプログラムを示します。

```c:drawStr()
void drawStr(char *sTemperature, char *sMessage)
{
  int font = 4;
  
  if( !strcmp(msg_01, sTemperature) || !strcmp(msg_02, sTemperature) )
  {
    font = 4;
  }
  else
  {
    font = 7;
  }
  M5.Lcd.fillRect(OSD_X, OSD_Y, OSD_W, OSD_H, OSD_BG_COLOR);
  M5.Lcd.drawCentreString(sTemperature, 160, 80, font);
  M5.Lcd.drawCentreString(sMessage, 160, 144, 4);
}
```

```c:任意の文字列を画面に表示するデバッグ機能
#define ERR_OK       0
int debugDrawStr()
{
  char sTemperature[STRLEN] = {'\0'};
  char sMessage[STRLEN]     = {'\0'};
  int err = ERR_OK;

  while(Serial.available()){ Serial.read(); } // clear buffer
  
  Serial.println("-- Debug drawStr()------------");
  
  Serial.print("Enter Temperature : ");
  err = getStr(&sTemperature[0]);
  if(err<0) return err;
  Serial.print("\n");
  
  Serial.print("Enter Message     : ");
  err = getStr(&sMessage[0]);
  if(err<0) return err;
  Serial.print("\n");
  
  drawStr(sTemperature, sMessage);
  
  while(Serial.available()){ Serial.read(); } // clear buffer
  return ERR_OK;
}
```

※getStr()は付録のソースをご参照ください。

##8. Controllerの実装
温湿度センサから室温を取得し、室温とメッセージの文字列を生成し、ディスプレイに描画します。

```c:myM5StackThermometer()
void myM5StackThermometer()
{
  float fTemperature;
  char sTemperature[STRLEN] = {'\0'};
  char sMessage[STRLEN]     = {'\0'};

  fTemperature = dht12.readTemperature();
#if ENABLE_DBG
  Serial.println(fTemperature);
#endif

  strcpy(sTemperature, genTemperatureStr(fTemperature));
  strcpy(sMessage,     genMessageStr(fTemperature));

  drawStr(sTemperature, sMessage);
}
```

![24_s.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/5627bf10-c782-1ef5-0f7c-9e6da7c024be.jpeg)

##9. Debug Utilsの実装
デバッグ機能はマクロでオンオフできるようにします。

```c:DebugUtils
#define ENABLE_DBG 1

...

#if ENABLE_DBG
...
#endif
```

debugDrawStr()以外にもデバッグや機能検討のためのプログラムを入れています。

```c:debugMenu()
void debugMenu()
{
  char buf;

  while(1)
  {
    while(Serial.available()){ Serial.read(); } // clear buffer

    Serial.println("-- DEBUG MENU ----------------");
    Serial.println("1 : selectTextColor()");
    Serial.println("2 : debugDrawStr()");
    Serial.println("3 : printHelp()"); 
    Serial.println("q : Quit Debug Menu"); 
    Serial.print("Select : ");

    while(!Serial.available()){};
    buf = Serial.read();
    Serial.println(buf);
    
    switch(buf)
    {
      case '1':
        selectTextColor();
        break;
      case '2':
        debugDrawStr();
        break;
      case '3':
        printHelp();
        break;
      case 'q':
        Serial.println("-- QUIT ----------------------");
        while(Serial.available()){ Serial.read(); } // clear buffer
        return;
      default:
        Serial.println("?");
        break;
    }
  }
}
```

デバッグ機能を使ってテキストの色を設定したりメッセージをディスプレイに出力した例：

![35_s.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/155f1fef-23a1-d7cc-8ddc-1f1373df7f60.jpeg)
![m5_s.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/b950e45c-b26f-bf18-708c-415f2679c4fe.jpeg)

##10. TDDのメリット、デメリット
プログラムのステップ数やUnit Testの実行時間といったメトリクスも考慮しながらメリット、デメリットを挙げてみます。メンテ対象のコードが増えることよりもメリットの方が大きいと思います。

|    |ベタ書きで開発|TDDを取り入れて開発
|----|----|----
|メリット|・実ステップ数39行とコンパクト|・Unit Test、結合テスト、システムテストと段階的に品質の確認ができる<br>・異なるテストレベルのテストを組合わせて網羅できる<br>・Unit Testの段階で不具合を修正すればシステムテストと比べて圧倒的に短時間で再テストができる(19個のUnit Testにかかった時間は0.051秒)<br>・Unit Testでテストできるモジュールは全体の一部とはいえデグレにいち早く気づく仕組みを持てる
|デメリット|・品質の確認がシステムテストに集中する<br>・不具合修正のたびにシステムテストレベルの再テストが発生する|・テストコード含めメンテ対象のコードが増える

##11. おわりに
- initDisplay()、genTemperatureStr()、genMessageStr()、drawStr()、myM5StackThermometer()の5個の関数のうち設計を工夫することで2個の関数をTDDで実装できました。5個中2個(40%)とはいえ3章のプログラムが1個中0個(0%)だったのと比べると進化した感じがします。
- TDDの威力はUnit Testでいつでもチェックができることに加え、テストのアーキテクチャ設計(どのようなテストを行えば良いかやどのようなテストを組み合わせるかを設計する)とソフトのアーキテクチャ設計(ソフトウェアのアーキテクチャを工夫してUnit Testをできるようにする)を経ることで不具合検出が多層化するというのもあるように思いました。
- TDDを回すのにテスト設計が必須なことから、[ソフトウェアテスト技法練習帳](https://gihyo.jp/book/2020/978-4-297-11061-1)はテストエンジニアやSQAだけでなく開発者にも役立ちそうに思いました。
- ArduinoやM5Stackはソフトウェアテストのツールをサクッと作るのに便利[^5]なのですが、Unit Testのフレームワークが提供されていて筆者のようなSQAがソフトウェア開発を試すのにも有用と思いました。

##X.付録
###X.1 ソースコードおよびステップ数
今回開発したソースコードとステップ数を以下に掲載します。

- 筆者の開発環境はArduino IDE Ver. 1.8.9、AUnit by Brian T. Park Ver. 1.3.0、esp32 by Espressif Systems Ver. 1.0.4、M5Stack by M5Stack Ver. 0.2.9です。
- ライセンスはMITです(DHT12の[サンプルコード](https://github.com/m5stack/M5Stack/tree/master/examples/Modules/DHT12)の[ライセンス](https://github.com/m5stack/M5Stack/blob/master/LICENSE)を踏襲しています)。
- GitHubのリンクは[こちら](https://github.com/pbjpkas/MyM5StackThermometer)です。

```c:MyM5StackThermometer.ino
/*
  This code is based on
  https://github.com/m5stack/M5Stack/tree/master/examples/Modules/DHT12
 */
/*
For configuration library:
DHT12 dht12("Scale temperature","ID device for I2C");
On "Scale temperature" you can select the preset scale:
CELSIUS, FAHRENHEIT or KELVIN.
And on "ID device", you can put ID sensor, on DHT12
normally is 0x5c.
Examples:
  DHT12 dht12;
The preset scale is CELSIUS and ID is 0x5c.
  DHT12 dht12(KELVIN);
the preset scale is KELVIN and ID is 0x5c.
  DHT12 dht12(FAHRENHEIT,0x53);
The preset scale is FAHRENHEIT and ID is 0x53.
*/
#include <stdio.h>  //sprintf
#include <string.h> //strcpy, strcmp
#include <M5Stack.h>
#include "DHT12.h"
#include <Wire.h>     //The DHT12 uses I2C comunication.
DHT12 dht12;          //Preset scale CELSIUS and ID 0x5c.

#define ENABLE_UT 0
#if ENABLE_UT
  #line 30 "MyM5StackThermometer.ino"
  #include <AUnit.h>
#endif

#define ENABLE_DBG 1

#define STRLEN  32

//On-Screen Display Layout
#define OSD_X    0
#define OSD_Y   60
#define OSD_W  320
#define OSD_H  120

#define OSD_BG_COLOR TFT_WHITE
#define OSD_FG_COLOR TFT_GREEN

//message strings
char msg_01[] = "under -10.0";
char msg_02[] = "over 40.0";
char msg_11[] = "cold";
char msg_12[] = "comfortable";
char msg_13[] = "hot";

void monolithic_implementation()
{
  char str[STRLEN] = {'\0'};

  // M5.Lcd.drawCentreString(const char *string, int dX, int poY, int font);
  // Only font numbers 2,4,6,7 are valid. Font 6 only contains characters [space] 0 1 2 3 4 5 6 7 8 9 : . - a p m
  // Font 7 is a 7 segment font and only contains characters [space] 0 1 2 3 4 5 6 7 8 9 : .
  // see: https://github.com/m5stack/M5Stack/blob/master/examples/Advanced/Display/TFT_Clock/TFT_Clock.ino
  int font = 4;

  float temperature = dht12.readTemperature();

  Serial.print("Temperature: ");
  Serial.println(temperature);
  
  M5.Lcd.fillRect(OSD_X, OSD_Y, OSD_W, OSD_H, OSD_BG_COLOR);
  M5.Lcd.setTextColor(OSD_FG_COLOR);
  
  if(temperature < -10.0)
  {
    strcpy(str, msg_01);
    font = 4;
  }
  else if(40.0 < temperature)
  {
    strcpy(str, msg_02);
    font = 4;
  }
  else
  {
    sprintf(str, "%.1f", temperature);
    font = 7;
  }
  M5.Lcd.drawCentreString(str, 160, 80, font);
  
  if(temperature < 20.0)
  {
    strcpy(str, msg_11);
  }
  else if(25.0 < temperature)
  {
    strcpy(str, msg_13);
  }
  else
  {
    strcpy(str, msg_12);
  }
  M5.Lcd.drawCentreString(str, 160, 144, 4);
}

void initDisplay()
{
  M5.Lcd.fillRect(0, 0, 320, 240, OSD_BG_COLOR);
  M5.Lcd.setTextColor(TFT_BLUE);
  M5.Lcd.drawCentreString("MyM5Stack Thermometer by ka's", 160, 220, 2);
  M5.Lcd.setTextColor(TFT_GREEN);
}

char *genMessageStr(float fTemperature)
{
  if(fTemperature < 20.0)
  {
    return msg_11;
  }
  else if(25.0 < fTemperature)
  {
    return msg_13;
  }
  else
  {
    return msg_12;
  }
}

char *genTemperatureStr(float fTemperature)
{
  static char str[STRLEN] = {'\0'};

  if(fTemperature < -10.0)
  {
    return msg_01;
  }
  else if(40.0 < fTemperature)
  {
    return msg_02;
  }
  else
  {
    sprintf(str, "%.1f", fTemperature);
    return str;
  }
}

void drawStr(char *sTemperature, char *sMessage)
{
  int font = 4;
  
  if( !strcmp(msg_01, sTemperature) || !strcmp(msg_02, sTemperature) )
  {
    font = 4;
  }
  else
  {
    font = 7;
  }
  M5.Lcd.fillRect(OSD_X, OSD_Y, OSD_W, OSD_H, OSD_BG_COLOR);
  M5.Lcd.drawCentreString(sTemperature, 160, 80, font);
  M5.Lcd.drawCentreString(sMessage, 160, 144, 4);
}

void myM5StackThermometer()
{
  float fTemperature;
  char sTemperature[STRLEN] = {'\0'};
  char sMessage[STRLEN]     = {'\0'};

  fTemperature = dht12.readTemperature();
#if ENABLE_DBG
  Serial.println(fTemperature);
#endif

  strcpy(sTemperature, genTemperatureStr(fTemperature));
  strcpy(sMessage,     genMessageStr(fTemperature));

  drawStr(sTemperature, sMessage);
}

void setup() {
  M5.begin();
  Wire.begin();
  Serial.begin(115200);
  
  initDisplay();
  
#if ENABLE_DBG
  Serial.print("Enter 'd' to Debug Menu\r\n");
#endif

#if ENABLE_UT
  aunit::TestRunner::setTimeout(0); // A timeout value of 0 means an infinite timeout.
#endif
}

void loop() {
#if ENABLE_UT
  aunit::TestRunner::run();
#else
  //monolithic_implementation();
  myM5StackThermometer();

  #if ENABLE_DBG
  if(Serial.available())
  {
    char buf;
    buf = Serial.read();
    if(buf=='d')//Debug
    {
      debugMenu();
    }
  }
  #endif

  delay(2000);

#endif //ENABLE_UT
}


/*
 * Debug Menu
 */
#if ENABLE_DBG
#define ERR_OK       0
#define ERR_INVALID -1
int getStr(char *buf)
{
  int i;

  i=0;
  while(1)
  {
    if(Serial.available())
    {
      buf[i] = Serial.read();
      Serial.print(buf[i]); //echo-back

      if ( (buf[i] == 0x08) or (buf[i] == 0x7f) ) //BackSpace, Delete
      {
        buf[i] = '\0';
        if(i) i--;
      }
      else if( (buf[i] == '\r') or (buf[i] == '\n') )
      {
        buf[i] = '\0';
        return strlen(buf);
      }
      else
      {
        i++;
        if(i>=STRLEN)
        {
          Serial.print("### BUFFER FULL, CLEAR. ###\r\n");
          for(i=0; i<STRLEN; i++) buf[i] = '\0';
          i=0;
          return ERR_INVALID;
        }
      }
    }
  }// while
}

// color definition: https://github.com/m5stack/M5Stack/blob/master/src/utility/In_eSPI.h
void selectTextColor()
{
  char buf;
  unsigned int textColor = TFT_BLACK;

  while(Serial.available()){ Serial.read(); } // clear buffer

  Serial.println("-- COLOR ---------------------");
  Serial.println("a : BLACK");
  Serial.println("b : BLUE");
  Serial.println("c : GREEN");
  Serial.println("d : CYAN");
  Serial.println("e : RED");
  Serial.println("f : MAGENTA");
  Serial.println("g : YELLOW");
  Serial.println("h : WHITE");
  Serial.println("i : ORANGE");
  Serial.println("j : GREENYELLOW");
  Serial.println("k : PINK");
  Serial.print("Select : ");

  while(!Serial.available()){};
  buf = Serial.read();
  Serial.println(buf);
  
  switch(buf)
  {
    case 'a':
      textColor = TFT_BLACK;
      break;
    case 'b':
      textColor = TFT_BLUE;
      break;
    case 'c':
      textColor = TFT_GREEN;
      break;
    case 'd':
      textColor = TFT_CYAN;
      break;
    case 'e':
      textColor = TFT_RED;
      break;
    case 'f':
      textColor = TFT_MAGENTA;
      break;
    case 'g':
      textColor = TFT_YELLOW;
      break;
    case 'h':
      textColor = TFT_WHITE;
      break;
    case 'i':
      textColor = TFT_ORANGE;
      break;
    case 'j':
      textColor = TFT_GREENYELLOW;
      break;
    case 'k':
      textColor = TFT_PINK;
      break;
    default:
      Serial.println("?");
      break;
  }
  M5.Lcd.setTextColor(textColor);
  
  while(Serial.available()){ Serial.read(); } // clear buffer
}

int debugDrawStr()
{
  char sTemperature[STRLEN] = {'\0'};
  char sMessage[STRLEN]     = {'\0'};
  int err = ERR_OK;

  while(Serial.available()){ Serial.read(); } // clear buffer
  
  Serial.println("-- Debug drawStr()------------");
  
  Serial.print("Enter Temperature : ");
  err = getStr(&sTemperature[0]);
  if(err<0) return err;
  Serial.print("\n");
  
  Serial.print("Enter Message     : ");
  err = getStr(&sMessage[0]);
  if(err<0) return err;
  Serial.print("\n");
  
  drawStr(sTemperature, sMessage);
  
  while(Serial.available()){ Serial.read(); } // clear buffer
  return ERR_OK;
}

void printHelp()
{
    Serial.println("-- HELP ----------------------");
    Serial.print( F("This is ") );
    Serial.print( F(__FILE__) );
    Serial.print( F(" ") );
    Serial.print( F("Build at ") );
    Serial.print( F(__DATE__) );
    Serial.print( F(" ") );
    Serial.print( F(__TIME__) );
    Serial.print( F("\r\n") );
}

void debugMenu()
{
  char buf;

  while(1)
  {
    while(Serial.available()){ Serial.read(); } // clear buffer

    Serial.println("-- DEBUG MENU ----------------");
    Serial.println("1 : selectTextColor()");
    Serial.println("2 : debugDrawStr()");
    Serial.println("3 : printHelp()"); 
    Serial.println("q : Quit Debug Menu"); 
    Serial.print("Select : ");

    while(!Serial.available()){};
    buf = Serial.read();
    Serial.println(buf);
    
    switch(buf)
    {
      case '1':
        selectTextColor();
        break;
      case '2':
        debugDrawStr();
        break;
      case '3':
        printHelp();
        break;
      case 'q':
        Serial.println("-- QUIT ----------------------");
        while(Serial.available()){ Serial.read(); } // clear buffer
        return;
      default:
        Serial.println("?");
        break;
    }
  }
}
#endif // ENABLE_DBG


/*
 * Unit Test
 */
#if ENABLE_UT
// UT:genMessageStr
test(genMessageStr_150)
{
  assertEqual(0, strcmp(genMessageStr(15.0f), msg_11));
}

test(genMessageStr_199)
{
  assertEqual(0, strcmp(genMessageStr(19.9f), msg_11));
}

test(genMessageStr_200)
{
  assertEqual(0, strcmp(genMessageStr(20.0f), msg_12));
}

test(genMessageStr_225)
{
  assertEqual(0, strcmp(genMessageStr(22.5f), msg_12));
}

test(genMessageStr_250)
{
  assertEqual(0, strcmp(genMessageStr(25.0f), msg_12));
}

test(genMessageStr_251)
{
  assertEqual(0, strcmp(genMessageStr(25.1f), msg_13));
}

test(genMessageStr_300)
{
  assertEqual(0, strcmp(genMessageStr(30.0f), msg_13));
}

// UT:genTemperatureStr
test(genTemperatureStr_m200)
{
  assertEqual(0, strcmp(genTemperatureStr(-20.0f), msg_01));
}

test(genTemperatureStr_m150)
{
  assertEqual(0, strcmp(genTemperatureStr(-15.0f), msg_01));
}

test(genTemperatureStr_m101)
{
  assertEqual(0, strcmp(genTemperatureStr(-10.1f), msg_01));
}

test(genTemperatureStr_m100)
{
  assertEqual(0, strcmp(genTemperatureStr(-10.0f), "-10.0"));
}

test(genTemperatureStr_m050)
{
  assertEqual(0, strcmp(genTemperatureStr(-5.0f), "-5.0"));
}

test(genTemperatureStr_m001)
{
  assertEqual(0, strcmp(genTemperatureStr(-0.1f), "-0.1"));
}

test(genTemperatureStr_000)
{
  assertEqual(0, strcmp(genTemperatureStr(0.0f), "0.0"));
}

test(genTemperatureStr_225)
{
  assertEqual(0, strcmp(genTemperatureStr(22.5f), "22.5"));
}

test(genTemperatureStr_400)
{
  assertEqual(0, strcmp(genTemperatureStr(40.0f), "40.0"));
}

test(genTemperatureStr_401)
{
  assertEqual(0, strcmp(genTemperatureStr(40.1f), msg_02));
}

test(genTemperatureStr_500)
{
  assertEqual(0, strcmp(genTemperatureStr(50.0f), msg_02));
}

test(genTemperatureStr_600)
{
  assertEqual(0, strcmp(genTemperatureStr(60.0f), msg_02));
}

#endif //ENABLE_UT
```

![steps.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/79fa5b04-b23c-891f-e35c-ccb5b45cd109.png)

###X.2 DHT12::readTemperature()の修正
<strike>DHT12のByte Addr 0x03(Temperature scale)のBit8(温度の正負を示すビット)の処理漏れを修正しました。</strike>
最新版のDHT12.cppをご使用ください(以下の修正は[M5Stack](https://github.com/m5stack/M5Stack)に[プルリクエスト](https://github.com/m5stack/M5Stack/pull/200)しマージ済みです)(2020/1/13)

![DHT12-temperature-scale-description.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/68d0c66c-6014-dab4-4623-2b3b4b58a3ff.png)

```c:DHT12.cpp
float DHT12::readTemperature(uint8_t scale)
{
	float resultado=0;
	uint8_t error=read();
	if (error!=0) return (float)error/100;
	
	resultado=datos[2]+(float)(datos[3]&0x7f)/10;
	if(datos[3]&0x80)
	{
		resultado = -resultado;
	}
	
	if (scale==0) scale=_scale;
	switch(scale) {
		case CELSIUS:
			break;
		case FAHRENHEIT:
			resultado=resultado*1.8+32;
			break;
		case KELVIN:
			resultado=resultado+273.15;
			break;
	};
	return resultado;
}
```

[^1]: このプロトタイプはMyM5StackThermometer.ino、DHT12.cpp、DHT12.hの3ファイルで構成されます(DHT12.cppおよびDHT12.hは[DHT12サンプルコード](https://github.com/m5stack/M5Stack/tree/master/examples/Modules/DHT12)と同一のため掲載は割愛します)。
[^2]: テスト設計駆動開発(Test-Design-Driven Development)、TDDDと呼んでもよいかもしれません。
[^3]: 準正常系(例：動作保証環境の上限、下限における長時間の動作)、異常系(例：センサが故障して正常な値を返さない、センサの配線が断線している)、見やすさ(例：文字の大きさ、フォント、色、文字の間隔、輝度、コントラスト)などいろいろあるかと思いますがひとまず抜粋したものにフォーカスします。
[^4]: フォーカスしていないテスト観点も入っています。
[^5]: 最近の製作事例として「[ストップウォッチを使う性能テストを実ステップ300行に満たない自動テストシステムで自動化する](https://qiita.com/pbjpkas/items/d525d7d19095d1e26452)」があります。