觉得测试 Java EE 应用程序太困难、不方便或者太复杂?通过阅读本文,您将了解现实情况并非如此,同时还将了解如何高效进行单元测试。
测试是 Java Platform, Enterprise Edition (Java EE) 仍存的神秘领域之一。人们常常错误地认为 Java EE 应用程序的测试比较困难、不方便或者太复杂。从五年多前 Java EE 5 发布以来,实际情况并非人们所认为的那样。在本文中,我将探究单元测试。在后续文章中,我将探讨集成测试和嵌入式容器测试。
注:您可以在这里找到用于本文的 Maven 3 项目 TestingEJBAndCDI,已使用 NetBeans 7 和 GlassFish v3.x 对其进行过测试。
向 Oracle 询问 Java 的未来
我将使用一个“oracle”应用程序,借助后台的一些顾问,它能够预测 Java 的未来。需要澄清一下,“oracle”在此处另有他义:
“在古代,oracle 是在神的启示下,能够提出明智的忠告或具有先知先觉能力,可以预言或预知未来的人或机构。因此,这是一种占卜。”
在现代,即便一个 oracle 也需要依靠表示状态传输 (REST) 来公开他的预言(参见清单 1)。OracleResource 是一个 Enterprise JavaBeans (EJB) 3.1 bean,通过用于 RESTful Web 服务的 Java API (JAX-RS) 作为上下文和依赖注入 (CDI) 托管 bean 的 REST 资源和网关来提供。OracleResource 维护一个注入 consultant 池 (Instance<Consultant> company),并请求第一个 consultant 预测 Java 的未来。
03 |
public class OracleResource {
|
06 |
Instance<Consultant> company;
|
08 |
Event<Result> eventListener;
|
11 |
@Produces (MediaType.TEXT_PLAIN)
|
12 |
public String predictFutureOfJava() {
|
13 |
checkConsultantAvailability();
|
14 |
Consultant consultant = getConsultant();
|
15 |
Result prediction = consultant.predictFutureOfJava();
|
16 |
eventListener.fire(prediction);
|
18 |
if (JAVA_IS_DEAD.equals(prediction)) {
|
19 |
throw new IllegalStateException( "Please perform a sanity / reality check" );
|
22 |
return prediction.name();
|
25 |
void checkConsultantAvailability() {
|
26 |
if (company.isUnsatisfied()) {
|
27 |
throw new IllegalStateException( "No consultant to ask!" );
|
31 |
Consultant getConsultant() {
|
32 |
for (Consultant consultant : company) {
|
清单 1:作为 CDI 托管 Bean 的 REST 资源和网关的 EJB 3.1 Bean
Consultant 是一个由 Blogger、ReasonableConsultant 和 SmartConsultant 实现的 Java 接口。OracleResource 只是选择第一个 Consultant,向其询问 Java 的未来。除了 JAVA_IS_DEAD(会引起 IllegalStateException)外,所有答案均可接受。通常,您会使用 javax.inject.Qualifier 明确您的选择,但 javax.enterprise.inject.Instance 的测试比较困难,因此我使用 javax.enterprise.inject.Instance 来使测试更加“有趣”。
1 |
public class Blogger implements Consultant {
|
4 |
public Result predictFutureOfJava() {
|
6 |
return Result.JAVA_IS_DEAD;
|
清单 2:Blogger Consultant 实现
所有预言都作为事务事件进行分配。每个预言均在 EJB 容器启动的独立事务中执行。这是一种约定;无需为此进行额外配置。
PredictionAudit EJB 3.1 bean 接收事件,它使用 Java Persistence API (JPA) 2 保存所有成功的和失败的预言,因为已知有些 consultant 会回滚他们的结论(参见清单 3)。
02 |
public class PredictionAudit {
|
07 |
public void onSuccessfulPrediction( @Observes (during = TransactionPhase.AFTER_SUCCESS) Result result) {
|
08 |
persistDecision(result, true );
|
11 |
public void onFailedPrediction( @Observes (during = TransactionPhase.AFTER_FAILURE) Result result) {
|
12 |
persistDecision(result, false );
|
15 |
void persistDecision(Result result, boolean success) {
|
16 |
Prediction prediction = new Prediction(result, success);
|
17 |
em.persist(prediction);
|
20 |
public List<Prediction> allDecisions() {
|
21 |
return this .em.createNamedQuery(Prediction.findAll).getResultList();
|
清单 3:事件驱动的 PredictionAudit
CDI 事件巧妙地将 OracleResource 与 PredictionAudit 分离,但同时也使测试变得更加困难。无论事务是提交还是回滚,对每个预言均保存 JPA 2 实体 Prediction(参见清单 4)。
03 |
@XmlAccessorType (XmlAccessType.FIELD)
|
04 |
@NamedQuery (name = Prediction.findAll, query = "Select d from Prediction d" )
|
05 |
public class Prediction {
|
07 |
public final static String PREFIX = "com.abien.testing.oracle.entity.Prediction." ;
|
08 |
public final static String findAll = PREFIX + "findAll" ;
|
13 |
@Column (name = "prediction_result" )
|
14 |
@Enumerated (EnumType.STRING)
|
15 |
private Result result;
|
16 |
@Temporal (TemporalType.TIME)
|
17 |
private Date predictionDate;
|
18 |
private boolean success;
|
21 |
this .predictionDate = new Date();
|
24 |
public Prediction(Result result, boolean success) {
|
27 |
this .success = success;
|
清单 4:JPA 2 实体结论
Maven 3 与单元测试 — 开始之前
Java EE 6 应用程序的单元测试没什么特别之处。只需将 JUnit 库添加到 pom.xml 文件中(参见清单 5),将您的类放到 src/test/java 目录中。在标准 Maven 生命周期 (mvn clean install) 期间,将自动执行所有 JUnit 测试。
2 |
< groupId >junit</ groupId >
|
3 |
< artifactId >junit</ artifactId >
|
4 |
< version >4.8.2</ version >
|
清单 5:在 pom.xml 中包括 JUnit 库
加载标准 Maven 原型包含的 Java EE 6 API 类时将收到一条奇怪的错误(参见清单 6)。
2 |
< groupId >javax</ groupId >
|
3 |
< artifactId >javaee-web-api</ artifactId >
|
5 |
< scope >provided</ scope >
|
清单 6:对 API 的引用不可用
Maven 信息库中的标准 Java EE 6 API 经过一个工具的处理,该工具从字节码中删除方法的主体实现,从而使 javaee-web-api 依赖对单元测试不可用。从 Java EE 6 API 加载类的任何尝试都将导致类似下面的错误:
1 |
Absent Code attribute in method that is not native or abstract in class file javax/enterprise/util/TypeLiteral
|
3 |
java.lang.ClassFormatError:Absent Code attribute in method that is not native or abstract in class file javax/enterprise/util/TypeLiteral
|
5 |
at java.lang.ClassLoader.defineClass1(Native Method)
|
6 |
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
|
清单 7:从 Java EE 6 API 加载类时导致的错误
您应该用应用程序供应商的实现替换 Java EE 6 API 类。对 GlassFish v3.1,最方便的方法是在嵌入式容器上使用单个依赖:
2 |
< groupId >org.glassfish.extras</ groupId >
|
3 |
< artifactId >glassfish-embedded-all</ artifactId >
|
5 |
< scope >provided</ scope >
|
清单 8:Java EE 6 API 替换
当然,您可以为 JUnit 测试或编译挑选 CDI、JPA、EJB 等库,或者使用 JBoss 或 Geronimo Maven 镜像中的有效替代品。
为何选择 Mockito?
Mockito 是一个易用的开源模拟库。Mockito 能够从类或接口创建“智能代理”(也称为模拟)。虽然这些代理不附带任何行为,但它们仍然十分有用。您可以调用方法,但只会返回默认值,或空值。创建这些模拟后,将使用 when(mock.getAnswer()).then(42) 语法记录它们的行为。
Mockito 非常适合“模拟”各种难以实现的类、资源或服务。您只要了解一个 org.mockito.Mockito 类就可以开始学习使用 Mockito 了。when-then“领域专用语言”由来自 Mockito 类的静态方法组成。对 org.mockito.Mockito 类的文档注释很好。事实上,由 org.mockito.Mockito 类中的 JavaDoc 标记生成了完整的文档集。
使用 Mockito 对 Java EE 6 进行单元测试
PredictionAudit 将根据事务结果设置“success”标记,并使用 EntityManager 保存 Prediction 实体。要精确测试 PredictionAudit 类,必须“模拟”EntityManager。我们将只测试 EntityManager#persist 方法是否接收正确参数,而不测试是否真正保存 Prediction 实体。(在后续文章中,我们将探讨对 PredictionAudit 与实际 JPA EntityManager 功能间交互的集成测试。)
使用改进的静态 mock(EntityManager.class) 方法创建模拟出的 EntityManager 实例。
01 |
import static org.mockito.Mockito.*;
|
03 |
public class PredictionAuditTest {
|
05 |
private PredictionAudit cut;
|
08 |
public void initializeDependencies() {
|
09 |
cut = new PredictionAudit();
|
10 |
cut.em = mock(EntityManager. class );
|
14 |
public void savingSuccessfulPrediction() {
|
15 |
final Result expectedResult = Result.BRIGHT;
|
16 |
Prediction expected = new Prediction(expectedResult, true );
|
17 |
this .cut.onSuccessfulPrediction(expectedResult);
|
18 |
verify(cut.em, times( 1 )).persist(expected);
|
22 |
public void savingRolledBackPrediction() {
|
23 |
final Result expectedResult = Result.BRIGHT;
|
24 |
Prediction expected = new Prediction(expectedResult, false );
|
25 |
this .cut.onFailedPrediction(expectedResult);
|
26 |
verify(cut.em, times( 1 )).persist(expected);
|
清单 9:PredictionAudit 的单元测试与模拟
模拟出的 EntityManager 实例直接注入到默认的可见域 PredictionAudit#em 中。在 savingSuccessfulPrediction 测试方法中(参见清单 9),创建了一个 Result 实例并将它传递给 onSuccessfulPrediction 方法,该方法创建 Prediction 实例并调用 EntityManager#persist(参见清单 3)。静态方法 verify(cut.em,times(1)).persist(expected) 验证是否使用所期望的参数仅调用过一次 EntityManager#persist 方法。
一个更复杂的模拟案例
EJB 3.1 OracleResource bean 的 javax.enterprise.event.Event 和 javax.enterprise.inject.Instance 这两个实例的注入对于测试来说更为有趣。将使用 Mockito#mock 方法再次执行 CDI 依赖项的初始化和模拟(参见清单 10)。
01 |
public class OracleResourceTest {
|
03 |
private OracleResource cut;
|
06 |
public void initializeDependencies() {
|
07 |
this .cut = new OracleResource();
|
08 |
this .cut.company = mock(Instance. class );
|
09 |
this .cut.eventListener = mock(Event. class );
|
清单 10:CDI 依赖项的初始化和模拟
我们从 helper 方法 checkConsultantAvailability 的测试开始,该方法验证顾问的可用性。如果未找到 Consultant 实现,我们希望生成 IllegalStateException。静态方法 Mockito#when 记录 Mockito#mock 方法所返回实例期待的行为。我们只需为 Instance#isUnsatisfied 返回 true,并且期待一个 IllegalStateException:
1 |
@Test (expected = IllegalStateException. class )
|
2 |
public void checkConsultantAvailabilityWithoutConsultant(){
|
3 |
when( this .cut.company.isUnsatisfied()).thenReturn( true );
|
4 |
this .cut.checkConsultantAvailability();
|
清单 11:预先记录模拟行为
如果 Consultant 给出一个很荒唐的预言,EJB bean OracleResource 将抛出一个 IllegalStateException。Consultant 接口的 Blogger 实现始终返回 JAVA_IS_DEAD,这将引发 IllegalStateException。为测试这一行为,我们必须模拟出 javax.enterprise.inject.Instance 返回的 Iterator:
1 |
Iterator mockIterator(Consultant consultant) { |
2 |
Iterator iterator = mock(Iterator. class );
|
3 |
when(iterator.next()).thenReturn(consultant);
|
4 |
when(iterator.hasNext()).thenReturn( true );
|
清单 12:模拟出 Iterator
我们改变 Instance 的行为使其返回模拟的 Iterator 实例:
1 |
@Test (expected = IllegalStateException. class )
|
2 |
public void unreasonablePrediction() {
|
3 |
Consultant consultant = new Blogger();
|
4 |
Iterator iterator = mockIterator(consultant);
|
5 |
when( this .cut.company.iterator()).thenReturn(iterator);
|
6 |
this .cut.predictFutureOfJava();
|
清单 13:返回模拟出的 Iterator
模拟出实例将使您可以控制实际使用哪个 Consultant 实现。在我们的案例中,我们可以仅使用 Blogger 实现来检查是否正确抛出了 IllegalStateException。您也可以创建一个 Consultant 模拟,返回这个模拟,而不是返回实际的 Blogger 实例。这种做法尤其适用于 Java EE 平台上具有很强依赖性的 Consultant 实现。
还模拟出了 Event 实例,这使您可以验证已执行的方法调用:
02 |
public void unreasonablePredictionFiresEvent() {
|
03 |
Consultant consultant = new Blogger();
|
04 |
Result expectedResultToFire = consultant.predictFutureOfJava();
|
05 |
Iterator iterator = mockIterator(consultant);
|
06 |
when( this .cut.company.iterator()).thenReturn(iterator);
|
09 |
this .cut.predictFutureOfJava();
|
10 |
} catch (IllegalStateException e) {
|
13 |
verify( this .cut.eventListener, times( 1 )).fire(expectedResultToFire);
|
清单 14:测试 Event#fire 调用
对失败案例的测试稍微麻烦一点。我们先接受 IllegalStateException,然后检查实际上是否调用了 Event#fire 方法。
与 Java SE 类似
Java EE 6 应用程序的单元测试与测试 Java Platform, Standard Edition (Java SE) 没有区别。Java EE 6 组件只是一些带批注的类。您无需以特殊的方式对待它们;而是应该重点关注业务逻辑的验证。
在容器内部测试所有内容是一种常见的错误做法。即使是 Java EE 6 容器也要花数秒钟的时间来启动和部署应用程序。在容器中测试业务逻辑不仅浪费时间,还会带来不必要的复杂性。在 Java EE 6 中,您应该始终将单元测试与集成测试分开。甚至应该分别执行单元测试和集成测试。使用模拟出的环境进行真正的单元测试快如闪电。PredictionAuditTest 和 OracleResourceTest 的执行甚至用不了半秒钟:
01 |
------------------------------------------------------- |
05 |
------------------------------------------------------- |
07 |
Running com.abien.testing.oracle.boundary.OracleResourceTest |
09 |
Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.254 sec |
11 |
Running com.abien.testing.oracle.control.PredictionAuditTest |
13 |
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.025 sec |
17 |
Tests run: 8, Failures: 0, Errors: 0, Skipped: 0 |
清单 15:测试结果
最常见错误的观点:您需接口来进行模拟
接口的引入往往是由模拟造成的。然而,现代框架根本不需要接口就可实现模拟。可以直接由传统 Java 对象 (POJO) 类方便地创建模拟。PredictionArchiveResource 使用直接注入的 PredictionAudit 类(参见清单 16)。
03 |
public class PredictionArchiveResource {
|
06 |
PredictionAudit audit;
|
09 |
@Produces (MediaType.APPLICATION_JSON)
|
10 |
public List<Prediction> allPredictions( @DefaultValue ( "5" ) @QueryParam ( "max" ) int max) {
|
12 |
List<Prediction> allPredictions = audit.allPredictions();
|
14 |
if (allPredictions.size() <= max) {
|
15 |
return allPredictions;
|
17 |
return allPredictions.subList( 0 , max);
|
清单 16:POJO 类注入
尽管 PredictionAudit 是一个类而不是一个接口,但也可以轻松地模拟它。为了测试返回列表的大小限制,对 PredictionAudit 类进行了全面模拟(参见清单 17)。
01 |
public class PredictionArchiveResourceTest {
|
03 |
PredictionArchiveResource cut;
|
06 |
public void initialize() {
|
07 |
this .cut = new PredictionArchiveResource();
|
08 |
this .cut.audit = mock(PredictionAudit. class );
|
12 |
public void allDecisionsWithMaxLesserReturn() throws Exception {
|
14 |
List<Prediction> prediction = createDecisions(expectedSize);
|
15 |
when( this .cut.audit.allPredictions()).thenReturn(prediction);
|
16 |
List<Prediction> allDecisions = this .cut.allPredictions( 3 );
|
17 |
assertThat(allDecisions.size(), is(expectedSize));
|
21 |
public void allDecisionsWithMaxGreaterReturn() throws Exception {
|
24 |
List<Prediction> prediction = createDecisions(max);
|
25 |
when( this .cut.audit.allPredictions()).thenReturn(prediction);
|
26 |
List<Prediction> allDecisions = this .cut.allPredictions(expected);
|
27 |
assertThat(allDecisions.size(), is(expected));
|
31 |
public void allDecisionsWithMaxEqualReturn() throws Exception {
|
35 |
List<Prediction> createDecisions( final int nr) {
|
36 |
return new ArrayList<Prediction>() {
|
38 |
for ( int i = 0 ; i < nr; i++) {
|
39 |
add( new Prediction(Result.BRIGHT, true ));
|
清单 17:Java 类模拟
PredictionArchiveResource 类中唯一值得关注的业务逻辑就是列表大小的限制(参见清单 16)。QueryParameter 的最大值用于计算子列表的大小。PredictionArchiveResource 模拟实例允许返回任意大小的 List<Prediction>。无需访问 JPA 层(甚至数据库)就能控制列表的内容和大小将会极大简化测试。模拟还显著加快了测试执行速度。数据库访问需要几秒钟,而几毫秒就可完成对模拟的访问。
Java EE 6 使接口成为可选。您可以注入类(CDI 托管 bean 和 EJB bean),而不是接口,这丝毫不会牺牲任何“企业”特性。也完全不必为了模拟而使用接口。使用普通类(而非接口)作为一般的构建块,不仅可以使生产代码的实现更加精简,而且还能简化测试。您不必决定是模拟类还是接口。
总结
至此,我们仅测试了对象和方法的内部功能。为使测试尽可能简单,完全模拟出了周围的基础架构。我们的测试工作完全符合单元测试的定义:“在计算机编程中,单元测试是一种方法,使用这种方法对源代码的每个单元进行测试,以确定它们是否适用。单元是指应用程序最小的可测试部分。在过程编程中,单元可能是一个单独的函数或过程。在面向对象的编程中,单元通常是一个方法……”(http://en.wikipedia.org/wiki/Unit_testing)
尽管我们所有单元测试的结果均“成功”,但我们仍然不清楚应用程序是否可部署。JPA 2 映射错误、不一致的 persistence.xml 或 JPA 查询拼写错都根本无法使用也不应使用经典单元测试来测试。诸如 CDI 事件传递、复杂依赖注入或 JPA 查询之类与基础架构相关的逻辑只能使用集成测试在类似于生产的环境中进行测试。在后续文章中,我将探讨集成测试的方方面面。
另请参见
顾问兼作家 Adam Bien 是 Java EE 6 和 7、EJB 3.X、JAX-RS、CDI 和 JPA 2.X JSR 专家组成员。他从 JDK 1.0 就开始使用 Java 技术,并在几个大型项目中使用了 servlet/EJB 1.0,目前是 Java SE 和 Java EE 项目的架构师和开发人员。他编辑了多本关于 JavaFX、J2EE 和 Java EE 的图书,并且是《Real World Java EE Patterns—Rethinking Best Practices》和《Real World Java EE Night Hacks—Dissecting the Business Tier》两本书的作者。Adam 还是 Java Champion 和 JavaOne 2009 Rock Star。
相关推荐
在"javaee-testing-master"这个项目中,你可能会发现各种配置文件、测试类和脚本,它们展示了如何利用上述工具进行有效的Java EE单元测试。通过学习和实践这些示例,你可以更好地理解和掌握在复杂企业级环境中进行...
2. **测试阶段**:测试计划涵盖了多种类型的测试,包括单元测试、集成测试、系统测试、性能测试、验收测试以及评估测试。这些测试分别针对代码模块、多个模块间的交互、整体系统、系统性能、用户接受度和最终测试...
JUnit是Java常用的单元测试框架,而Apache Maven或Gradle可以帮助自动化构建和测试流程。 这个基于Java EE的网上测试系统不仅涵盖了Web开发的基本技术,还涉及到软件工程的最佳实践,如模块化、测试驱动开发和持续...
另外,测试框架如Arquillian的集成也使得单元测试和集成测试更为简单。 通过阅读《Java EE7权威指南》,开发者不仅可以掌握Java EE7的基本用法,还能了解到如何在实际项目中应用这些技术,提高开发效率,构建出健壮...
13. **部署与测试**:如何在Eclipse中配置和启动Tomcat、Jetty等应用服务器,以及如何进行单元测试和集成测试。 通过阅读“Java EE Web编程(Eclipse 平台).pdf”,你将能够获得以上各个方面的详细指导,并且...
本文主要介绍了基于Annotation的Java单元测试框架,讨论了Annotation在Java EE中的应用、反射技术的使用和JUnit单元测试框架的结合,建立了一个自动化单元测试框架结构。 一、Annotation概述 Annotation是Java 5中...
在本篇内容中,我们将深入探讨《使用Java EE实施SOA》这一主题,解析其核心概念、技术背景以及实现过程中的关键步骤和技术要点。 ### 一、SOA与Java EE概述 #### 1.1 SOA(面向服务的架构) SOA是一种软件设计方法...
使用JUnit进行单元测试,通过GlassFish的内置日志系统和管理工具进行应用的监控和调试,确保应用的稳定性和性能。 10. **最佳实践**: 了解并遵循Java EE 6的最佳实践,如使用CDI进行依赖管理,利用EJB的异步处理...
6. **部署与测试**: 使用Maven或Gradle进行项目构建,Jenkins或Travis CI进行持续集成,单元测试可能使用JUnit,集成测试可能用到Arquillian。 7. **文档**:如“博客系统需求规格说明说.doc”可能是项目初期的需求...
8. **测试与调试**:源码可能包括单元测试和集成测试,以验证不同组件之间的交互是否正确。Eclipse、IntelliJ IDEA等IDE可能被用来进行Java EE的开发和测试,而Flash Builder或IntelliJ IDEA的Flex插件则用于Flex...
同时,这也将帮助你掌握如何在大型项目中组织代码,如何设置合理的架构,以及如何使用单元测试和集成测试来验证应用的功能。 总的来说,学习Java EE并深入理解SSM框架,不仅可以提升你的编程技能,还能使你在面对...
11. **部署与测试**:掌握WAR和EAR文件的打包与部署,以及如何使用JUnit等工具进行单元测试和集成测试。 12. **开发工具与环境**:熟悉Eclipse、NetBeans或IntelliJ IDEA等集成开发环境(IDE),以及如何设置和配置...
6. **单元测试和集成测试**:书中可能讲解如何使用JUnit、TestNG等工具进行单元测试,以及使用Mockito、Arquillian等进行集成测试,确保代码质量。 7. **RESTful服务**:随着Web服务的发展,RESTful风格的API设计变...
第1章 Java EE的基本知识 1 1.1 Java EE的出现及其...23.3 利用JUnit进行单元测试 324 23.4 利用StrutsTestCase对Struts进行测试 328 23.5 压力测试和JMeter 334 23.6 其他开源测试工具 339 23.7 小结 343
3. **测试阶段**:使用JUnit、TestNG等工具进行单元测试,结合集成测试框架如Arquillian进行服务器上的测试。 4. **部署阶段**:将应用打包成WAR或EAR文件,部署到应用服务器如Tomcat、WildFly、WebLogic等。 5. **...
10. **测试与调试**:了解JUnit、TestNG等单元测试工具,以及如何在集成环境中如Tomcat、Glassfish或WildFly上部署和调试应用程序。 通过本教程,学习者不仅可以掌握Java EE的基本概念和技术,还能学习到如何将这些...