TDD Boot Campに参加しました

【追記】今回作成したコードを最下部に追記しました。

まずは、主催者・スタッフの方々にお礼を言いたいと思います。講演だけでもすごく勉強になったのに、こうやって演習の時間まで設けてもらうのはすごく準備等に時間や手間がかかったことでしょう。本当にお疲れ様です。

私個人としては、各テーブル内で一番よかったと思う人をテーブル内メンバー6人で決める「テーブル賞」に推薦していただきました!まだまだ未熟な自分というのは自覚していますが、こうやって認められるのは素直に嬉しい限りです。ありがとうございます♪

で、グループ賞として技術評論者様から本一冊をいただける権利をもらいました。あまり選択する時間も無かったので、前から気になっていた「パターン・Wiki・XP」の本を選びました。が、後々考えると会場で宣伝があった「スクラムアジャイルプロジェクト管理」を選んでいればよかったかなー。

パターン、Wiki、XP ~時を超えた創造の原則 (WEB+DB PRESS plusシリーズ)

パターン、Wiki、XP ~時を超えた創造の原則 (WEB+DB PRESS plusシリーズ)


実践!アジャイルプロジェクト管理 -スクラムではじめる最強エンタープライズ開発-

実践!アジャイルプロジェクト管理 -スクラムではじめる最強エンタープライズ開発-


基本的に演習や講演に集中しててあまりログは取っていないけど、覚えている範囲で。

Lasse氏の講演

  • TDDやるとコードは15%増しになるけど、欠陥率が最大90%減になる
  • レガシーコードは最初にテストを作る
  • デモ:coberturaに機能追加する*1
    1. テストできそうな箇所を小さい範囲にメソッド抽出
    2. さらに、副作用がある箇所をprotectedメソッドに抽出
    3. サブクラスで副作用メソッドをオーバーライドして無効化
    4. テストのために、検出用変数をprivateからpublicに変更
    5. 検出用変数にアクセスして、assertを記述

スポンサーセッション

TDD演習

  • お題は「LRUキャッシュ付Map」の実装
  • 初めてのペアプロ体験
  • 2人で意見を交わしながら進めていくのはいい感じ、けど疲れた
  • コーディング自体は、自分PCでやるのも他人PCでやるのもちょっとやりづらかった
    • 無駄に自分の開発環境をEmacsバインドにカスタマイズしてるせい…orz

Lasse氏のTDDコード

  • テストコードが洗練されてて綺麗
    • テストに関係ないものは、徹底的に排除する
    • Mapを扱うお題だけどvalueの値自体はテストに関係ないので、put(key, key)みたいなヘルパーメソッドを作る
  • 時間関係のテストはFakeを使って、テストをやりやすくする

振り返り

  • 環境周りを整備すればよかった
    • 最低、リポジトリ用意
    • もっと言えば、CIも用意
      • CIとの連携を手軽にやるならMavenになるけど、知らない人にはハードル高い
      • 演習のレベルではあまりCIのメリットがないかも*2
  • 今回のTDD演習の内容は基礎として身につけておきたい
    • 実業務になると、もっと多くの要因が絡んできてそれに対処しなければならないことが多くなるだろうから
    • 例えば今関わっている携帯アプリの開発だと、実機を使う部分と使わなくてもよい部分をいかにして切り分けてテストしやすくするかが大事



今回のTDD演習で作ったコードです。途中で時間切れになったのでgdgdなところも多々ありますが、晒しておきます。

package lrucache;

import java.util.*;

public class LRUCache {

	private static final int DEFAULT_SIZE = 2;

	private final Map<String, String> map = new HashMap<String, String>();
	private final List<String> list = new ArrayList<String>();

	private int size;

	public LRUCache(int i) {
		size = i;
	}

	public LRUCache() {
		this(DEFAULT_SIZE);
	}

	public void put(String key, String value) {
		if (map.size() >= size) {
			map.remove(list.remove(0));
		}
		map.put(key, value);
		updateList(key);
	}

	public String get(String key) {
		updateList(key);
		return map.get(key);
	}

	private void updateList(String key) {
		list.remove(key);
		list.add(key);
	}

	public String getNewest() {
		return list.get(list.size() - 1);
	}

	public void setSize(int newSize) {
		int currentSize = list.size();
		if (newSize >= this.size || currentSize <= newSize) {
			this.size = newSize;
			return;
		}

		this.size = newSize;
		for (int i = getStartIndex(); i >= 0; i--) {
			map.remove(list.remove(i));
		}
	}

	private int getStartIndex() {
		return list.size() - this.size - 1;
	}

}
package lrucache;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;

import org.junit.*;

public class LRUCacheTest {

	// --------------------------------------------------- 最初の仕様

	@Test
	public void putとgetができること() {
		LRUCache cache = new LRUCache();
		cache.put("a", "A");
		cache.put("b", "B");
		assertEquals("A", cache.get("a"));
		assertEquals("B", cache.get("b"));
	}

	@Test
	public void 最初にputしたものが削除されること() {
		LRUCache cache = new LRUCache();
		cache.put("a", "A");
		cache.put("b", "B");
		cache.put("c", "C");
		cache.put("d", "D");
		assertNull(cache.get("a"));
		assertNull(cache.get("b"));
		assertEquals("C", cache.get("c"));
	}

	@Test
	public void getしたやつが一番あたらしいこと() {
		LRUCache cache = new LRUCache();
		cache.put("a", "A");
		cache.put("b", "B");
		cache.get("a");
		assertEquals("a", cache.getNewest());
		cache.put("c", "C");
		assertEquals("c", cache.getNewest());
	}

	@Test
	public void 最初にputしても途中でgetされたものは削除されないこと() {
		LRUCache cache = new LRUCache();
		cache.put("a", "A");
		cache.put("b", "B");
		cache.get("a");
		cache.put("c", "C");

		assertEquals("A", cache.get("a"));
		assertNull(cache.get("b"));
		assertEquals("C", cache.get("c"));
	}

	@Test
	public void 最初に最大値を指定できること() throws Exception {
		LRUCache cache = new LRUCache(3);
		cache.put("a", "A");
		cache.put("b", "B");
		cache.put("c", "C");
		cache.put("d", "D");

		assertNull(cache.get("a"));
		assertEquals("B", cache.get("b"));
		assertEquals("C", cache.get("c"));
		assertEquals("D", cache.get("d"));
	}

	// --------------------------------------------------- 仕様変更1

	@Test
	public void 最大値を途中で増やせること() throws Exception {
		LRUCache cache = new LRUCache();
		cache.put("a", "A");
		cache.put("b", "B");
		cache.put("c", "C");
		cache.setSize(3);
		cache.put("d", "D");
		assertEquals("B", cache.get("b"));

	}

	@Test
	public void 最大値を途中で減らせること_満タンの時() throws Exception {
		LRUCache cache = new LRUCache();
		cache.put("a", "A");
		cache.put("b", "B");
		cache.put("c", "C");
		cache.setSize(1);
		assertNull(cache.get("a"));
		assertNull(cache.get("b"));
		assertEquals("C", cache.get("c"));
	}

	@Test
	public void 最大値を途中で減らせること_減らしたときに上限までいっていないとき() throws Exception {
		LRUCache cache = new LRUCache(3);
		cache.put("a", "A");
		cache.setSize(1);
		assertEquals("A", cache.get("a"));
		cache.put("b", "B");
		assertNull(cache.get("a"));
		assertEquals("B", cache.get("b"));
	}

	@Test
	public void 最大値を途中で減らせること_減らしたときに上限を超えているとき21() throws Exception {
		LRUCache cache = new LRUCache(3);
		cache.put("a", "A");
		cache.put("b", "B");
		cache.setSize(1);
		assertNull(cache.get("a"));
		assertEquals("B", cache.get("b"));
	}

	@Test
	public void 最大値を途中で減らせること_減らしたときに上限を超えているとき42() throws Exception {
		LRUCache cache = new LRUCache(4);
		cache.put("a", "A");
		cache.put("b", "B");
		cache.put("c", "C");
		cache.put("d", "D");
		cache.setSize(2);
		assertNull(cache.get("a"));
		assertEquals("D", cache.get("d"));
		assertEquals("C", cache.get("c"));
	}

	@Test
	public void 最大値を途中で減らせること_減らしたときに上限を超えているとき53() throws Exception {
		LRUCache cache = new LRUCache(5);
		cache.put("a", "A");
		cache.put("b", "B");
		cache.put("c", "C");
		cache.put("d", "D");
		cache.put("e", "E");
		cache.setSize(3);
		assertNull(cache.get("a"));
		assertNull(cache.get("b"));
		assertEquals("E", cache.get("e"));
		assertEquals("D", cache.get("d"));
		assertEquals("C", cache.get("c"));
	}

	@Test
	public void 同じものをputしたときも新しくなること() throws Exception {
		LRUCache cache = new LRUCache();
		cache.put("a", "A");
		cache.put("b", "B");
		cache.put("a", "A");
		cache.put("c", "C");
		assertEquals("A", cache.get("a"));
	}

	// --------------------------------------------------- 仕様変更2

	@Test
	public void putまたはgetしたときの時間を取得できること() throws Exception {
		fail();
	}

	@Ignore
	@Test
	public void 時間切れのオブジェクトを削除できること() throws Exception {
		fail();
	}
}

*1:この辺ちょっとうろ覚え。もし間違っていたらご指摘ください。

*2:プロダクトクラス・テストクラスそれぞれ1つずつだから、コンパイルエラーやテスト漏れもまず起きないだろうし