2008年1月8日火曜日

GUIの自動試験ツール、FEST

年末年始にGUIの自動試験ツールであるFESTを試したのでその紹介を。ちなみに、FESTとは「Fixtures for Easy Software Testing」の略称で、いくつかのProjectの組み合わせとなっている。昔「abbot」というProjectだったものの後継だと思われる。
  1. いつものようにmaven projectを作成し、pom.xmlを以下のように編集する。
    <project xmlns="http://maven.apache.org/POM/4.0.0"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <groupId>shin1o</groupId>
      <artifactId>studyFest01</artifactId>
      <packaging>jar</packaging>
      <version>1.0-SNAPSHOT</version>
      <name>studyFest01</name>
      <url>http://maven.apache.org</url>
      <dependencies>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.4</version>
        </dependency>
        <dependency>
          <groupId>fest</groupId>
          <artifactId>fest-swing</artifactId>
          <version>0.7</version>
        </dependency>
        <dependency>
          <groupId>fest</groupId>
          <artifactId>fest-assert</artifactId>
          <version>0.7</version>
        </dependency>
      </dependencies>
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
              <source>1.5</source>
              <target>1.5</target>
            </configuration>
          </plugin>
        </plugins>
      </build>
      <repositories>
        <repository>
          <id>fest</id>
          <name>fest</name>
          <url>http://fest.googlecode.com/svn/trunk/fest/m2/repository</url>
        </repository>
      </repositories>
    </project>
    
    2008/01/04に0.8がリリースされている模様。
  2. 適当に試験対象のFrameを作成する。加算をするFrameをサンプルとして使用しているけど、中身はどーでもいい。html上で長くなりすぎないよぅにformatもしていないのでコピペする場合はctrl+fで。
    package shin1o;
    
    import javax.swing.*;
    
    @SuppressWarnings("serial")
    public class MyFrame extends JFrame {
      JTextField text1, text2, text3;
      JButton button1, button2;
    
      public MyFrame() {
        getContentPane().setLayout(new java.awt.BorderLayout());
        setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    
        JPanel panel = new JPanel(new java.awt.FlowLayout());
        text1 = new JTextField(); text1.setName("text1");
        text1.setPreferredSize(new java.awt.Dimension(125, 25));
        text2 = new JTextField(); text2.setName("text2");
        text2.setPreferredSize(new java.awt.Dimension(125, 25));
        text3 = new JTextField(); text3.setName("text3");
        text3.setEditable(false);
        text3.setPreferredSize(new java.awt.Dimension(125, 25));
        panel.add(text1); panel.add(text2); panel.add(text3);
        getContentPane().add(panel, java.awt.BorderLayout.NORTH);
    
        button1 = new JButton(new AbstractAction() {
          public void actionPerformed(java.awt.event.ActionEvent event) {
            int int1 = 0, int2 = 0;
            try {
              int1 = Integer.parseInt(text1.getText());
            } catch (NumberFormatException e) {
              text1.requestFocusInWindow();
            }
            try {
              int2 = Integer.parseInt(text2.getText());
            } catch (NumberFormatException e) {
              text2.requestFocusInWindow();
            }
            text3.setText(String.valueOf(int1 + int2));
          }
        });
        button1.setText("add"); button1.setName("button1");
        button2 = new JButton(new AbstractAction() {
          public void actionPerformed(java.awt.event.ActionEvent event) {
            text1.setText(""); text2.setText(""); text3.setText("");
          }
        });
        button2.setText("clear"); button2.setName("button2");
        panel = new JPanel(new java.awt.FlowLayout());
        panel.add(button1); panel.add(button2);
        getContentPane().add(panel, java.awt.BorderLayout.SOUTH);
    
        setTitle("MyFrame"); setSize(400, 150);
      }
    }
  3. ここからがFESTの使い方。まずは以下のようなカンジがFESTの入り口となる。
    package shin1o;
    
    import java.awt.event.KeyEvent;
    
    import org.fest.assertions.Assertions;
    import org.fest.swing.core.MouseButton;
    import org.fest.swing.core.RobotFixture;
    import org.fest.swing.fixture.FrameFixture;
    import org.junit.*;
    
    public class MyFrameTest {
      FrameFixture frame;
      RobotFixture robot;
    
      @Before
      public void setUp() {
        frame = new FrameFixture(new MyFrame());
        robot = frame.robot;
        frame.show().target.setLocation(200, 200);
        robot.delay(1000);
      }
    
      @After
      public void tearDown() {
        robot.cleanUp();
      }
    
      @Test
      public void test01() {
        // 100
        frame.textBox("text1").pressAndReleaseKeys(KeyEvent.VK_1,
            KeyEvent.VK_0, KeyEvent.VK_0);
        Assertions.assertThat(frame.textBox("text1").text()).isEqualTo("100");
      }
    }
    このTestCaseをJUnitに実行してもらうと、text1に"100"という文字が入力され、それをassertしている処理が成功する。

重要なClass…操作系

org.fest.swing.fixture.FrameFixture」「org.fest.swing.core.RobotFixture」のふたつのインスタンスが「操作のエミュレート」の面で見た場合に肝となるClass。
  1. FrameFixtureインスタンスを作成する。コンストラクタの第一引数にはWindowのインスタンスを渡す。
  2. RobotFixtureをFrameFixtureから取得する。
  3. FrameFixtureのインスタンスやRobotFixtureのインスタンスを使って色々操作をする。
注意点として、RobotFixtureのインスタンスはひとつしか作成してはいけないという事。FrameFixtureを複数作成する時は、二つめ以降のFrameFixtureのコンストラクタの第一引数にひとつめのFrameFixtureから取得できるRobotFixtureを指定し、第二引数にWindowのインスタンスを渡すこと。今日これではまって困った。RobotFixtureのインスタンスを複数作成してしまった瞬間に処理が帰ってこなくなってしまぅんだヨー!年始の学習が役に立つ場面がさっそく来たぜ!とか思ってたら開始早々に出鼻を挫かれるとか泣けた。Alex Ruiz@FESTの中の人、Thanksでした。 ちなみにこのインターフェースは…そぅ、あちこちで流行りの「fluentなインターフェース」ですね。

重要なClass…評価系

org.fest.assertions.Assertions」にある「assertThat()」メソッド。assertThat()はJUnitでも4.4から同じ名称のメソッドが追加されてたね。どっちが先かは知らんけど、FESTのassertThatはこれまた流れるようなインターフェースで記述することになります。ちなみに、actual, expectedの値を指定する順番がJUnitのassertメソッドと比べると逆になるので注意が必要かも。

もぅ少しまともなTestを書いてみる

せっかく加算できるんだから、加算させてみよう。といぅ事でFrameFixtureをメインに使って操作をし、org.fest.assertions.Assertionsを使って評価をするよぅにtest01()を修正してみる。
@Test public void test01() {
  // 100
  frame.textBox("text1").pressAndReleaseKeys(KeyEvent.VK_1,
      KeyEvent.VK_0, KeyEvent.VK_0);
  Assertions.assertThat(frame.textBox("text1").text()).isEqualTo("100");
  // 200
  frame.textBox("text2").pressAndReleaseKeys(KeyEvent.VK_2,
      KeyEvent.VK_0, KeyEvent.VK_0);
  Assertions.assertThat(frame.textBox("text2").text()).isEqualTo("200");
  robot.delay(1000);
  // click "add"
  frame.button("button1").click();
  Assertions.assertThat(frame.textBox("text1").text()).isEqualTo("100");
  Assertions.assertThat(frame.textBox("text2").text()).isEqualTo("200");
  Assertions.assertThat(frame.textBox("text3").text()).isEqualTo("300");
  robot.delay(1000);
  // click "clear"
  frame.button("button2").click();
  Assertions.assertThat(frame.textBox("text1").text()).isEqualTo("");
  Assertions.assertThat(frame.textBox("text2").text()).isEqualTo("");
  Assertions.assertThat(frame.textBox("text3").text()).isEqualTo("");
  robot.delay(1000);
}
text1に"100"、text2に"200"を入力し、「add」してみたり「clear」してみてその結果をassertThat().isEqualTo()で評価している。

同じTestを違う書き方で書く

次はFrameFixtureではなくRobotFixtureメインで操作をしてみる。FrameFixtureのFinderを使わないので、Frameをとっかかりに外から各Componentにアクセスできることが前提条件となる。ただ、Finderが必要ないため各ComponentにsetName()しなくても良いのはラクかもしれん。そして、慣れているorg.junit.Assert#assertEquals()で評価を行う。
@Test public void test02() {
  MyFrame myFrame = (MyFrame) frame.target;
  // 100
  myFrame.text1.requestFocusInWindow();
  robot.pressAndReleaseKeys(KeyEvent.VK_1, KeyEvent.VK_0, KeyEvent.VK_0);
  Assert.assertEquals("100", myFrame.text1.getText());
  // 200
  myFrame.text2.requestFocusInWindow();
  robot.pressAndReleaseKeys(KeyEvent.VK_2, KeyEvent.VK_0, KeyEvent.VK_0);
  Assert.assertEquals("200", myFrame.text2.getText());
  robot.delay(1000);
  // click "add"
  robot.click(myFrame.button1, MouseButton.LEFT_BUTTON, 1);
  Assert.assertEquals("100", myFrame.text1.getText());
  Assert.assertEquals("200", myFrame.text2.getText());
  Assert.assertEquals("300", myFrame.text3.getText());
  robot.delay(1000);
  // click "clear"
  robot.click(myFrame.button2, MouseButton.LEFT_BUTTON, 1);
  Assert.assertEquals("", myFrame.text1.getText());
  Assert.assertEquals("", myFrame.text2.getText());
  Assert.assertEquals("", myFrame.text3.getText());
  robot.delay(1000);
}

さらに違う書き方

といっても、test02()とほとんど変わらない。そもそも、test02()では「各Componentを直接参照できる」んだから、操作系の細かいエミュレートはどーでも良い時なんかはそのままJComponentのメソッドを使えばえぇがな!というやり方。test02()では主にKeyの入力系を「requestFocusInWindow()して1文字ずつKeyCodeを送る」というやり方をしている。
myFrame.text1.requestFocusInWindow();
robot.pressAndReleaseKeys(KeyEvent.VK_1, KeyEvent.VK_0, KeyEvent.VK_0);
これが面倒なので、次の一行にまとめるだけ。
myFrame.text1.setText("100");
Testをあんまり真面目に書きすぎても時間がもったいないし、手を抜けるとこは抜いた方が良い。日本語の問題も心配だし。 Swingが信用ならないからリアルにエミュレートしたいところだが、そもそもそんな処理の違い(リアルにエミュレートするか直接setText()するか)で問題が発生するなら、おそらく業務的な要件といぅよりはFramework側だったりライブラリ側の問題だろぅから、あんまり重要では無いだろぅし。 こんなカンジでかなり便利に使える模様です。ちなみにこのエントリでは最も気になっていたFEST-SwingFEST-Assertしか使って無いけど、他にもFEST-MockFEST-Reflectというモジュール群があります。FEST-Mockは想像がつくけど、FEST-Reflectionってのがどんなものか気になる。ひょっとして試験フェーズに特化したDI/AOPコンテナだったり?とか思うとwktkするなぁ!

4 件のコメント:

youchan さんのコメント...

GUIのテストが必要になって、FESTを評価しています。
こちらのブログ記事が大変参考になりました。ありがとうございます。

私のほうで評価していて一点補足がありますので、コメントさせていただきいます。

Assertについてですが、Fixtureを使って以下のようにも記述することができます。
この方法なら、コンポーネントの参照を取れなくてもファインダーから直接アクセスすることができます。

frame.textBox("text1").requireText("100")

youchan さんのコメント...

すいません。もう一点補足です。(というかこっちのほうが重要かも)

click()やrequireText()などのメソッドはいわゆる「流れるインタフェース」になっていて、以下のようにメソッドチェーンでつなぐことができます。

frame.textBox("text1")
.click()
.requireText("100");

テストを気持ちよく記述できる、とても優れたインタフェースだと思います。

sss さんのコメント...

コメントありがとうございます。

Componentのfindもfluentなインターフェースについても、エントリ中にて紹介しております。

fluentなインターフェースは便利ですよね〜。
Componentのfindに関しては、個人的には型安全を確保する為には文字列での名称指定はちょっとな〜、とか思ったりしてはいました。
あと、注意点としてこの記事は随分昔の記事なので、今のFestとは多少違ったりするかもしれません。

師子乃 さんのコメント...

こんにちは。

GUI用の試験ツール、試したいですね。