はじめに
この記事は
第1弾 「カレーを作るな、カレールーを入れた煮込みを作れ」:超新人エンジニア向け 抽象化と命名の話
第2弾 「冷蔵と冷凍と常温を分けて!」:超新人エンジニア向け クラス設計の話
に続く第3弾です。
読む順は関係しませんので、面白そうと思ったら読んでいただけるとありがたいです。
前回までで、
- 処理は抽象化して切り出す
- 役割ごとにクラスを分ける
という話をしました。
今回は、その中身である「メソッド」と「ユニットテスト」の話です。
メソッドとは何か
メソッドをまず初心者の方に説明するなら、ジュース工場 だと思ってください。
オレンジを渡したらオレンジジュースを返す。
ぶどうを渡したらぶどうジュースを返す。
材料を受け取って、加工して、結果を返すもの、それがメソッドです。
/**
* ジュースを作る処理
* @param $zairyo 材料(オレンジ、ぶどうなど)
* @return $juice ジュース(オレンジジュース、ぶどうジュースなど)
*/
function makeJuice($zairyo) {
// 材料を絞るとジュースができる
$juice = shiboru($zairyo);
// そのジュースを返却する
return $juice;
}
基本はこのように、材料を渡したら それをどのように加工するのか・それを使って何をするのか ということがまとまっているものがメソッドです。
さらにここに処理を追加していきます。
ジュースを絞るとき、柑橘系だったら皮をむいてからジュースを絞ることにします。
/**
* ジュースを作る処理
* @param $zairyo 材料(オレンジ、ぶどうなど)
* @return $juice ジュース(オレンジジュース、ぶどうジュースなど)
*/
function makeJuice($zairyo) {
// かんきつの場合は皮をむく
if (in_array($zairyo, ['mikan', 'orange', 'lemon'], true)) {
$zairyo = muku($zairyo);
}
// 材料を絞るとジュースができる
$juice = shiboru($zairyo);
// そのジュースを返却する
return $juice;
}
このように書くことによって、もし柑橘系の果物だったら、皮をむいてからジュースを絞るという処理になりました。
どんどん行きます。
例えば、材料が空だったらどうしましょうか。きっとレビューで「空判定したほうがいいよ」とコメントをもらったこともあるでしょう。
/**
* ジュースを作る処理
* @param $zairyo 材料(オレンジなど)
* @return $juice|false ジュース(オレンジジュースなど) or 作れない場合は false
*/
function makeJuice($zairyo) {
// 材料がない場合は作れない
if (empty($zairyo)) {
return false;
}
// かんきつの場合は皮をむく
if (in_array($zairyo, ['mikan', 'orange', 'lemon'], true)) {
$zairyo = muku($zairyo);
}
// 材料を絞るとジュースができる
$juice = shiboru($zairyo);
// そのジュースを返却する
return $juice;
}
このように書くことによって、ジュース工場に空っぽの箱が届いてしまった場合の扱いについて決めることができました。
さらに処理を追加していきます。
もし果物以外が届いてしまった場合はどうしたらいいでしょうか?
例えば寿司が入ってしまった場合、寿司を寿司ジュースにすることはできませんので、falseで返すのがよさそうです。
その場合、処理においては何であれば問題なくジュースにできるか、事前に決めておく必要があります。
まずは材料の定義を外に出す
/**
* ジュースにできる果物
*/
const FRUITS = [
'grape',
'apple',
'mikan',
'orange',
'lemon',
];
ついでに柑橘も定義してしまいましょう。
/**
* かんきつ類
*/
const CITRUS = [
'mikan',
'orange',
'lemon',
];
それでは、ジュースにできるもの以外が届いてしまった場合の処理を追加します。
/**
* ジュースを作る処理
* @param $zairyo 材料(オレンジなど)
* @return $juice|false ジュース(オレンジジュースなど) or 作れない場合は false
*/
function makeJuice($zairyo) {
// 材料がない場合は作れない
if (empty($zairyo)) {
return false;
}
// 果物でなければ作れない
if (!in_array($zairyo, FRUITS, true)) {
return false;
}
// かんきつの場合は皮をむく
if (in_array($zairyo, CITRUS, true)) {
$zairyo = muku($zairyo);
}
// 材料を絞るとジュースができる
$juice = shiboru($zairyo);
// そのジュースを返却する
return $juice;
}
ここでのポイント
このメソッドはこのような構造になっています。
- 入力:材料
- 加工:
- 入力チェック
- 種類チェック(FRUITS)
- 条件付き処理(CITRUS)
- 絞る
- 出力:ジュース or false
中身の処理よりも、入力と出力の関係 で捉えるのが大事です。
ユニットテストとは何か
メソッドを作ったら、次は対応するユニットテストを作る必要があります。
ユニットテストとは、
この入力を入れたら、この結果になるはずだ
を確認するものです。
テストケースを作る
| ケース | 入力 | 期待する出力 |
|---|---|---|
| 材料がない場合 | null | false |
| 果物でない場合 | sushi | false |
| かんきつの場合 | orange | オレンジジュース |
| 非かんきつの場合 | grape | ぶどうジュース |
PHPUnit(Provider形式)
/**
* ジュース生成テスト
*
* @dataProvider makeJuiceProvider
*/
public function testMakeJuice($zairyo, $expected) {
// ジュースを作る
$actual = makeJuice($zairyo);
// 期待した結果になっていることを確認する
$this->assertEquals($expected, $actual);
}
/**
* テストデータ
*/
public function makeJuiceProvider() {
return [
// 材料がない
'材料がない場合' => [
null,
false,
],
// 果物でない
'果物でない場合' => [
'sushi',
false,
],
// かんきつ
'かんきつの場合' => [
'orange',
'オレンジジュース',
],
// 非かんきつ
'非かんきつの場合' => [
'grape',
'ぶどうジュース',
],
];
}
なぜこの4つで十分なのか
上記のジュースを作るメソッドのテストするケースとして、基本的には上記の4つのケースで十分と考えられます。それはなぜか。
テストケースは思いつきではなく、分岐から作るからです。
分岐を洗い出す
// 材料がない
if (empty($zairyo)) {
return false;
}
// 果物じゃない
if (!in_array($zairyo, FRUITS, true)) {
return false;
}
// かんきつなら皮をむく
if (in_array($zairyo, CITRUS, true)) {
// 皮をむく
}
// かんきつの場合は皮をむいたもの・それ以外はそのまま絞る
分岐ごとにテストを作る
- 材料がない → 1ケース
- 果物じゃない → 1ケース
- かんきつ → 1ケース
- 非かんきつ → 1ケース
よくある間違い
// かんきつを増やす
'lemon',
'orange',
'mikan',
一見ケースを増やしてしっかりテストしているように見えますが、これにはあまり意味がありません。なぜなら増やしたこれらのケースが通る分岐が全て同じです。
Aを通るケース・Bを通るケース・・・と様々なケースを用意するから意味があるのであって、かんきつだけを増やすのは、テストとしては無意味なケースの量産になってしまっています。
if (in_array($zairyo, CITRUS, true)) {
つまり、
- lemon → かんきつ
- orange → かんきつ
- mikan → かんきつ
全部同じ処理です。
テストケースは、データの種類ではなく、分岐の種類で分ける必要があります。「たくさんテストを書いた」は評価になりません。
同じ分岐を何度も通しているだけでは意味がなく、重要なのは、すべての分岐を漏れなく通しているかです。
まとめ
- メソッドは工場(入力 → 加工 → 出力)
- テストは入力と出力の確認
- テストケースは分岐から作る