読者です 読者をやめる 読者になる 読者になる

EasyMock#anyObject()で使ったオブジェクトを実行後に取り出して検証する

Java

つい最近3.1がリリースされたEasyMockだが、かつての記事でやりたいと思っていた、EasyMock#anyObject()で使われたオブジェクトを実行後に取り出す方法を考えた。

使用例は以下のような感じになった。

モデル。ageとnameという2つのフィールドを持つPOJO

public class Person {

    private final int age;

    private final String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return age + " : " + name;
    }
}

インタフェース。テストではMockを使う。

public interface PersonDAO {

	public void insert(Person person);
}

使用例。

public class Sample {

	public static void main(String[] args) throws Exception {
		Sample sample = new Sample();
		sample.run();
	}

	private void run() throws Exception {
		IMocksControl control = createControl();
		PersonDAO personDAO = control.createMock(PersonDAO.class);

		// record
		RecordableAnyObject<Person> anyPerson = new RecordableAnyObject<Person>();
		personDAO.insert(anyPerson.create());
		personDAO.insert(anyPerson.create());

		control.replay();

		doTestMethod(personDAO);

		control.verify();

		Person person1 = anyPerson.getResult();
		System.out.println(person1); // Ichiro(20)

		Person person2 = anyPerson.getResult();
		System.out.println(person2); // Jiro(18)

		Person person3 = anyPerson.getResult();
		System.out.println(person3); // null
	}

	public void doTestMethod(PersonDAO personDAO) {
		Person fuga = new Person(20, "Ichiro");
		personDAO.insert(fuga);

		Person fuga2 = new Person(18, "Jiro");
		personDAO.insert(fuga2);
	}
}

ここでのdoTestMethod()が今回テストしたいメソッド。
RecordableAnyObjectってのが新しく作ったクラス。

PersonDAOはMockなので、振る舞いを記録する際にPersonDAO#insert(Person)に与えるPersonを教えてあげないとならない。
ところがPersonはdoTestMethod()の中でインスタンスを作っているため、通常であればEasyMock#anyObject(Person.class)等の方法で回避するところだけど、この場合、後からPersonの中身を確認したい、ってことが(多分)できないので、時と場合によっては問題になる。

新しく作ったRecordableAnyObjectはそんな悩みを解決するクラス。RecordableAnyObject#create()でanyObject()相当の機能を提供し、RecordableAnyObject#getResult()によって後から使われたオブジェクトを取り出すことができる。

で、そのクラスはこちら。

public class RecordableAnyObject<T> {

    private RecordableArgumentMatcher matcher = new RecordableArgumentMatcher();

    public T create() {
        EasyMock.reportMatcher(matcher);
        return null;
    }

    @SuppressWarnings("unchecked")
    public T getResult() {
        Object obj = matcher.actualList.poll();
        if (obj != null) {
            return (T) obj;
        } else {
            return null;
        }
    }

    private static class RecordableArgumentMatcher implements IArgumentMatcher {

        private Queue<Object> actualList = new LinkedList<Object>();

        private Object last;

        @Override
        public boolean matches(Object actual) {
            if (actual != null) {
                last = actual;
                actualList.add(actual);
            }
            return true;
        }

        @Override
        public void appendTo(StringBuffer buffer) {
            if (last != null) {
                buffer.append(last.toString());
            }
        }
    }
}

使用例を見れば分かるように、内部ではQueueを使って順番を管理しているので、getResult()を呼び出すたびに使われたオブジェクトを順番に取得することができる。

ここまでできると、doTestMethodのテストとして、PersonDAOの実装クラスがなくても、PersonDAO#insert(Person)がきちんと呼ばれていること(順番や実際に登録されるPersonオブジェクトの内容も含めて)をユニットテストで確認ができる。

記録する(状態をもつ)必要がある都合上、anyObject()相当の機能が静的メソッドじゃなくなったのはしょうがない。