
在使用Spring Boot的@DataJpaTest进行数据层测试时,开发者可能会遇到一个令人困惑的现象:单个测试方法独立运行时能够顺利通过,但当整个测试类或所有测试一起运行时,部分测试却失败,并抛出org.opentest4j.AssertionFailedError。错误信息通常指示预期的List类型与实际返回的List类型不匹配,例如:
expected: "[com.example.recipesapi.model.Recipe@45f421c] (List12@14983265)" but was: "[com.example.recipesapi.model.Recipe@45f421c] (ArrayList@361483eb)"
这个错误表明,尽管列表中的元素内容可能相同,但它们所属的List实现类不同(例如,java.util.List.of()返回的可能是内部的不可变列表类型,而JPA查询结果通常是java.util.ArrayList或其他可变列表)。assertThat(...).isEqualTo(...)在比较集合时,不仅会比较元素内容,还会比较集合的运行时类型。当类型不完全匹配时,即使内容相同,断言也会失败。
此外,JPA实体(如Recipe类)的equals()和hashCode()方法实现也可能影响测试的稳定性。在提供的Recipe实体中,equals()方法仅基于id字段进行比较。在测试中,我们手动创建Recipe对象并赋予ID,然后将其保存到数据库。然而,如果数据库配置了ID自增策略(GenerationType.IDENTITY),数据库在保存时可能会重新分配ID。如果测试中使用的recipe1对象与从数据库中检索出的Recipe对象ID不一致,即使它们代表同一逻辑实体,isEqualTo断言也会因为equals()方法返回false而失败。
为了避免因List实现类型不同导致的断言失败,推荐使用更专注于元素内容比较的断言方法,例如AssertJ库提供的containsExactly或containsExactlyInAnyOrder。
针对上述问题,如果元素的顺序是重要的,可以使用containsExactly。
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
class RecipeRepositoryTest {
@Autowired
private RecipeRepository recipeRepositoryUnderTest;
@BeforeEach
void tearDown() {
// 确保每个测试前清空数据库,保证测试隔离性
recipeRepositoryUnderTest.deleteAll();
}
@Test
void shouldFindSingleRecipeByName() {
// given
String searchName = "Tomato soup";
// 创建Recipe对象,注意:手动设置的ID在saveAll后可能被数据库覆盖
Recipe recipe1 = new Recipe(
null, // 最好将ID设为null,让数据库自动生成
"Tomato soup",
"Delicious tomato soup",
Arrays.asList("1. ", "2. "),
Arrays.asList("1. ", "2. ")
);
Recipe recipe2 = new Recipe(
null, // 最好将ID设为null,让数据库自动生成
"Mushrooms soup",
"Delicious mushrooms soup",
Arrays.asList("1. ", "2. "),
Arrays.asList("1. ", "2. ")
);
// 保存到数据库,此时recipe1和recipe2的ID会被数据库更新
recipeRepositoryUnderTest.save(recipe1); // 单独保存以获取更新后的ID
recipeRepositoryUnderTest.save(recipe2);
// when
List<Recipe> recipesList = recipeRepositoryUnderTest.findRecipeByName(searchName.toLowerCase());
// then
// 使用 containsExactly 比较列表元素,忽略列表的具体实现类型
// 这里需要确保recipe1对象是数据库中实际存在的那个,或者比较其业务属性
assertThat(recipesList).containsExactly(recipe1);
// 更好的做法是比较对象的业务属性,或者重新从数据库加载以确保是同一个对象实例
// 例如:assertThat(recipesList).hasSize(1).extracting(Recipe::getName).containsExactly("Tomato soup");
}
// 其他测试方法保持不变,或同样应用containsExactly
@Test
void shouldFindTwoRecipesByName() {
String searchName = "oup";
Recipe recipe1 = new Recipe(null, "Tomato soup", "Delicious tomato soup", Arrays.asList("1. ", "2. "), Arrays.asList("1. ", "2. "));
Recipe recipe2 = new Recipe(null, "Mushrooms soup", "Delicious mushrooms soup", Arrays.asList("1. ", "2. "), Arrays.asList("1. ", "2. "));
recipeRepositoryUnderTest.save(recipe1);
recipeRepositoryUnderTest.save(recipe2);
List<Recipe> recipesList = recipeRepositoryUnderTest.findRecipeByName(searchName.toLowerCase());
// 使用 containsExactlyInAnyOrder,因为查询结果的顺序可能不确定
assertThat(recipesList).containsExactlyInAnyOrder(recipe1, recipe2);
}
@Test
void findByNameShouldReturnEmptyListOfRecipes() {
String searchName = "Tomato soup";
List<Recipe> recipesList = recipeRepositoryUnderTest.findRecipeByName(searchName.toLowerCase());
assertThat(recipesList).isEmpty(); // 或者 assertThat(recipesList).containsExactly();
}
}注意事项:
如果坚持使用isEqualTo进行断言,可以通过将预期列表强制转换为与实际列表相同的类型来解决问题。通常,ArrayList是一个通用的选择,因为JPA查询结果往往是ArrayList。
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.util.ArrayList; // 引入 ArrayList
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
class RecipeRepositoryTest {
@Autowired
private RecipeRepository recipeRepositoryUnderTest;
@BeforeEach
void tearDown() {
recipeRepositoryUnderTest.deleteAll();
}
@Test
void shouldFindSingleRecipeByName() {
// given
String searchName = "Tomato soup";
Recipe recipe1 = new Recipe(
null, // 建议设为null,让数据库生成ID
"Tomato soup",
"Delicious tomato soup",
Arrays.asList("1. ", "2. "),
Arrays.asList("1. ", "2. ")
);
Recipe recipe2 = new Recipe(
null, // 建议设为null,让数据库生成ID
"Mushrooms soup",
"Delicious mushrooms soup",
Arrays.asList("1. ", "2. "),
Arrays.asList("1. ", "2. ")
);
recipeRepositoryUnderTest.save(recipe1); // 单独保存以获取更新后的ID
recipeRepositoryUnderTest.save(recipe2);
// when
List<Recipe> recipesList = recipeRepositoryUnderTest.findRecipeByName(searchName.toLowerCase());
// then
// 将预期列表包装成 ArrayList,使其类型与实际结果类型一致
assertThat(recipesList).isEqualTo(new ArrayList<>(List.of(recipe1)));
}
// 其他测试方法类似处理
}注意事项:
当实体使用GenerationType.IDENTITY(或其他依赖数据库生成ID的策略)时,equals()方法仅依赖于id字段可能会导致问题。在实体尚未持久化时,id为null;持久化后,id才会被赋值。这意味着同一个逻辑实体在持久化前后,其equals()方法的结果可能不同。
为了在JPA实体中实现健壮的equals()和hashCode(),通常建议:
对于测试而言,如果equals()方法存在歧义或依赖于JPA生命周期,最好的做法是:
在Spring Data JPA测试中,处理List断言失败时,首选方案是使用AssertJ提供的containsExactly或containsExactlyInAnyOrder方法,它们专注于元素内容的比较,从而避免了因List实现类型差异导致的断言失败。其次,可以通过将预期列表包装成ArrayList来确保类型一致性,但这仍然依赖于实体equals()方法的正确性。
此外,理解JPA实体equals()和hashCode()的实现对于编写稳定可靠的测试至关重要,特别是当使用数据库自增ID时。在测试中,优先比较实体的业务属性或确保比较的是从数据库中加载的最新实体状态,能够有效提高测试的健壮性。遵循这些最佳实践,可以帮助开发者构建更可靠、更易维护的Spring Data JPA测试套件。
以上就是解决Spring Data JPA测试中List断言失败的常见问题的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号