应该如何设计执行 CRUD 操作的 bean 的单元测试?

Posted

技术标签:

【中文标题】应该如何设计执行 CRUD 操作的 bean 的单元测试?【英文标题】:How should unit tests for beans that perform CRUD operations be designed? 【发布时间】:2014-10-26 13:41:47 【问题描述】:

我正在开发一个基于 Java EE 7 的应用程序,它使用 EJB、CDI 和 JPA 的组合对 SQL 数据库执行创建、读取、更新和删除操作。我想为我的应用程序的服务层开发一系列单元测试,但我正在努力了解如何创建任何有意义的单元测试用例来增加价值,而不仅仅是为了代码覆盖而进行的单元测试。我发现的大多数示例实际上都是使用内存数据库的集成测试。

应用程序的服务层是使用实体、控制和边界模式设计的。

Entity 是一个 JPA 注释 bean,包含各种 getter、setter 和命名查询,以及标准的 toString、equals 和 hashCode 方法。

Control 是一个使用 @Dependent 注释的 CDI 托管 bean,它包含调用 JPA 实体管理器持久化、合并和删除方法的创建、更新、删除 void 方法。该控件还包含一些读取方法,这些方法使用 JPA 命名查询或 JPA 条件 API 从数据库中返回 List 对象。 create、update 和 delete 方法执行一些基本检查,例如检查记录是否已经存在,但这也是通过相关的 JPA EntityManager 方法完成的。

边界是一个使用@Stateless 注释的 EJB 托管 bean,包含最终用户可识别的方法,例如 createWidget、deleteWidget、updateWidget、activateWidget、discontinuedWidget、findAllWidgets 和 findASpecificWidget。对于更复杂的实体,边界将应用业务逻辑,但许多实体非常简单,不包含任何业务逻辑。 createWidget、deleteWidget、updateWidget、activateWidget、discontinuWidget 方法被声明为 void 并使用异常来处理失败,例如数据库约束违规,然后将其传递到应用程序的 Web 层以向用户返回用户友好的消息.

我知道在编写单元测试时,我应该使用模拟框架来模拟 EntityManager 之类的东西来单独测试方法,并且当方法被声明为 void 时,测试用例应该检查状态是否已正确更改.问题是我很难看到大多数单元测试除了检查模拟框架是否正常工作而不是我的应用程序代码之外,还能做些什么。

我的问题是我应该如何设计有意义的单元测试来验证边界和控制组件的正确操作,因为控制组件只是调用各种 JPA EntityManager 方法并且边界组件在某些情况下不应用业务逻辑?或者在这种情况下没有任何好处,相反我应该专注于编写集成测试。

更新

以下是用于维护小部件列表的服务组件示例:

public class WidgetService 

    @PersistenceContext
    public EntityManager em;

    public void createWidget(Widget widget) 

        if (checkIfWidgetDiscontinued(widget.getWidgetCode())) 
            throw new ItemDiscontinuedException(String.format(
                    "Widget %s already exists and has been discontinued.",
                    widget.getWidgetCode()));
        

        if (checkIfWidgetExists(widget.getWidgetCode())) 
            throw new ItemExistsException(String.format("Widget %s already exists",
                    widget.getWidgetCode()));
        

        em.persist(widget);
        em.flush();
    

    public void updateWidget(Widget widget) 
        em.merge(widget);
        em.flush();
    

    public void deleteWidget(Widget widget) 
        try 
            Object ref = em.getReference(Widget.class, widget.getWidgetCode());
            em.remove(ref);
            em.flush();
         catch (PersistenceException ex) 
            Throwable rootCause = ExceptionUtils.getRootCause(ex);
            if (rootCause instanceof SQLIntegrityConstraintViolationException) 
                throw new DatabaseConstraintViolationException(rootCause);
             else 
                throw ex;
            
        
    

    public List<Widget> findWithNamedQuery(String namedQueryName,
            Map<String, Object> parameters, int resultLimit) 
        Set<Map.Entry<String, Object>> rawParameters = parameters.entrySet();
        Query query = this.em.createNamedQuery(namedQueryName);
        if (resultLimit > 0) 
            query.setMaxResults(resultLimit);
        
        for (Map.Entry<String, Object> entry : rawParameters) 
            query.setParameter(entry.getKey(), entry.getValue());
        
        return query.getResultList();
    

    public List<Widget> findWithComplexQuery(int first, int pageSize, String sortField,
            SortOrder sortOrder, Map<String, Object> filters) 

        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Widget> q = cb.createQuery(Widget.class);
        Root<Widget> referenceWidget = q.from(Widget.class);
        q.select(referenceWidget);

        //Code to apply sorting and build filterCondition removed for brevity

        q.where(filterCondition);

        TypedQuery<Widget> tq = em.createQuery(q);
        if (pageSize >= 0) 
            tq.setMaxResults(pageSize);
        
        if (first >= 0) 
            tq.setFirstResult(first);
        

        return tq.getResultList();
    

    public long countWithComplexQuery(Map<String, Object> filters) 

        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Long> q = cb.createQuery(Long.class);
        Root<Widget> referenceWidget = q.from(Widget.class);
        q.select(cb.count(referenceWidget));

        //Code to build filterCondition removed for brevity

        q.where(filterCondition);

        TypedQuery<Long> tq = em.createQuery(q);

        return tq.getSingleResult();
    


    private boolean checkIfWidgetExists(String widgetCode) 
        int count;
        Query query = em.createNamedQuery(Widget.COUNT_BY_WIDGET_CODE);
        query.setParameter("widgetCode", widgetCode);
        count = ((Number) query.getSingleResult()).intValue();

        if (count == 1) 
            return true;
         else 
            return false;
        
    

    private boolean checkIfWidgetDiscontinued(String widgetCode) 
        int count;
        Query query = em
                .createNamedQuery(Widget.COUNT_BY_WIDGET_CODE_AND_DISCONTINUED);
        query.setParameter("widgetCode", widgetCode);
        query.setParameter("discontinued", true);
        count = ((Number) query.getSingleResult()).intValue();

        if (count == 1) 
            return true;
         else 
            return false;
        
    
 

以下是用于维护小部件列表的边界组件示例:

@Stateless
public class WidgetBoundary 

    @Inject
    private WidgetService widgetService;

    public void createWidget(Widget widget) 
        widgetService.createWidget(widget);
    

    public void updateWidget(Widget widget) 
        widgetService.updateWidget(widget);
    

    public void deleteWidget(Widget widget) 
        widgetService.deleteWidget(widget);
    

    public void activateWidget(String widgetCode) 
        Widget widget;

        widget = widgetService.findWithNamedQuery(Widget.FIND_BY_WIDGET_CODE,
                QueryParameter.with("widgetCode", widgetCode).parameters(), 0).get(0);

        widget.setDiscontinued(false);
        widgetService.updateWidget(widget);
    

    public void discontinueWidget(Widget widget) 
        widget.setDiscontinued(true);
        widgetService.updateWidget(widget);
    

    public List<Widget> findWithComplexQuery(int first, int pageSize, String sortField,
            SortOrder sortOrder, Map<String, Object> filters) 
        return widgetService.findWithComplexQuery(first, pageSize, sortField, sortOrder,
                filters);
    

    public Long countWithComplexQuery(Map<String, Object> filters) 
        return widgetService.countWithComplexQuery(filters);
    

    public List<Widget> findAvailableWidgets() 
        return widgetService.findWithNamedQuery(Widget.FIND_BY_DISCONTINUED, QueryParameter.with("discontinued", false).parameters(), 0);
    


【问题讨论】:

TL;博士。不要发布您的代码的描述。贴出代码。 当您指出测试简单的 CRUD bean 并没有做很多事情时,我认为您一针见血。尽管如此,拥有它们并没有什么坏处。您更重要的测试将围绕您的外观,它将结合业务逻辑和(可能)crud 操作。使用好的外观(参见设计模式)可以产生好的代码和有意义的测试。我的 2 美分值。 @JBNizet 这个问题与我已经得到的代码没有直接关系,而是与为 CRUD bean 编写单元测试的概念有关。正如您在评论中所说的那样,问题已经很长了,我不想通过添加代码来使其超出必要的时间,而是希望描述就足够了。尽管如此,我已经更新了问题以包含两个示例类 【参考方案1】:

您的代码很难测试,因为职责没有正确分离。

WidgetBoundary 几乎不做任何事情,并将所有事情委托给 WidgetService。

WidgetService 混合了业务逻辑(例如在创建小部件之前检查它是否已停用)和持久性逻辑(例如保存或查询小部件)。

这使得 WidgetBoundary 完全愚蠢,不值得测试,而 WidgetService 太复杂而无法轻松测试。

业务逻辑应该移到边界(我称之为服务)。服务(应该称为 DAO)应该只包含持久性逻辑。

这样,您可以测试 DAO 执行的查询是否正常工作(通过使用测试数据填充您的数据库、调用查询方法并查看它是否返回正确的数据)。

您还可以通过模拟 DAO 轻松快速地测试业务逻辑。这样,您就不需要任何数据库来测试业务逻辑。例如,createWidget() 方法的测试可能如下所示:

@Test(expected = ItemDiscontinuedException)
public void createWidgetShouldRejectDiscontinuedWidget() 
    WidgetDao mockDao = mock(WidgetDao.class);
    WidgetService service = new WidgetService(mockDao);
    when(mockDao.countDiscontinued("someCode").thenReturn(1);

    Widget widget = new Widget("someCode");
    service.createWidget(widget);                

【讨论】:

谢谢,现在我明白为什么应该包含代码了 :) 您是否看到使用模拟框架对 DAO 进行单元测试有什么好处,还是最好留给使用真实数据库进行集成测试?跨度> 不,我认为使用模拟进行 DAO 测试没有任何价值。

以上是关于应该如何设计执行 CRUD 操作的 bean 的单元测试?的主要内容,如果未能解决你的问题,请参考以下文章

API 设计:移除未使用的 CRUD 服务

我想用gin开发一个使用redis和数据库的系统。 我应该如何进行架构设计?

Spring框架中的单例bean是线程安全的吗?

MyBatis学习总结——使用MyBatis对表执行CRUD操作

如何在 Blazor WASM 中对当前经过身份验证的用户帐户信息执行 CRUD 操作?

MyBatis学习总结——使用MyBatis对表执行CRUD操作