事象
認証に関わるサービスクラス(ドメイン層)のテストコードを実行した時、期待値(user)とメソッドが返した値(actual)が異なるのにも関わらず、isEqualTo()がtrueになってしまった。
user = com.example.webapp.test_data.login.User [/*(中略)*/Granted Authorities=[USER]]
actual = com.example.webapp.entity.LoginUser [/*(中略)*/Granted Authorities=[ADMIN, USER]]
@Test
void test_loadUserByUsername_ADMIN() {
//(中略)
assertThat(actual).isEqualTo(user); //true
}
userもactualも共にUserクラス(org.springframework.security.core.userdetails.User)(を継承して作成した独自のクラス)のインスタンスであり、username, password, authoritiesをインスタンス変数として持つ
public User(StringSE username,
StringSE password,
CollectionSE<? extends GrantedAuthority> authorities)
原因
isEqualTo()は比較にUserクラスのequalsメソッド(java.util.Objectsのオーバーライド)を用いており、equals()が比較するのはインスタンス同士ではなくusernameだけである。よって、authoritiesが違ってもtrueになってしまい、結果的にUser.isEqualTo()もtrueになってしまう。
以下は、ドキュメント(spring.pleiades.io)から引用したUser.equals()の説明
指定されたオブジェクトが同じ username 値を持つ User インスタンスである場合、true を返します。
以下はその実装
/**
* Returns {@code true} if the supplied object is a {@code User} instance with the
* same {@code username} value.
* <p>
* In other words, the objects are equal if they have the same username, representing
* the same principal.
*/
@Override
public boolean equals(Object obj) {
if (obj instanceof User user) {
return this.username.equals(user.getUsername());
}
return false;
}
デバッガーで変数を追ってみても、確かにusername同士を比較していることがわかる。
対応
authorities同士で比較すると、ちゃんとfalseになる。
@Test
void test_loadUserByUsername_ADMIN() {
//(中略)
//assertThat(actual).isEqualTo(user); true
assertThat(actual.getAuthorities()).isEqualTo(user.getAuthorities()); //false
}
以下は興味のある方のみ
isEqualTo()のメソッド呼び出しを辿ってみる
AbstractAssert.isEqualTo()(org.assertj.core.apiパッケージ)
/** {@inheritDoc} */
@Override
public SELF isEqualTo(Object expected) {
//(中略)
objects.assertEqual(info, actual, expected);
return myself;
}
Objects.assertEqual()(org.assertj.core.internalパッケージ)
public void assertEqual(AssertionInfo info, Object actual, Object expected) {
if (!areEqual(actual, expected))
throw failures.failure(info, shouldBeEqual(actual, expected, comparisonStrategy, info.representation()));
}
Objects.areEqual()(org.assertj.core.internalパッケージ)
comparisonStrategyというのは、Objectsクラスがフィールドとして持つStandardComparisonStratedy(ComparisonStrategyインターフェースの実装)のインスタンス
private boolean areEqual(Object actual, Object other) {
return comparisonStrategy.areEqual(actual, other);
}
ComparisonStrategy.areEqual()(org.assertj.core.internalパッケージ)
抽象メソッド
public interface ComparisonStrategy {
/**
* Returns true if actual and other are equal according to the implemented comparison strategy.
*
* @param actual the object to compare to other
* @param other the object to compare to actual
* @return true if actual and other are equal according to the underlying comparison strategy.
*/
boolean areEqual(Object actual, Object other);
StandardComparisonStratedy.areEqual()(org.assertj.core.internalパッケージ)
上にも書いたが、ComparisonStrategyインターフェースの実装
/**
* Returns {@code true} if the arguments are deeply equal to each other, {@code false} otherwise.
* <p>
* It mimics the behavior of {@link java.util.Objects#deepEquals(Object, Object)}, but without performing a reference
* check between the two arguments. According to {@code deepEquals} javadoc, the reference check should be delegated
* to the {@link Object#equals equals} method of the first argument, but this is not happening. Bug JDK-8196069 also
* mentions this gap.
*
* @param actual the object to compare to {@code other}
* @param other the object to compare to {@code actual}
* @return {@code true} if the arguments are deeply equal to each other, {@code false} otherwise
*
* @see <a href="https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8196069">JDK-8196069</a>
*
*/
@Override
public boolean areEqual(Object actual, Object other) {
//(中略。かなり長い分岐処理。actualとotherが配列である場合や、基本型である場合の比較をしている。)
return actual.equals(other);
}