Skip to content.

Sections
Personal tools
You are here: Home » コミュニティ » masarl memorial » masarl.cocolog-nifty.com » main » 2004 » 02 » Cotton Bolls: トートロジーのアンチパターン

Cotton Bolls: トートロジーのアンチパターン

Document Actions

« 開発環境としてのMake | トップページ | ココログのカスタマイズ »

2004.02.29

トートロジーのアンチパターン

テストファーストを行うと,テストコードの中に実装コードが紛れ込むことがある.

先日,工場の機械類を監視するシステムの開発をしていた.監視システムは機械の状況をポーリングし,故障を発見すると画面上にダイアログを表示する.機械は複数あり,一度に故障が何箇所も発生してしまう場合がある.このとき,故障の数だけダイアログを表示し,位置をずらして古いダイアログを隠してしまわないようにしなければならない.また,24時間稼動なので,ユーザがいないと画面上にダイアログがどんどんたまっていく.それを防ぐため,ダイアログが表示される数の上限を決め,古いダイアログは自動的に閉じて消費メモリを増やさない工夫が必要だ.

そこで,ダイアログの配置と上限を管理するクラスをTroubleDialogFactoryにする.ダイアログ全体の管理はそれを生成するTroubleDialogFactoryに任せ,故障をフックするコードがTroubleDialogFactoryにダイアログの生成と表示を依頼する.そうすれば,現在表示されているダイアログの状況から次に表示するダイアログの位置と上限が管理できそうだ.

まずはテストコードだ.TroubleDialogFactoryTestクラスを書こう.最初は,故障1件でダイアログが画面中央に表示されればよい.画面中央の位置を知るには,スクリーンサイズを知る必要がある.調べてみると,java.awt.Toolkitクラスを使えばできそうだ.

import junit.extensions.converter.*;
import java.awt.*;

public class TroubleDialogFactoryTest extends ConverterTestCase {
  public TroubleDialogFactoryTest(String name) {
    super(name);
  }
  public void test() throws Exception {
    TroubleDialogFactory factory = new TroubleDialogFactory(new Frame());
    TroubleRecord record = new TroubleRecord();
    Dialog dialog = factory.create(record);

    Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
    Point expected =  new Point(screenSize.width / 2 - dialog.getWidth() / 2,
                                screenSize.height / 2 - dialog.getHeight() / 2);
    converter = new BeanValueConverter(converter, Dialog.class, "location");
    converter = new ClassFieldValueConverter(converter, "{0},{1}",
                                             Point.class, "x", "y");
    converter.assertEquals(expected, dialog);
  }
}

上のコードでは,まずダイアログの親ウィンドウとなるFrameオブジェクトでTroubleDialogFacotryを生成する.次に故障を表すTroubleRecordオブジェクトを作り,Factoryのcreateメソッドでダイアログを生成する.このダイアログの位置をテストすればよいわけだ.

ダイアログ位置の期待値を求めるため,Toolkit.getScreenSizeメソッドでスクリーンサイズを取得する.スクリーンサイズとダイアログの大きさから,画面中央にダイアログがある場合の左上の座標(Dialog.getLocationの返り値)を求め,expected変数に代入する.最後のconverterは,DialogをgetLocationの返り値に,Pointをx, yのフィールド値に変換するコードだ.

テストコードができたので,適当にTroubleDialogFactory.createメソッドを書いて失敗させる:

There was 1 failure:
1) test(TroubleDialogFactoryTest)
   junit.framework.AssertionFailedError:expected:<640,512> but was:<0,0>

何も設定しなければダイアログの位置は0, 0になっているようだ.今度はテストが通るように実装してみよう:

import java.awt.*;

public class TroubleDialogFactory {
  private Frame parent;
  public TroubleDialogFactory(Frame parent) {
    this.parent = parent;
  }
  public Dialog create(TroubleRecord record) {
    Dialog dialog = new Dialog(parent, "故障ダイアログ");
    Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
    Point location = new Point(screenSize.width / 2 - dialog.getWidth() / 2,
                               screenSize.height / 2 - dialog.getHeight() / 2);
    dialog.setLocation(location);
    return dialog;
  }
}

なんと,ダイアログの位置を求める部分がテストコードと同じになってしまった!――これがトートロジーのアンチパターンと呼んでいるものだ.テストコードと実装コードが同じになっては,そもそもテストする意味がない.同語反復で何もテストしていないのだ(このパターンは,Kent Beckのいう"Fake it"とは異なる.上のコードは実装として正しくみえるからだ).

トートロジーのアンチパターンを解決するには,「テストコードには特殊解,実装コードには一般解」ということを思い出せばよい.つまり,特殊解が設定できるクラス設計にしてしまうわけだ:

public void test() throws Exception {
  TroubleDialogFactory factory = new TroubleDialogFactory(new Frame());
  factory.setScreenSize(new Dimension(600, 400));
  factory.setDefaultDialogSize(new Dimension(200, 100));

  TroubleRecord record = new TroubleRecord();
  Dialog dialog = factory.create(record);
  converter = new BeanValueConverter(converter, Dialog.class, "location");
  converter = new ClassFieldValueConverter(converter, "{0},{1}",
                                           Point.class, "x", "y");
  converter.assertEquals("200,150", dialog);
}

上のコードでは,TroubleDialogFactoryにスクリーンサイズとダイアログサイズを設定できるようにした.こうすれば,特殊解がはっきりわかるテストコードになり,トートロジーは消える.さらにその特典として,以後のテストケース(複数の故障が出た場合のテスト,ダイアログ数が上限値を超える場合のテスト)が格段に書きやすくなる.これらはテストのためだけに導入されたメソッドだが,テストを書きやすくし,コードの信頼性を上げるには必要なメソッドなのだ.

ここで,実際にスクリーンサイズやダイアログサイズを設定しない場合のテストはどうするのか,という疑問を持つ人もいるかもしれない.しかし,それは受け入れテスト(といっても自動化する必要はない)で確認すればいいだけの話だ.そもそもJUnitのコードですべてをテストできないし,実際に動かさないと絶対に信頼はできない.

トートロジーのアンチパターンは,「実装の詳細をテストする」というアンチパターンと共に現れることも多い.JUnitを使い始めたころ,SQL文を直接テストするテストコードを書いていたことがある.検索条件からくるSQL文のいろんなパターンに対応しようとすると,どうしてもテストコードが一般的になり,テストコードと実装コードが同じになってしまう.これは,SQL文という実装の詳細をテストしようとしたことからくる誤りだ.このあたりの話は,XP をはじめよう:トートロジーになってしまったテストにも少し書いた.このときは,特殊解を設定しやすいクラス設計で解決したわけではなく,テスト対象をブラックボックス化し,外側からテストを行うことでトートロジーをなくした.

XPにはYAGNIという過度の機能拡張を戒めるルールがある.しかし,一方でテストコードから元の仕様では必要のない機能拡張が求められることも多い.今回のようなメソッド単位の小さなものから,モデルとビューを分離するというアーキテクチャレベルのものまで,様々なものがあるようだ.この辺のバランスがXPのコードの品質を上げるポイントなのかもしれない.

07:45 PM in JUnit | 固定リンク

トラックバック

この記事のトラックバックURL:
http://app.cocolog-nifty.com/t/trackback/246009

この記事へのトラックバック一覧です: トートロジーのアンチパターン:

コメント

コメントを書く