Springのテストで気になること

Spring Framework上で動くサービスをJUnitでテストすると、自分の意図しない動きをすることがあって、お家で調査。お家だと、Webサイトのアクセス制限とかないし(GitHubとかにもアクセスできるw)、ネットワークの帯域の制限もないし(ダウンロードしほうだい)、PCのスペック高いからEclipseとかサクサク動くし(しょっちゅう固まったりしない)、画面の解像度高いし(調査時間の半分はスクロールだったりしない)。はっきりいって、圧倒的に効率がよい。
職場は仕方ないの、、限られた資源をみんなで分け合うから。。優秀な技術者に、良い環境を与えてあげてください。


さ、気を取り直して。
EclipseMavenプロジェクトを作って、設定でソースコードのダウンロードをオンにして、pom.xmlに必要なアプリ(とりあえず以下)を追加してやって、Mavenの「プロジェクトの更新」を実行すれば、準備オッケー。1文。

  <dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>4.1.6.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>4.1.6.RELEASE</version>
    </dependency>
  </dependencies>

で、いくつかのファイルを、古典的なやり方で追加した。この方が、挙動がわかりやすいと思ったからだ。ただ最近のやり方に精通してないからではないかって?ご名答。完全に浦島太郎です。

ほぼほぼ「Hello World!」なのだが、HelloMessageData クラスから、staticな文字列を拾ってくるところだけ、追加している。ちょっとうっとうしいが、各ファイルの中身を書いておく。


HelloMessageData.java … staticな文字列を保持するだけのクラス。疑似データベースといったところ

package hello;

public class HelloMessageData {

    private static String message = "Hello World!";

    public static String getMessage() {
        return message;
    }

    public static void setMessage(String message) {
        HelloMessageData.message = message;
    }

}

MessageService.java … 文字列を取得するだけのインターフェース

package hello;

public interface MessageService {
    String getMessage();
}

HelloMessageService.java … 上記インターフェースの実装

package hello;

public class HelloMessageService implements MessageService {

    private final String message;

    HelloMessageService() {
        this.message = HelloMessageData.getMessage();
    }

    public String getMessage() {
        return message;
    }

}

MessagePrinter.java … メッセージサービス(MessageService)を用いて取得した文字列を表示するクライアント

package hello;

public class MessagePrinter {

    private MessageService messageService;
    public void setMessageService(MessageService messageService) {
        this.messageService = messageService;
    }

    public String getMessage() {
        return messageService.getMessage();
    }

    public void pringMessage() {
        System.out.println(messageService.getMessage());
    }

}

applicationContext.xml … 忌まわしき、設定ファイル

<?xml version="1.0" encoding="UTF-8"?>
<beans
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">

    <bean id="messageService" class="hello.HelloMessageService" />

    <bean id="messagePrinter" class="hello.MessagePrinter">
        <property name="messageService" ref="messageService"/>
    </bean>

</beans>


とまあ、こんな感じ。MessagePrinter が、メッセージを表示するだけのクライアント。ここに MessageService という(文字列を渡すだけの)インスタンスをインジェクションし、文字列を取得し、表示する。文字列は、MessageService の実装がインスタンス化されるときにだけ、HelloMessageData クラスから取得し、インスタンス内に保持する。
動かし方は、単純なメインクラスを用いるなら、例えば次のようになる。


Main.java

package main;

import hello.MessagePrinter;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {

    public static void main(String[] str) {
        @SuppressWarnings("resource")
        ApplicationContext context
            = new ClassPathXmlApplicationContext("applicationContext.xml");
        MessagePrinter printer
            = (MessagePrinter) context.getBean("messagePrinter");
        printer.pringMessage();
    }

}


ここで、本題。MessagePrinter クラスのテストを行う。とりあえず、Spring Frameworkが提供するクラスを用いず、素のJUnitで作成してみた。


HelloMessagePrinterTest.java

package hello;

import static org.junit.Assert.*;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class HelloMessagePrinterTest {

    ApplicationContext context
        = new ClassPathXmlApplicationContext("applicationContext.xml");

    @Test
    public void test_getMessage1() {
        MessagePrinter printer = (MessagePrinter) context.getBean("messagePrinter");
        assertEquals("Hello World!", printer.getMessage());
    }

    /**
     * {@code ApplicationContext} がリフレッシュされていないので、"Hello World!"が
     * 取得される。
     */
    @Test
    public void test_getMessage2() {
        HelloMessageData.setMessage("Nice to meet you!");

        MessagePrinter printer = (MessagePrinter) context.getBean("messagePrinter");
        assertEquals("Hello World!", printer.getMessage());
    }

    /**
     * {@code ApplicationContext} がリフレッシュされていないので、"Hello World!"が
     * 取得されるはずなのだが、"Nice to meet you!"が取得される。
     */
    @Test
    public void test_getMessage3() {
        HelloMessageData.setMessage("What's your Name?");

        MessagePrinter printer = (MessagePrinter) context.getBean("messagePrinter");
        assertEquals("Hello World!", printer.getMessage());
    }

}


アプリケーションコンテキストは、(テストクラスのインスタンスが生成されるときに)1度だけ生成されるはず。つまり、MessageServiceインスタンスは、文字列「Hello World!」を保持し、その後変更されないはず。なので、常に「Hello World!」という文字列が取得され、上記テストは3つとも成功するはず。だったのだが、そうではなかった。
最初の2つのテストは成功するのだが、最後のテストは、次のメッセージを残して失敗する。

org.junit.ComparisonFailure: expected:<[Hello World]!> but was:<[Nice to meet you]!>


テストの実行順序に依存することはわかっているのだが(例えば、3つ目のテストを単独で流すと成功する)。なぜだ。。


2015/05/24追記

Spring Frameworkコンポーネントを使ったら、とりあえずテストが通った。謎は解けないままだが。
ちなみに、test-applicationContext.xml の中身は、上記 applicationContext.xml といっしょ。

package hello;

import static org.junit.Assert.*;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext.xml")
public class HelloMessagePrinterTest {

    @Autowired
    ApplicationContext context;

    @Test
    public void test_getMessage1() {
        MessagePrinter printer = (MessagePrinter) context.getBean("messagePrinter");
        assertEquals("Hello World!", printer.getMessage());
    }

    /**
     * {@code ApplicationContext} がリフレッシュされていないので、"Hello World!"が
     * 取得される。
     */
    @Test
    public void test_getMessage2() {
        HelloMessageData.setMessage("Nice to meet you!");

        MessagePrinter printer = (MessagePrinter) context.getBean("messagePrinter");
        assertEquals("Hello World!", printer.getMessage());
    }

    /**
     * {@code ApplicationContext} がリフレッシュされていないので、"Hello World!"が
     * 取得される。
     */
    @Test
    public void test_getMessage3() {
        HelloMessageData.setMessage("What's your Name?");

        MessagePrinter printer = (MessagePrinter) context.getBean("messagePrinter");
        assertEquals("Hello World!", printer.getMessage());
    }

}