【问题标题】:JPA Entity must be unit tested and how?JPA 实体必须进行单元测试,如何进行?
【发布时间】:2015-09-06 20:22:50
【问题描述】:

我有一个 Spring MVC 应用程序 (Spring Boot v. 1.2.5),它使用 JPA 与流行的 Sql 数据库进行交互。
因此,我有几个实体映射数据库中的所有表。显然,这些类只有 getter/setter 和实体之间关系的注释。

例如:

@Entity
@Table
public class Article {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @JsonView(View.Private.class)
    private Long id;

    @Column(nullable = false)
    @JsonView(View.Public.class)
    private String name;

    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    @JoinColumn(name = "categoryId", nullable = false, updatable = false)
    @JsonIgnore
    private Category category;

   //Constructors Getters and Setters
   ...
}

我的问题是:我应该对这些类进行单元测试吗?我应该测试什么?如何

【问题讨论】:

  • 我建议使用 Groovy 或 Lombok 为您生成所有这些方法。
  • 我已经在使用 Lombok
  • 那么不,相信注释库(JPA 和 Bean Validation)会完成他们的工作。
  • @mat_boy 添加了一种解决方案,它有助于增加 JPA 实体的代码覆盖率和单元测试

标签: java spring hibernate unit-testing jpa


【解决方案1】:

我建议你测试你写的所有东西(或者你选择写的)......所以在这种情况下,我看到了以下元素:

@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false)
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "categoryId", nullable = false, updatable = false)

你用这个注解定义了一些行为(我只从 JPA 中挑选那些,同样的事情应该用 JSONView 注解来完成),如果一切正常(就像你定义的那样),你想进行单元测试。

@Test(expect = SQLException.class)
public void should_not_allow_null_name() {
    /* Given */ Article article = new Article(null, new Category());
    /* When */  articleRepository.save(article);
}

通过这种(简单)测试,如果行为尊重您已实施的内容,您可以遵循。如果有人(可能是您自己)删除了此注释,您将收到警报。

但不要测试默认行为(例如,列名,您选择让 JPA(和 ORM)为您选择......所以不要测试框架,这是限制。

关于如何测试它,我喜欢使用(从 Spring Boot 开始)名为 DBSetup 的项目。它允许我在我的测试中硬编码我的数据集,而不是冗长的 XML。这是忍者小队的一个非常有趣的项目。

测试示例:

数据库测试配置:

@Configuration
@EnableJpaRepositories(basePackages = "lan.dk.podcastserver.repository")
@EntityScan(basePackages = "lan.dk.podcastserver.entity")
public class DatabaseConfiguraitonTest {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .build();
    }

    @Bean
    @Autowired
    public FullTextEntityManager fullTextEntityManager(EntityManager entityManager) {
        return getFullTextEntityManager(entityManager);
    }

    public static final DateTimeFormatter formatter = new DateTimeFormatterBuilder().append(DateTimeFormatter.ISO_LOCAL_DATE).appendLiteral(" ").append(DateTimeFormatter.ISO_LOCAL_TIME).toFormatter();
    public static final Operation DELETE_ALL_PODCASTS = deleteAllFrom("PODCAST");
    public static final Operation DELETE_ALL_ITEMS = deleteAllFrom("ITEM");
    public static final Operation DELETE_ALL_TAGS = sequenceOf(deleteAllFrom("PODCAST_TAG"), deleteAllFrom("TAG"));
    public static final Operation DELETE_ALL = sequenceOf(DELETE_ALL_ITEMS, DELETE_ALL_TAGS, DELETE_ALL_PODCASTS, DELETE_ALL_TAGS);
}

==> https://gist.github.com/davinkevin/bb4f62aaec031b68b8f3

然后测试:

@Transactional
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {DatabaseConfiguraitonTest.class, HibernateJpaAutoConfiguration.class})
public class ItemRepositoryTest {

    @Autowired DataSource dataSource;
    @Autowired ItemRepository itemRepository;

    private final static DbSetupTracker dbSetupTracker = new DbSetupTracker();
    public static final Operation INSERT_REFERENCE_DATA = sequenceOf(
            insertInto("PODCAST")
                    .columns("ID", "TITLE", "URL", "TYPE", "HAS_TO_BE_DELETED")
                    .values(1, "AppLoad", null, "RSS", false)
                    .values(2, "Geek Inc HD", "http://fake.url.com/rss", "YOUTUBE", true)
                    .build(),
            insertInto("ITEM")
                    .columns("ID", "TITLE", "URL", "PODCAST_ID", "STATUS", "PUBDATE", "DOWNLOADDDATE")
                    .values(1L, "Appload 1", "http://fakeurl.com/appload.1.mp3", 1, Status.FINISH, now().minusDays(15).format(formatter), now().minusDays(15).format(formatter))
                    .values(2L, "Appload 2", "http://fakeurl.com/appload.2.mp3", 1, null, now().minusDays(30).format(formatter), null)
                    .values(3L, "Appload 3", "http://fakeurl.com/appload.3.mp3", 1, Status.NOT_DOWNLOADED, now().format(formatter), null)
                    .values(4L, "Geek INC 123", "http://fakeurl.com/geekinc.123.mp3", 2, Status.DELETED, now().minusYears(1).format(formatter), now().format(formatter))
                    .values(5L, "Geek INC 124", "http://fakeurl.com/geekinc.124.mp3", 2, Status.FINISH, now().minusDays(15).format(formatter), now().minusDays(15).format(formatter))
                    .build(),
            insertInto("TAG")
                    .columns("ID", "NAME")
                    .values(1L, "French Spin")
                    .values(2L, "Studio Knowhere")
                    .build(),
            insertInto("PODCAST_TAG")
                    .columns("PODCAST_ID", "TAG_ID")
                    .values(1, 1)
                    .values(2, 2)
                    .build()
    );

    @Before
    public void prepare() throws Exception {
        Operation operation = sequenceOf(DELETE_ALL, INSERT_REFERENCE_DATA);
        DbSetup dbSetup = new DbSetup(new DataSourceDestination(dataSource), operation);

        dbSetupTracker.launchIfNecessary(dbSetup);
    }

    @Test
    public void should_find_by_podcast_and_page() {
        /* Given */
        dbSetupTracker.skipNextLaunch();
        Integer podcastId = 1;
        PageRequest pageRequest = new PageRequest(1, 1, Sort.Direction.ASC, "id");

        /* When */
        Page<Item> itemByPodcast = itemRepository.findByPodcast(podcastId, pageRequest);

        /* Then */
        PageAssert
                .assertThat(itemByPodcast)
                .hasSize(1)
                .hasTotalElements(3)
                .hasTotalPages(3)
                .hasNumberOfElements(1);

        ItemAssert
                .assertThat(itemByPodcast.getContent().get(0))
                .hasTitle("Appload 2");
    }

==> https://gist.github.com/davinkevin/df041729608dc21bf7f3

【讨论】:

  • 您的推理似乎令人信服(虽然使用注释节省的时间似乎将用于进行单元测试):+1。我会等几个小时,如果没有更令人信服的答案,我会接受你的回答
  • @Kevin Davin 真的是单元测试吗?这是集成测试,因为我们在这里创建了整个 spring 上下文。
  • 自从有了这条评论,您现在可以在 Spring 中访问 Slice 以仅加载应用程序的一部分。在这种情况下,我只想加载 JPA 切片,这不会加载全局应用程序?。
【解决方案2】:

您应该测试功能而不是类。如果您不确定映射是否正常工作,那么测试此类对象的保存/加载可能适合您。但是,单元测试也应该隔离持久层,这样你就可以测试你的业务逻辑而不是持久层。

【讨论】:

  • 说得对。虽然您始终可以使用集成测试来测试您的持久层。几乎不需要检查您的实体类以进行单元测试,因为它不会完成太多,因为这一层没有业务逻辑。
【解决方案3】:

在域实体上,很明显您应该测试每个编码行为。测试域实体行为与单元测试任何类型的函数/方法没有什么不同。通常,您为每个聚合根声明一个测试套件,并检查不变量是否正确应用以及突变是否以预期的方式发生。在这个级别,你完全不知道持久性

在 99.9% 的情况下,域实体也是持久实体,这意味着它们被存储到某些持久数据存储中/从其中重新水化,并且您将使用一些框架来解决 ORM/ODM 问题,例如 JPA+Spring Data+休眠。通常,这些框架实现 API,用元数据装饰您的域元素,这些元数据告诉它们必须如何表示到数据存储中 (is it a good practice?)。由于整个应用程序的行为取决于此声明,因此在许多情况下值得进行测试

很明显,如果您的底层没有 DB,那么测试实体在 DB 中的表示方式是没有意义的,因此这些测试属于集成测试。我们倾向于将集成测试视为测试整个应用程序堆栈的东西,但为什么我们不能进行域级集成测试呢?

假设我有

public interface UserRepository extends CrudRepository<User, String> {

    @Query("select * from users where birthdate=?1")
    public List<User> findUsersByBirthdate(Date birthdate);
} 

如果我只是使用已经从 Spring Data 实现的 userRepository.findById(),我可以依赖 3rd 方代码并且不怀疑它的正确性,但现在我要引入一个值得的自定义行为测试

@Test
public void given_user_when_find_by_different_birthdate_should_return_empty_list () {
    repository.save(new User(BirthdatesFixture.1ST_JANUARY));
    assertThat(repository.findUsersByBirthdate(BirthdatesFixture.1ST_FEUBRARY)).isEmpty()
}

一般来说,我喜欢为每个存储库创建一个集成测试用例,以检查我的关键自定义声明是否正确。在其他一些情况下(尤其是映射注释),依赖绿色的“e2e 集成测试”就足够了。我们应该永远记住,只有在没有其他测试可以断言关于新添加的代码的某些内容时,我们才必须添加测试(TDD 对此有所帮助)

【讨论】:

    【解决方案4】:

    具有代码覆盖率的 JPA 实体的 JUnit 测试

      public class ArticleTest {
    
        public Article crateTestSuite(){
          return new Article ();
         }
    
        @Test
        public void testGetId() {
         Long id= 0;
         Xyz xyz =null;
         xyz = crateTestSuite();
         id = xyz.getId()
    
        }
    
        @Test
        public void setId(Integer id) {
         Long id= 0;
         Xyz xyz =null;
         xyz = crateTestSuite();
         xyz.setId(id)
        } 
    }
    

    【讨论】:

    • 上面的代码完成了什么测试?即使您使用断言语句 .. 这就像检查 setter 和 getter 只是为了提高代码覆盖率。
    猜你喜欢
    • 1970-01-01
    • 2020-02-05
    • 2012-08-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多