TDD Boot Camp in 愛媛 ふりかえり

基調講演

当日講演スライドはこちら

お約束の アレ から始まり、最初のテスト駆動開発とは何かというお話の内容としては自分が実践してきたテスト駆動開発と認識のズレはなかったので良かった。勉強会 を実施したり テスト駆動開発の記事 を書いて専門学校の講義に使ったりしてきたけど やはり俺のテスト駆動開発に対する理解は間違っていなかった ようだ。ただ、テスト駆動開発で言及されているリファクタリングとマーティンファウラーの リファクタリング ではサイズ感が違う というのは新しい気づきだった。

続いて、ライブコーディングのお題は FizzBuzz 問題 。 内容は 50 分でわかるテスト駆動開発 を以前視聴していたので流れは大体知っていたけどお題から TODO に落とし込むあたりの作業が動画の時よりバージョンアップしていてよりわかりやすい流れになっていた。後半のシンプルな FizzBuzz プログラムを使った保守運用に向けた解説は実際に現場で起きている問題を反映しているのだと思うと興味深い内容だった。このへんが レガシーコード改善ガイド へとつながっていくのかなと個人的に思った。

ライブコーディングの内容として自分は 抽象から具体へ具体から抽象へ を小さなサイクルで何度も回しているなという印象を受けた。自分も最近は 抽象と具体 の往復は意識しているのだがやはり上手な人が実際にやるのを見れるのは実に学びがある。こればかりは文章ではなかなか伝えられないのよね。

TDD&ペアプログラミング デモ

ペアプログラミング(英: pair programming)は、2 人のプログラマが 1 台のワークステーションを使って共同でソフトウェア開発を行う手法という説明が起源である。一方が単体テストを打ち込んでいるときに、もう一方がそのテストを通るクラスについて考えるといったように、相補的な作業をする。プログラム開発の現場では、一人で複数台を同時に使ったり、一台に複数台のディスプレイを使うことも多くなり、具体的なやり方は変わっている。

実際にキーボードを操作してコードを書く人を「ドライバ」、もう 1 人を「ナビゲータ」と呼ぶ。30 分ごとか、単体テストを 1 つ完成させる度に役割を交替するのがよいとされる。また、1 日に一度の頻度でパートナーを変えるのがよいともされている。

— Wikipedia

実演でもドライバ交代時に US 配列キーボードで戸惑うなど ペアプロあるある が見受けられた。ちなみに自分のメイン開発環境は VSCode に Vim キーバインドなのでペアが Vimer でもないかぎり間違いなく戸惑うと思う。幸いペアの方が Visual Studio Live Share を VSCode で使ってもオッケーとのことだったのでその辺の問題は回避できた。

TDD&ペアプログラミング 実習(1 回目)

午後からペアプログラミングによるテスト駆動開発の実践に入るわけだけど、今回自分が選択した言語は JavaScript でテスティングフレームワークには Mocha を使った構成でやった。表記スタイルを TDD にしているので BDD スタイルとは違うけどあしからず。以下コードがたくさん出てくるけど、これは プログラミングの速さを競うのではなく、テストを書いて動かすことによるフィードバックを受けながら、リファクタリングを忘れず着実に進めていく 演習の目的をできるだけ伝えようとしたらこうなってしまったわけであり結論だけ知りたいならばここは読み飛ばして クロージング へ。まあ、個人的にはここがテスト駆動開発のキモだと思ってるけど・・・

仕様

最初にお題だけど今回は 整数の区間。調べたら 過去にも取り上げられたお題 みたい。

整数閉区間を示すクラス(あるいは構造体)をつくりたい。整数閉区間オブジェクトは下端点と上端点を持ち、文字列表現も返せる(例: 下端点 3, 上端点 8 の整数閉区間の文字列表記は "[3,8]")。ただし、上端点より下端点が大きい閉区間を作ることはできない。整数の閉区間は指定した整数を含むかどうかを判定できる。また、別の閉区間と等価かどうかや、完全に含まれるかどうかも判定できる。

ペアと最初に取り組んだのがお題の理解と TODO リスト への分割。とりあえず ただし、また、 といった接続詞ごとに分割して プログラミングしやすいところから組むようにした。

整数閉区間オブジェクトは下端点と上端点を持ち、
文字列表現も返せる(例: 下端点 3, 上端点 8 の整数閉区間の文字列表記は "[3,8]")。
ただし、
上端点より下端点が大きい閉区間を作ることはできない。
整数の閉区間は指定した整数を含むかどうかを判定できる。
また、
別の閉区間と等価かどうかや、完全に含まれるかどうかも判定できる。
整数閉区間を示すクラス(あるいは構造体)をつくりたい。

TODO リスト

最初に作った TODO リスト がこちら。

  • ❏ 整数区間オブジェクトを作る

    • ❏ 下端点と上端点を持つ
  • ❏ 文字列表現も返す

    • ❏ 下端点 3, 上端点 8 ならば文字列”[3,8]“を返す

仮実装

最初に失敗するコードを書いたらまずは 仮実装 でテストをパスするベタ書きのコードを書いた。 クラスを作りたいとのことだけど JavaScriptclass キーワードを使うのは人によってはアレなんだけどペアの人は使ってもオッケーな人だったので class キーワードで実装した。これが function による実装なら、それはそれで面白かったかも。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mocha.setup("tdd");

const assert = chai.assert;
suite("ClosedRangeTest", () => {
suite("文字列表現を返す", () => {
test("下端点 3, 上端点 8 ならば文字列[3,8]を返す", () => {
assert.equal("[3,8]", ClosedRange.stringify());
});
});
});

class ClosedRange {
static stringify() {
return "[3,8]";
}
}

三角測量

仮実装TODO リスト を一つ片付けたけどコードはベタ書きのまま・・・ここは 三角測量 を実施してメソッドの一般化を進めることにした。まず、 TODO リスト を追加する。

  • ❏ 整数区間オブジェクトを作る

    • ❏ 下端点と上端点を持つ
  • ❏ 文字列表現も返す

    - ✓ 下端点 3, 上端点 8 ならば文字列"\[3,8\]"を返す
    
    - ❏ 下端点 4, 上端点 9

    ならば文字列”[4,9]“を返す

追加テストが失敗(レッド)からベタ書きのコードを変更してテスト成功(グリーン)へ。ちなみにこの間8分ごとに交代しながらペアプログラミングしてるけどナビゲーター役の時もなんだかんだで脳内コーディングして活発に意見交換してた。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
suite("ClosedRangeTest", () => {
suite("文字列表現を返す", () => {
test("下端点 3, 上端点 8 ならば文字列[3,8]を返す", () => {
closedRange = new ClosedRange(3, 8);
assert.equal("[3,8]", closedRange.stringify());
});

test("下端点 4, 上端点 9 ならば文字列[4,9]を返す", () => {
closedRange = new ClosedRange(4, 9);
assert.equal("[4,9]", closedRange.stringify());
});
});
});

class ClosedRange {
constructor(lower, upper) {
this._lower = lower;
this._upper = upper;
}
stringify() {
let result = `[${this._lower},${this._upper}]`;
return result;
}
}

リファクタリング

最初の TODO リスト が片付いたのでここで一息ついて リファクタリング に取り組む。

  • ✓ 整数区間オブジェクトを作る

    • ✓ 下端点と上端点を持つ
  • ✓ 文字列表現も返す

    • ✓ 下端点 3, 上端点 8 ならば文字列”[3,8]“を返す

    • ✓ 下端点 4, 上端点 9 ならば文字列”[4,9]“を返す

以下のコード部分は私が書いた部分だけど、おわかりいただけただろうか?

1
2
3
4
5
6
7
8
9
10
11
12
suite("ClosedRangeTest", () => {
suite("文字列表現を返す", () => {
test("下端点 3, 上端点 8 ならば文字列[3,8]を返す", () => {
closedRange = new ClosedRange(3, 8); (1)
assert.equal("[3,8]", closedRange.stringify());
});

test("下端点 4, 上端点 9 ならば文字列[4,9]を返す", () => {
closedRange = new ClosedRange(4, 9); (2)
assert.equal("[4,9]", closedRange.stringify());
});
});

(1),(2) の部分は JavaScript だとグローバル変数宣言になる! JavaScript の中でもトップレベルのあかんやつやらかしたけどペアの指摘のおかげで早期発見早期対応ができた。言い訳だけど最近は Ruby のコードばかり書いていたので変数もその感覚で書いてた。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
suite("ClosedRangeTest", () => {
suite("文字列表現を返す", () => {
test("下端点 3, 上端点 8 ならば文字列[3,8]を返す", () => {
const closedRange = new ClosedRange(3, 8);
1;
assert.equal("[3,8]", closedRange.stringify());
});

test("下端点 4, 上端点 9 ならば文字列[4,9]を返す", () => {
const closedRange = new ClosedRange(4, 9);
2;
assert.equal("[4,9]", closedRange.stringify());
});
});
});

(1),(2) は const で再代入不可のローカル変数にリファクタリングする。

あと、今回はバージョン管理システムを使って作業を進めているんだけどすでにこの時点で6回ぐらいコミットしてる。コミットメッセージに test とか refactor とかつけてやってるんだけどこれは Conventional Commits という仕様があるそうだ、初めて知った。

TDD&ペアプログラミング 実習(2 回目)

仕様

最初のグループをプログラムに実装したので次のグループに取り組む。

ただし、
上端点より下端点が大きい閉区間を作ることはできない。
整数の閉区間は指定した整数を含むかどうかを判定できる。
また、
別の閉区間と等価かどうかや、完全に含まれるかどうかも判定できる。
整数閉区間を示すクラス(あるいは構造体)をつくりたい。

TODO リスト

TODO リスト は前回のパターンの踏襲しつつとりあえず例外を投げる実装にしてみる。

  • ❏ 上端点より下端点が大きい閉区間を作ることはできない

    • ❏ 下端点 8, 上端点 3 ならばエラーが発生する

仮実装から実装へ

(1) でまず期待する振る舞いを書いて失敗する(レッド)ことを確認したら、(2) のコンストラクタ作成時に例外を投げるようにしてテストを成功させる(グリーン)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
suite("ClosedRangeTest", () => {
suite("文字列表現を返す", () => {
test("下端点 3, 上端点 8 ならば文字列[3,8]を返す", () => {
const closedRange = new ClosedRange(3, 8);
assert.equal("[3,8]", closedRange.stringify());
});

test("下端点 4, 上端点 9 ならば文字列[4,9]を返す", () => {
const closedRange = new ClosedRange(4, 9);
assert.equal("[4,9]", closedRange.stringify());
});
});
suite("上端点より下端点が大きい閉区間を作ることはできない", () => {
test("下端点 8, 上端点 3 ならばエラーが発生する", () => {
assert.throws(() => {
new ClosedRange(8, 3);
1;
}, "作れません");
});
});
});

class ClosedRange {
constructor(lower, upper) {
if (lower > upper) {
throw new Error("作れません");
2;
}
this._lower = lower;
this._upper = upper;
}
stringify() {
let result = `[${this._lower},${this._upper}]`;
return result;
}
}

例外メッセージが 作れません とか・・・ちょっとアレだけどもう少し全体のイメージができたてから リファクタリング しよう。

リファクタリング

その後、さらに TODO リスト を追加してコードに落とし込んでいったのでここで リファクタリング の時間に入る。

  • ✓ 上端点より下端点が大きい閉区間を作ることはできない

    • ✓ 下端点 8, 上端点 3 ならばエラーが発生する
  • ✓ 整数の閉区間は指定した整数を含むかどうかを判定できる

    • ✓ 閉区間[3,8]の場合、3 ならは含まれると判定(true)される

    • ✓ 閉区間[3,8]の場合、8 ならは含まれると判定(true)される

    • ✓ 閉区間[3,8]の場合、6 ならは含まれると判定(true)される

    • ✓ 閉区間[3,8]の場合、2 ならは含まれないと判定(false)される

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
suite("ClosedRangeTest", () => {
suite("文字列表現を返す", () => {
test("下端点 3, 上端点 8 ならば文字列[3,8]を返す", () => {
const closedRange = new ClosedRange(3, 8);
assert.equal("[3,8]", closedRange.stringify());
});

test("下端点 4, 上端点 9 ならば文字列[4,9]を返す", () => {
const closedRange = new ClosedRange(4, 9);
assert.equal("[4,9]", closedRange.stringify());
});
});
suite("上端点より下端点が大きい閉区間を作ることはできない", () => {
test("下端点 8, 上端点 3 ならばエラーが発生する", () => {
assert.throws(() => {
new ClosedRange(8, 3);
}, "作れません");
});
});
suite("整数の閉区間は指定した整数を含むかどうかを判定できる", () => {
test("閉区間[3,8]の場合、3ならは含まれると判定(true)される", () => {
const closedRange = new ClosedRange(3, 8);
1;
assert.isTrue(closedRange.isInRange(3));
});
test("閉区間[3,8]の場合、8ならは含まれると判定(true)される", () => {
const closedRange = new ClosedRange(3, 8);
2;
assert.isTrue(closedRange.isInRange(8));
});
test("閉区間[3,8]の場合、6ならは含まれると判定(true)される", () => {
const closedRange = new ClosedRange(3, 8);
3;
assert.isTrue(closedRange.isInRange(6));
});
test("閉区間[3,8]の場合、2ならは含まれないと判定(false)される", () => {
const closedRange = new ClosedRange(3, 8);
4;
assert.isFalse(closedRange.isInRange(2));
});
});
});

class ClosedRange {
constructor(lower, upper) {
if (lower > upper) {
throw new Error("作れません");
}
this._lower = lower;
this._upper = upper;
}
stringify() {
let result = `[${this._lower},${this._upper}]`;
return result;
}

isInRange(number) {
if (this._lower <= number && this._upper >= number) {
return true;
}
return false;
}
}

(1),(2),(3),(4)の部分でインスタンスの作成が重複しているのでここは メソッドの抽出 を使って呼び出しを一箇所(5)にまとめることにした。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
suite("整数の閉区間は指定した整数を含むかどうかを判定できる", () => {
let closedRange;
setup("前準備", () => {
closedRange = new ClosedRange(3, 8); (5)
});
test("閉区間[3,8]の場合、3ならは含まれると判定(true)される", () => {
assert.isTrue(closedRange.isInRange(3));
});
test("閉区間[3,8]の場合、8ならは含まれると判定(true)される", () => {
assert.isTrue(closedRange.isInRange(8));
});
test("閉区間[3,8]の場合、6ならは含まれると判定(true)される", () => {
assert.isTrue(closedRange.isInRange(6));
});
test("閉区間[3,8]の場合、2ならは含まれないと判定(false)される", () => {
assert.isFalse(closedRange.isInRange(2));
});
});
});

テストコードも壊れていないのでここでバージョン管理システムにコミットしてコードレビューに入る。

コードレビュー(1 回目)

光栄にもコードレビュー1回目の代表に指名していただいたのでありがたくレビューを受けることにした。

TODO リスト

  • ✓ 文字列表現も返す

    • ✓ 下端点 3, 上端点 8 ならば文字列”[3,8]“を返す

    • ✓ 下端点 4, 上端点 9 ならば文字列”[4,9]“を返す

  • ✓ 整数区間オブジェクトを作る

    • ✓ 下端点と上端点を持つ
  • ✓ 上端点より下端点が大きい閉区間を作ることはできない

    • ✓ 下端点 8, 上端点 3 ならばエラーが発生する
  • ✓ 整数の閉区間は指定した整数を含むかどうかを判定できる

    • ✓ 閉区間[3,8]の場合、3 ならは含まれると判定(true)される

    • ✓ 閉区間[3,8]の場合、8 ならは含まれると判定(true)される

    • ✓ 閉区間[3,8]の場合、6 ならは含まれると判定(true)される

    • ✓ 閉区間[3,8]の場合、2 ならは含まれないと判定(false)される

  • ❏ 別の閉区間と等価かどうか判定できる

    • ❏ 閉区間[3,8]と閉区間[3,8]の場合、等価と判定(true)される

    • ❏ 閉区間[3,8]と閉区間[4,8]の場合、等価ではないと判定(false)される

  • ❏ 別の閉区間に完全に含まれるかどうかも判定できる

    • ❏ 閉区間[3,8]と閉区間[4,7]の場合、完全に含まれると判定(true)される

    • ❏ 閉区間[3,8]と閉区間[1,2]の場合、完全に含まれてないと判定(false)される

進捗状況は 7 割といったところ。

コード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
suite("ClosedRangeTest", () => {
suite("文字列表現を返す", () => {
1;
test("下端点 3, 上端点 8 ならば文字列[3,8]を返す", () => {
const closedRange = new ClosedRange(3, 8);
assert.equal("[3,8]", closedRange.stringify());
});

test("下端点 4, 上端点 9 ならば文字列[4,9]を返す", () => {
const closedRange = new ClosedRange(4, 9);
assert.equal("[4,9]", closedRange.stringify());
});
});
suite("上端点より下端点が大きい閉区間を作ることはできない", () => {
test("下端点 8, 上端点 3 ならばエラーが発生する", () => {
assert.throws(() => {
new ClosedRange(8, 3);
}, "作れません");
});
});
suite("整数の閉区間は指定した整数を含むかどうかを判定できる", () => {
let closedRange;
setup("前準備", () => {
closedRange = new ClosedRange(3, 8);
});
test("閉区間[3,8]の場合、3ならは含まれると判定(true)される", () => {
assert.isTrue(closedRange.isInRange(3));
});
test("閉区間[3,8]の場合、8ならは含まれると判定(true)される", () => {
assert.isTrue(closedRange.isInRange(8));
});
test("閉区間[3,8]の場合、6ならは含まれると判定(true)される", () => {
assert.isTrue(closedRange.isInRange(6));
2;
});
test("閉区間[3,8]の場合、2ならは含まれないと判定(false)される", () => {
assert.isFalse(closedRange.isInRange(2));
});
});
suite(" 別の閉区間と等価かどうか判定できる", () => {
let closedRange;
setup("前準備", () => {
closedRange = new ClosedRange(3, 8);
});
test("閉区間[3,8]と閉区間[3,8]の場合、等価と判定(true)される", () => {
const closedRange2 = new ClosedRange(3, 8);
3;
assert.isTrue(closedRange.equal(closedRange2));
});
// - [ ] 閉区間[3,8]と閉区間[4,8]の場合、等価ではないと判定(false)される
});
});

class ClosedRange {
constructor(lower, upper) {
if (lower > upper) {
throw new Error("作れません");
4;
}
this._lower = lower;
this._upper = upper;
}
stringify() {
let result = `[${this._lower},${this._upper}]`;
return result;
}

isInRange(number) {
5;
if (this._lower <= number && this._upper >= number) {
return true;
}
return false;
}

equal(other) {
if (this._lower === other._lower && this._upper === this._upper)
return true;
return false;
}
}

レビュー

主要なレビュー指摘事項は

  • null の場合のテストケースが考慮されていない (1)

  • 境界値テストとして境界値内のテストは不要で境界値外テストが不足している (2)

  • 整数以外の値(小数)が渡された場合のテストケースが考慮されていない (3)

  • メソッドの名としてイメージがしずらい (5)

  • 範囲判定条件が分かりづらい (5)

(4) に関しては 値オブジェクト の生成パターンを意識していたのだけど例外を投げるパターンは設計アプローチの一つとして議論した。普段、ボッチ開発している自分としては人様からまさかりを投げていただくというありがたい経験ができた。

TDD&ペアプログラミング 実習(3 回目)

仕様

コードレビューも終わり最後の機能の実装に入る。

また、
別の閉区間と等価かどうかや、完全に含まれるかどうかも判定できる。
整数閉区間を示すクラス(あるいは構造体)をつくりたい。

TODO リスト

  • ✓ 別の閉区間と等価かどうか判定できる

    • ✓ 閉区間[3,8]と閉区間[3,8]の場合、等価と判定(true)される

    • ✓ 閉区間[3,8]と閉区間[4,8]の場合、等価ではないと判定(false)される

  • ❏ 別の閉区間に完全に含まれるかどうかも判定できる

    - ❏ 閉区間\[3,8\]と閉区間\[4,7\]の場合、完全に含まれると判定(true)される
    
    - ❏

    閉区間[3,8]と閉区間[1,2]の場合、完全に含まれてないと判定(false)される

リファクタリング

では機能を追加してリリース!と思っていたけどペアにまずはリファクタリングと指摘されてはっと我に返る。意識はしているんだけどついつい機能を作る方に意識が行ってしまうのでこのような指摘は非常にありがたい。まずはコードレビュー指摘事項から取り組むことにする。

このテストケースは境界値テストとしては不要なので

1
2
3
4
5
...
test("閉区間[3,8]の場合、6ならは含まれると判定(true)される", () => {
assert.isTrue(closedRange.isInRange(6));
});
...

不要なテストを不足するテストに変更する。

1
2
3
4
5
...
test("閉区間[3,8]の場合、9ならは含まれると判定(false)される", () => {
assert.isFalse(closedRange.isInRange(9));
});
...

次に指摘された isInRange メソッドだけど

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
test("閉区間[3,8]の場合、3ならは含まれると判定(true)される", () => {
assert.isTrue(closedRange.isInRange(3));
});
test("閉区間[3,8]の場合、8ならは含まれると判定(true)される", () => {
assert.isTrue(closedRange.isInRange(8));
});
test("閉区間[3,8]の場合、9ならは含まれると判定(false)される", () => {
assert.isFalse(closedRange.isInRange(9));
});
test("閉区間[3,8]の場合、2ならは含まれないと判定(false)される", () => {
assert.isFalse(closedRange.isInRange(2));
});
});
suite(" 別の閉区間と等価かどうか判定できる", () => {
...
class ClosedRange {
....
isInRange(number) {
if (this._lower <= number && this._upper >= number) {
return true;
}
...

ここは include にメソッド名を変更する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
test("閉区間[3,8]の場合、3ならは含まれると判定(true)される", () => {
assert.isTrue(closedRange.include(3));
});
test("閉区間[3,8]の場合、8ならは含まれると判定(true)される", () => {
assert.isTrue(closedRange.include(8));
});
test("閉区間[3,8]の場合、9ならは含まれると判定(false)される", () => {
assert.isFalse(closedRange.include(9));
});
test("閉区間[3,8]の場合、2ならは含まれないと判定(false)される", () => {
assert.isFalse(closedRange.include(2));
});
});
...
suite(" 別の閉区間と等価かどうか判定できる", () => {
class ClosedRange {
...
include(number) {
if (this._lower <= number && this._upper >= number) {
return true;
}
...

範囲判定の部分も比較演算子が反転すると可読性が下がるので

1
2
3
4
5
6
7
...
include(number) {
if (this._lower <= number && this._upper >= number) {
return true;
}
return false;
...

$3 \leqq X \leqq 8$ のように読めるように変更する。

1
2
3
4
5
6
7
...
include(number) {
if (this._lower <= number && number <= this._upper) {
return true;
}
return false;
...

リリース

なんとか時間内に仕様を満たすプログラムを作ることが出来た。

TODO リスト

  • ✓ 文字列表現も返す

    • ✓ 下端点 3, 上端点 8 ならば文字列”[3,8]“を返す

    • ✓ 下端点 4, 上端点 9 ならば文字列”[4,9]“を返す

  • ✓ 整数区間オブジェクトを作る

    • ✓ 下端点と上端点を持つ
  • ✓ 上端点より下端点が大きい閉区間を作ることはできない

    • ✓ 下端点 8, 上端点 3 ならばエラーが発生する
  • ✓ 整数の閉区間は指定した整数を含むかどうかを判定できる

    • ✓ 閉区間[3,8]の場合、3 ならは含まれると判定(true)される

    • ✓ 閉区間[3,8]の場合、8 ならは含まれると判定(true)される

    • ✓ 閉区間[3,8]の場合、9 ならは含まれると判定(false)される

    • ✓ 閉区間[3,8]の場合、2 ならは含まれないと判定(false)される

  • ✓ 別の閉区間と等価かどうか判定できる

    • ✓ 閉区間[3,8]と閉区間[3,8]の場合、等価と判定(true)される

    • ✓ 閉区間[3,8]と閉区間[4,8]の場合、等価ではないと判定(false)される

  • ✓ 別の閉区間に完全に含まれるかどうかも判定できる

    • ✓ 閉区間[3,8]と閉区間[4,7]の場合、完全に含まれると判定(true)される

    • ✓ 閉区間[3,8]と閉区間[1,2]の場合、完全に含まれてないと判定(false)される

  • 後で考える

    • ❏ null や undefined の場合

    • ❏ 小数の場合

クラス図

diag-e41cf3b7f91aaeb501dda0c5a031bc6b.png

プログラム

closed_range.test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
mocha.setup("tdd");

const assert = chai.assert;
suite("ClosedRangeTest", () => {
suite("文字列表現を返す", () => {
test("下端点 3, 上端点 8 ならば文字列[3,8]を返す", () => {
const closedRange = new ClosedRange(3, 8);
assert.equal("[3,8]", closedRange.stringify());
});

test("下端点 4, 上端点 9 ならば文字列[4,9]を返す", () => {
const closedRange = new ClosedRange(4, 9);
assert.equal("[4,9]", closedRange.stringify());
});
});
suite("上端点より下端点が大きい閉区間を作ることはできない", () => {
test("下端点 8, 上端点 3 ならばエラーが発生する", () => {
assert.throws(() => {
new ClosedRange(8, 3);
}, "上端点(3)より下端点(8)が大きい閉区間は作れません");
});
});
suite("整数の閉区間は指定した整数を含むかどうかを判定できる", () => {
let closedRange;
setup("前準備", () => {
closedRange = new ClosedRange(3, 8);
});
test("閉区間[3,8]の場合、3ならは含まれると判定(true)される", () => {
assert.isTrue(closedRange.include(3));
});
test("閉区間[3,8]の場合、8ならは含まれると判定(true)される", () => {
assert.isTrue(closedRange.include(8));
});
test("閉区間[3,8]の場合、9ならは含まれると判定(false)される", () => {
assert.isFalse(closedRange.include(9));
});
test("閉区間[3,8]の場合、2ならは含まれないと判定(false)される", () => {
assert.isFalse(closedRange.include(2));
});
});
suite(" 別の閉区間と等価かどうか判定できる", () => {
let closedRange;
setup("前準備", () => {
closedRange = new ClosedRange(3, 8);
});
test("閉区間[3,8]と閉区間[3,8]の場合、等価と判定(true)される", () => {
const closedRange2 = new ClosedRange(3, 8);
assert.isTrue(closedRange.equal(closedRange2));
});
test("閉区間[3,8]と閉区間[4,8]の場合、等価ではないと判定(false)される", () => {
const closedRange2 = new ClosedRange(4, 8);
assert.isFalse(closedRange.equal(closedRange2));
});
});

suite("別の閉区間に完全に含まれるかどうかも判定できる", () => {
let closedRange;
setup("前準備", () => {
closedRange = new ClosedRange(3, 8);
});
test("閉区間[3,8]と閉区間[4,7]の場合、完全に含まれると判定(true)される", () => {
const closedRange2 = new ClosedRange(4, 7);
assert.isTrue(closedRange.includeObject(closedRange2));
});
test("閉区間[3,8]と閉区間[1,2]の場合、完全に含まれてないと判定(false)される ", () => {
const closedRange2 = new ClosedRange(1, 2);
assert.isFalse(closedRange.includeObject(closedRange2));
});
});
});

closed_range.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class ClosedRange {
constructor(lower, upper) {
if (lower > upper) {
throw new Error(
`上端点(${upper})より下端点(${lower})が大きい閉区間は作れません`
);
}
this._lower = lower;
this._upper = upper;
}
stringify() {
return `[${this._lower},${this._upper}]`;
}

include(number) {
return this._lower <= number && number <= this._upper;
}

equal(other) {
return this._lower === other._lower && this._upper === this._upper;
}

includeObject(object) {
return this._lower <= object._lower && object._upper <= this._upper;
}
}

ついでに デプロイ してみた。

コードレビュー(2 回目)

コードレビュー2回目は Ruby チームの発表。レビューする立場になってみるとこれはこれで難しいことがわかった。あと、プロダクトコードからではなくテストコードを構造化して日本語仕様として読むことでそこからプロダクトコードの問題点を指摘するというアプローチは言語を問わず有効だと思った。

クロージング

やはり俺のテスト駆動開発に対する理解は間違っていなかった という感触をつかめた TDD Boot Camp ではあったけどペアプログラミングに関しては今まで自分がやっていたのはペアプログラミングではなかった やはり俺のペアプログラミングは間違っている ので今回の経験を次回から反映していきたい。今回は品質に関しても多く言及していたけどその中で自分のテスト駆動開発に対する立ち位置は 動作するサンプルを書きながら設計を改善する活動 でありソフトウェア品質という面ではどこか一歩引いていたことを自覚できた。

つまり やはり俺のテスト駆動開発は動作するサンプルを書きながら設計を改善する活動だった ことを明確に意識することが出来た一方で やはり俺のテスト駆動開発における品質に対する意識は低かった のである。今後の課題として取り組みたい。あと、レガシーコード改善ガイドでは改善手順のなかでそのまま テスト駆動開発 を使うと記述されているのでテスト駆動開発はなにも新規開発だけで使える開発手法ではないという知見も今後広まっていくのではないかと思った。

参照

Author: k2works
Link: https://k2works.github.io/2020/02/28/2020-04-08/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.