Roo中的Test

问题引入

我们在Roo Shell里新建一个entity时,如果添加一个 -testAutomatically 的参数,Roo会自动帮我们在srctestjava路径下生成如下几个文件:

和相应的切面文件:

为什么要生成这些文件呢?这些文件的用途是什么呢?

背景知识

先让我们来了解一些背景知识,来自《Spring Roo in Action》

Roo中testing的层次

对于一般的web-based application来说,测试可分为三种:

  • isolated, method-level unit tests
  • more sophisticated, in-container integration tests
  • live, externally exe­cuted website tests

在Roo中,这三种类型的tests,分别命名为Unit TestIntegration TestFunctional Test。它们之间的层次结构如下所示:

层次结构

可以看到,随着层次的升高,测试的复杂度和所需时间都在不断增加。

这里有对上述三种测试层次的通俗描述,注意里面给出的例子:

Unit Tests

Tests the smallest unit of functionality, typically a method/function (e.g. given a class with a particular state, calling x method on the class should cause y to happen). Unit tests should be focussed on one particular feature (e.g., calling the pop method when the stack is empty should throw an InvalidOperationException).

Integration Tests

Integration tests build on unit tests by combining the units of code and testing the resulting combination. This can be either the innards of one system, or combining multiple systems together to do something useful. Also, another thing that differentiates integration tests from unit tests is the environment. Integration tests can and will use threads, access the database or do whatever is required to ensure that all of the code and the different environment changes will work correctly.

Functional & Acceptance Tests

Functional tests usually check a particular feature for correctness by comparing the results for a given input against the specification. Functional tests don’t concern themselves with intermediate results or program state (they don’t care that after doing x, object y has state z), they are written to test specified behaviour such as, “when the user clicks the magnifying glass button on the side bar, the document is magnified by 25%”.

可以看出,从上到下测试的粒度从小到大,所牵涉的“关注点”从少到多,关注的视角也从开发者角度转移到用户角度。

Roo中testing不同层次的实现方式:

对于上文说到的三层测试,Roo中的实现方式如下所示:

  • Unit Tests:
    采用JUnit实现。
  • Integration Tests:
    同样是采用JUnit实现。在运行时通过加载特殊的runner来运行testing的environment(例如某个Server)。
  • Functional Tests:
    采用Selenium框架来进行测试。

分别对应以下Roo Command:

  • Unit Tests:
    test stub 和 test mock
  • Integration Tests:
    test integration
  • Functional Tests:
    selenium test –controller

在讲解这三种command之前要了解一下Roo的Testing组件:DataOnDemand Test Framework Component

DataOnDemand Test Framework Component

上文第一节中提到的EntityDataOnDemand类是Roo Testing的核心Util类,根据《Spring Roo In Action》定义如下:

The DataOnDemand component is a useful test class that helps you generate test fix­tures, the data required to set up your test case.

只要你在Roo Shell中输入test integration --entity ~.model.Entity或者dod --entity ~.model.Entity,Roo即可自动为你生成:

  • EntityDataOnDemand.java
  • EntityDataOnDemand_Roo_DataOnDemand.aj
  • EntityDataOnDemand_Roo_Configurable.aj

其中EntityDataOnDemand_Roo_Configurable.aj在本blog这里曾经详细讲述过。EntityDataOnDemand_Roo_DataOnDemand.aj切面文件通过运行时织入,和EntityDataOnDemand.java组成了EntityDataOnDemand类。

Course类为例,可用下图表示:

EntityDataOnDemand

留意到其中三个methods:

“EntityDataOnDemand”可以理解成“按需生成的数据”,上述就是实现这种“需求”的三个核心的方法。下面对其一一展开。

getNewTransientCourse()

有如下定义:

The getNewTransientEntity method returns an initialized, transient entity with sam­ple data stored in each field. The entity is not persistent, so this method can be used by unit tests and integration tests.

具体的用法如下所示:

从上可知,getNewTransientEntity()方法返回一个瞬时态(New/Transient)的实体,该实体的每一个属性都根据index参数,按照某种逻辑赋予了初始值:

  • String类型的属性按照“属性名_index值”的形式初始化,如:description_5。
  • 数值型的属性以index的值初始化。
  • date类型的属性以接近当前时间的随机时间初始化。
  • Boolean类型的属性初始化为true。
  • 联系的实体(单的一方)会通过调用getNewTransient***()方法初始化。

可以看出,getNewTransientEntity()方法为我们获得一个具有一定标识能力的实体提供了很方便的途径。在实际运用中,常用来测试实体类某个方法是否正确,或者生成一个实体参数供测试的方法调用,例如:

在Intergration Test中,也可以先调用getNewTransientEntity()方法获得实体,再调用save()flush()方法使entity持久化:

getSpecificCourse()

有如下定义:

The getSpecificEntity method returns an entity from the internal list of persisted entities.

getSpecificEntity()方法与getNewTransientCourse()方法相似,也有index作为参数。不同的是getSpecificEntity()方法仅对集成测试(Integra­tion Tests)有价值,仅能用于JPA环境中,并且已经被持久化,托管给某个EntityManager。

getSpecificEntity()方法被调用时,Roo会在一个已经持久化的“内部列表”中返回一个指定index值的实体,如果这个列表不存在(例如第一次调用这个方法),Roo会自动生成10个实体到list里,并且将其持久化。所以,如果你不想数据库处于一个不一致的状态(有10个奇怪的测试用的实体),那么在你的integration test方法声明处添加@Transactional注解,Roo会在测试完成时roll back你的的修改。

在Intergration Test中,可以这样用:

getRandomCourse()

有如下描述:

If you don’t care which persistent entity instance you work with, ask for a random one with getRandomEntity().

值得注意的是,该方法只会返回10个不同的实体,也就是说重复调用getRandomCourse()并不一定返回两个不同的实体。可以通过为getNewTransientEntity()方法设置不同的index值来获得任意个不同的实体。

总的来说,DataOnDemand的作用是:

speed up your test writing, making the cre­ation of simple entity instances a trivial operation.

你可以重写DataOnDemand中的方法,使其适应你的需求。但要注意的是,要保持上述三个关键方法的正确性(因为后面某些测试方式依赖于这三个方法)。若要知道这三个方法的具体实现方式,可以自己用-testAutomatically命令看Roo自动生成的aj文件中的源代码,由于篇幅问题,这里就不作详细描述了。

了解完DataOnDemand Test Framework Component之后,再来看看Roo是如何实现三种不同类型的测试的。

Unit Test

test stub命令

test stub命令的定义如下:

The test stub command creates a JUnit test that constructs an instance of a class, and creates test stubs for each of the public methods.

例如,Sensor类的定义如下所示:

在Roo Shell中输入命令:test stub --class ~.entity.Sensor,Roo在src/test/java下以相同的包名新建了一个SensorTest.java文件,文件内容如下所示:

可以看出,test stubSensor类的每一个public方法,包括aj文件中的,都生成了同名的测试方法。如何理解@Test注解?可以看这里这里

mock

Roo中我们还可以通过“mock object”来进行单元测试。

什么是mock?可以参考如下定义:

Mock objects are objects that pretend to be instances of particular classes, but arecompletely controlled by the test developer. They appear to implement the specified interfaces, and you can configure them to return predictable values when their methods are called.

也就是说,通过“mock”我们可以模拟出一个指定的对象,这个对象可以协助我们对目标进行测试。通常我们可以令mock对象实现某个接口,由此代替测试目标的某个依赖。由于mock是可控可定义的,所以为我们控制测试目标提供了很大的方便:

Mock objects are often used when a layered application requires a particular Spring bean to collaborate with other beans, either in the same level or lower levels of the application.

test mock command

下面通过一个例子来说明Roo中mock test的步骤:

在Roo Shell中输入:test mock --entity ~.model.Course,Roo自动生成CourseTest.java文件(若文件已存在,则无任何改变),文件部分内容如下所示:

可以看到,Roo自动生成了CourseTest类,并用@MockStaticEntityMethods标注。自动生成testMethod()示例方法(本身只作示例用,并无特殊含义),该方法中通过调用AnnotationDrivenStaticEntityMockingControl类的方法来控制mock对象的输出。整个过程可以简述成:

  1. Roo为CourseTest单元测试类添加了@MockStaticEntityMethods注解。@MockStaticEntityMethods注解使整个CourseTest运行在“expectation record”模式中。在该模式下,所有静态方法的调用都不会被真正地执行,而是将该“执行”的调用放到某个内部的队列内。具体地,例子中的Course.countCourses()不会真正地执行(也就是该静态方法没有访问数据库),而是向mock维护着的某个内部队列中添加了类似“Course调用了countCourses()”的信息。
  2. 紧接着,调用expectReturn(expectedCount)方法,将expectedCount放进另外一个内部队列中,表明这个“expectation”应该由目前队列中最前面的静态方法返回(Course.countCourses()方法)。
  3. 调用playback()使CourseTest处于“回放(playback)”模式。在该模式下,随后的每一次静态的调用都会按其对应“expectation”入队的顺序返回相应的值。具体地,例子中最后一次Course.countCourses()的调用将会返回13。

可以理解成,通过@MockStaticEntityMethodsexpectReturn()方法登记那些需要mock的方法,然后在playback模式下,由mock对象(隐藏的)取代原对象的调用返回设定的return值。所以,利用其队列的性质,可以如下使用:

该测试可以正确通过。

书中还提到一个具体的例子,这里简单地叙述一下:

假设我们要测试“学生选课”这个用例,“选课”服务的代码如下:

可以看出,这个方法牵涉到三个实体类,分别是OfferingStudentRegistrationOfferingStudent之间是多对多的关系,它们通过Registration联系起来。具体的时序图如下所示:

时序图

那么,我们如何通过mock来测试completeRegistration方法呢?可以按如下步骤来进行:

  1. 输入命令test mock --entity ~.service.RegistrationServiceBeanImpl。Roo会自动生成RegistrationServiceDefaultImplBeanTest类,并用@MockStaticEntityMethods标注。
  2. 声明三个dod类:

  3. 声明测试用的service:

  4. 利用@before注解初始化上述变量:

  5. 编写测试方法:

至此mock测试编写完成。可以看出,在Unit Test中利用mock,我们可以将某个测试点跟运行环境隔离开来,并控制输入参数的变化,通过比较测试对象的输出或者其他行为来判断是否通过测试。

Integration Test

在Roo中进行集成测试(Integration Test)的步骤比较简单,可以分为两种,一种是利用Roo自动生成的EntityIntegrationTest类,另一种是手写代码进行Integration Test。

test integration command

在Roo Shell中我们输入test integration --entity ~.model.Sensor,Roo自动生成了EntityIntegrationTest.javaEntityIntegrationTest_Roo_Configurable.ajEntityIntegrationTest_Roo_IntegrationTest.aj文件。其中EntityIntegrationTest_Roo_IntegrationTest.aj文件如下所示:

可以看出,Roo对每个通过Roo命令生成的方法(finder,CRUD等)都生成了测试方法。

手写Integration Test

你可以采用这个命令:class --class ~.web.***Test --path SRC_TEST_JAVA为你的测试目标生成一个测试类。然后你要为这个测试类添加如下注解:

Spring Roo in Action中是这样解释@ContextConfiguration@RunWith注解的:

The @ContextConfiguration annotation defines the Spring context files to search for to load your test.

Next, you tell JUnit that it has to run under the Spring Framework with your @RunWith annotation.

接着你便可以在***Test.java中编写你的测试方法了。

Functional Test

上述的Unit Test和Integration Test都属于white-box tests,white-box tests关心的是程序内部运行的逻辑。下面我们要进行的是另一测试,叫做Functional Test,属于black-box tests的一种。我们从用户的角度出发,从程序的界面开始进行测试。

Selenium简介

在Roo中,Functional Test由Selenium web testing framework来实现。Spring Roo in Action中是这样介绍Selenium的:

Selenium (http://seleniumhq.org) is a suite of web testing tools. It can exercise browser-based tests against an application, and has a Firefox-based IDE (Selenium IDE) for building tests interactively against a live application.

利用Selenium,你可以:

  • 用例测试:基于浏览器的用例测试。
  • 监控:通过检测controller是否返回正确的值来监控整个程序是否正确运行。
  • 压力测试:利用Selenium自带的测试引擎和多台电脑来对程序进行压力测试。

下面我们开始一步步探索Selenium。

selenium command

在Roo Shell中输入:selenium test --controller ~.web.**Controller,自动生成如下文件:

可以知道,Roo自动地在menu.jspx中添加了链接到test-suite.xhtml的menu项,在pom.xml添加了selenium的依赖包,在src/main/webapp/selenium文件夹下新建了test-suite.xhtmltest-sensor.xhtml两个文件。

打开menu.jspx文件,看到Roo添加了如下代码:

可知道该menu项访问了/resources/selenium/test-suite.xhtml这个文件,但为什么可以直接访问程序的资源路径呢?所有url不是转发给controller了吗?打开webmvc-config.xml文件,看到如下代码:

可以看到,改代码声明了一个允许用get方法访问项目资源路径的配置,将/resources/**映射到/, classpath:/META-INF/web-resources/实际路径。所以我们可以直接访问上述文件。

再打开test-suite.xhtml文件,有如下代码:

可以看出,这个文件是各个selenium test的总入口,它以列表的形式提供了各个test-***.xhtml文件的链接。再打开test-sensor.xhtml文件,部分代码如下所示:

看上去像一个不知所云的html文件,但其实这是“HTML-based Selenium test language”,书中是这样解释的:

The HTML-based Selenium test language was designed so that power users and advanced business experts could read and interpret it.

可以把它想象成一个测试脚本,只不过这个脚本文件以html语言为基础而已。这样做有一个好处,就是我们可以用浏览器直接打开它,直观地观察这个配置文件:

示例

可以这样理解这些代码:test-sensor.xhtml模拟了一个用户的整个操作流程以及用户对流程中程序的反馈结果的响应方式,即通过“open”、“type”等参数模拟用户进行用例时的操作步骤,“verifyText”、“assertText”等参数定义用户判断用例的结果是否正确的标准。

接下来简单解释“open”、“type”这些参数的含义:

  • open:定义了测试的起点,即controller对应的url。
  • type:定义要填入参数的field(例如_identifier_id),和其相应的值(例如someIdentifier1)。
  • clickAndWait:让selenium框架触发id为proceed按钮(//input[@id = ‘proceed’]的含义),并且等待返回一个合法的值。
  • verifyText:监测某个html node的值,看是否跟预设的相符合,若不符合,输出信息,然后继续执行。
  • assertText:监测某个html node的值,看是否跟预设的相符合,若不符合,中断测试。

更多常用参数请看这里

运行 selenium

首先,确保你安装了Firefox浏览器(filefox可执行程序要在系统变量PATH里)和可以在命令行(或者终端)中运行mvn命令。然后开启web服务器,cd到项目文件夹,输入:mvn selenium:selenese来启动selenium tests。

输入命令后,firefox会自动启动,并进入了selenium test模式,在该模式下你会发现浏览器按照预先编写好的脚本一步步进行测试,测试完成后firefox会自动退出,mvn会显示测试的大致结果,并在target目录下生成了一个名为selenium.html的详细的测试报告。

总结

测试分为三种,分别是Unit Test,Integration Test和Functional Test。Roo中以test stub和test mock来实现Unit Test,其中stub最为简单,mock适合模拟依赖关系;以test integration来实现Integration Test,注意要用注解来指定运行环境;以selenium框架来实现Functional Test,用selenium language来模拟用户的操作和判断结果的逻辑。