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

时间:2014-10-26 13:41:47

标签: java unit-testing java-ee mocking

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

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

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

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

边界是使用@Stateless注释的EJB托管bean,并包含最终用户可识别的方法,如createWidget,deleteWidget,updateWidget,activateWidget,discontinueWidget,findAllWidgets和findASpecificWidget。对于更复杂的实体,边界将应用业务逻辑,但是许多实体非常简单并且不包含任何业务逻辑。 createWidget,deleteWidget,updateWidget,activateWidget,discontinueWidget方法被声明为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);
    }

}

1 个答案:

答案 0 :(得分: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);                
}
相关问题