问题引入
我们在Roo Shell里新建一个entity时,如果添加一个 -testAutomatically
的参数,Roo会自动帮我们在srctestjava
路径下生成如下几个文件:
EntityDataOnDemand.java
EntityIntegrationTest.java
和相应的切面文件:
EntityDataOnDemand_Roo_DataOnDemand.aj
EntityDataOnDemand_Roo_Configurable.aj
EntityIntegrationTest_Roo_Configurable.aj
EntityIntegrationTest_Roo_IntegrationTest.aj
为什么要生成这些文件呢?这些文件的用途是什么呢?
背景知识
先让我们来了解一些背景知识,来自《Spring Roo in Action》:
Roo中testing的层次
对于一般的web-based application来说,测试可分为三种:
- isolated, method-level unit tests
- more sophisticated, in-container integration tests
- live, externally executed website tests
在Roo中,这三种类型的tests,分别命名为Unit Test、Integration Test和Functional 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 fixtures, 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
类为例,可用下图表示:
留意到其中三个methods:
getNewTransientCourse(index)
getSpecificCourse(index)
getRandomCourse()
“EntityDataOnDemand”可以理解成“按需生成的数据”,上述就是实现这种“需求”的三个核心的方法。下面对其一一展开。
getNewTransientCourse()
有如下定义:
The getNewTransientEntity method returns an initialized, transient entity with sample data stored in each field. The entity is not persistent, so this method can be used by unit tests and integration tests.
具体的用法如下所示:
CourseDataOnDemand dod = new CourseDataOnDemand();
Course course = dod.getNewTransientEntity(5);
从上可知,getNewTransientEntity()
方法返回一个瞬时态(New/Transient)的实体,该实体的每一个属性都根据index
参数,按照某种逻辑赋予了初始值:
- String类型的属性按照“属性名_index值”的形式初始化,如:description_5。
- 数值型的属性以index的值初始化。
- date类型的属性以接近当前时间的随机时间初始化。
- Boolean类型的属性初始化为true。
- 联系的实体(单的一方)会通过调用
getNewTransient***()
方法初始化。
可以看出,getNewTransientEntity()
方法为我们获得一个具有一定标识能力的实体提供了很方便的途径。在实际运用中,常用来测试实体类某个方法是否正确,或者生成一个实体参数供测试的方法调用,例如:
Course course = dod.getNewTransientCourse(0);
course.setListPrice(new BigDecimal(10.0d));
assertNull(course.getListPrice());
在Intergration Test中,也可以先调用getNewTransientEntity()
方法获得实体,再调用save()
、flush()
方法使entity持久化:
Course course = dod.getNewTransientCourse(0);
course.setListPrice(new BigDecimal(10.0d));
course.persist();
course.flush();
assertNotNull(course.getId());
getSpecificCourse()
有如下定义:
The getSpecificEntity method returns an entity from the internal list of persisted entities.
getSpecificEntity()
方法与getNewTransientCourse()
方法相似,也有index作为参数。不同的是getSpecificEntity()
方法仅对集成测试(Integration Tests)有价值,仅能用于JPA环境中,并且已经被持久化,托管给某个EntityManager。
当getSpecificEntity()
方法被调用时,Roo会在一个已经持久化的“内部列表”中返回一个指定index值的实体,如果这个列表不存在(例如第一次调用这个方法),Roo会自动生成10个实体到list里,并且将其持久化。所以,如果你不想数据库处于一个不一致的状态(有10个奇怪的测试用的实体),那么在你的integration test方法声明处添加@Transactional
注解,Roo会在测试完成时roll back你的的修改。
在Intergration Test中,可以这样用:
Course course = dod.getSpecificCourse(0);
Long oldVersion = course.getVersion();
course.setListPrice(new BigDecimal(103.0d));
course.update();
course.flush();
assertTrue(oldversion != course.getVersion());
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 creation 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
类的定义如下所示:
@RooJavaBean
@RooToString
@RooJpaActiveRecord
@RooJson
public class Sensor {
@NotNull
private String identifier;
@NotNull
@ManyToOne
private SensorType type;
@NotNull
private Float min;
@NotNull
private Float max;
private String description;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "sensor", fetch = FetchType.LAZY)
private Set<SensorData> sensorDatas = new HashSet<SensorData>();
@OneToOne(cascade = CascadeType.ALL, optional = true)
private SensorLayout sensorLayout;
@ManyToOne
private Shed shed;
public static List<demo.imlab.ims.entity.Sensor> getRandomSensors(int limited) {
...
}
public static SensorData getSensorLastestData(demo.imlab.ims.entity.Sensor sensor) {
...
}
}
在Roo Shell中输入命令:test stub --class ~.entity.Sensor
,Roo在src/test/java
下以相同的包名新建了一个SensorTest.java
文件,文件内容如下所示:
public class SensorTest {
private Sensor sensor = new Sensor();
@Test
public void getIdentifier() {
org.junit.Assert.assertTrue(true);
}
@Test
public void setIdentifier() {
org.junit.Assert.assertTrue(true);
}
...
@Test
public void getSensorDatas() {
org.junit.Assert.assertTrue(true);
}
@Test
public void setSensorDatas() {
org.junit.Assert.assertTrue(true);
}
...
@Test
public void entityManager() {
org.junit.Assert.assertTrue(true);
}
@Test
public void countSensors() {
org.junit.Assert.assertTrue(true);
}
@Test
public void findAllSensors() {
org.junit.Assert.assertTrue(true);
}
...
@Test
public void persist() {
org.junit.Assert.assertTrue(true);
}
...
@Test
public void toJson() {
org.junit.Assert.assertTrue(true);
}
...
}
可以看出,test stub
为Sensor
类的每一个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
文件(若文件已存在,则无任何改变),文件部分内容如下所示:
@RunWith(JUnit4.class) @MockStaticEntityMethods public class CourseTest {
@Test
public void testMethod() {
int expectedCount = 13;
Course.countCourses();
org.springframework.mock.staticmock.AnnotationDrivenStaticEntityMockingControl.expectReturn(expectedCount);
org.springframework.mock.staticmock.AnnotationDrivenStaticEntityMockingControl.playback();
org.junit.Assert.assertEquals(13, Course.countCourses());
}
}
可以看到,Roo自动生成了CourseTest
类,并用@MockStaticEntityMethods
标注。自动生成testMethod()
示例方法(本身只作示例用,并无特殊含义),该方法中通过调用AnnotationDrivenStaticEntityMockingControl
类的方法来控制mock对象的输出。整个过程可以简述成:
- Roo为
CourseTest
单元测试类添加了@MockStaticEntityMethods
注解。@MockStaticEntityMethods
注解使整个CourseTest
运行在“expectation record”模式中。在该模式下,所有静态方法的调用都不会被真正地执行,而是将该“执行”的调用放到某个内部的队列内。具体地,例子中的Course.countCourses()
不会真正地执行(也就是该静态方法没有访问数据库),而是向mock维护着的某个内部队列中添加了类似“Course调用了countCourses()”的信息。 - 紧接着,调用
expectReturn(expectedCount)
方法,将expectedCount
放进另外一个内部队列中,表明这个“expectation”应该由目前队列中最前面的静态方法返回(Course.countCourses()
方法)。 - 调用
playback()
使CourseTest
处于“回放(playback)”模式。在该模式下,随后的每一次静态的调用都会按其对应“expectation”入队的顺序返回相应的值。具体地,例子中最后一次Course.countCourses()
的调用将会返回13。
可以理解成,通过@MockStaticEntityMethods
和expectReturn()
方法登记那些需要mock的方法,然后在playback
模式下,由mock对象(隐藏的)取代原对象的调用返回设定的return值。所以,利用其队列的性质,可以如下使用:
...
public void testMethod() {
int expectedCount = 13;
Course.countCourses();
org.springframework.mock.staticmock.AnnotationDrivenStaticEntityMockingControl.expectReturn(expectedCount);
Course.findAllCourses();
org.springframework.mock.staticmock.AnnotationDrivenStaticEntityMockingControl.expectReturn(new ArrayList<>());
org.springframework.mock.staticmock.AnnotationDrivenStaticEntityMockingControl.playback();
org.junit.Assert.assertEquals(13, Course.countCourses());
org.junit.Assert.assertEquals(0, Course.findAllCourses().size());
}
....
该测试可以正确通过。
书中还提到一个具体的例子,这里简单地叙述一下:
假设我们要测试“学生选课”这个用例,“选课”服务的代码如下:
public class RegistrationServiceDefaultImplBean
implements RegistrationService {
@Override
@Transactional
public void completeRegistration(
Long offeringId, List<Long> studentIds) {
Offering offering = Offering.findOffering(offeringId);
for (Long studentId : studentIds) {
Student student = Student.findStudent(studentId);
Registration registration = new Registration();
registration.setStudent(student);
registration.setOffering(offering);
registration.setAttended(false);
registration.setPaymentMade(false);
offering.addRegistration(registration);
registration.persist();
}
}
}
可以看出,这个方法牵涉到三个实体类,分别是Offering
、Student
和Registration
。Offering
和Student
之间是多对多的关系,它们通过Registration
联系起来。具体的时序图如下所示:
那么,我们如何通过mock来测试completeRegistration
方法呢?可以按如下步骤来进行:
- 输入命令
test mock --entity ~.service.RegistrationServiceBeanImpl
。Roo会自动生成RegistrationServiceDefaultImplBeanTest
类,并用@MockStaticEntityMethods
标注。 -
声明三个dod类:
private StudentDataOnDemand studentDod; private OfferingDataOnDemand offeringDod; private CourseDataOnDemand courseDod;
-
声明测试用的service:
private RegistrationServiceDefaultImplBean registrationServiceDefaultImplBean;
-
利用
@before
注解初始化上述变量:@Before public void setUp() { registrationServiceDefaultImplBean = new RegistrationServiceDefaultImplBean(); studentDod = new StudentDataOnDemand(); offeringDod = new OfferingDataOnDemand(); courseDod = new CourseDataOnDemand(); }
-
编写测试方法:
@Test public void testRegisterStudents() { //设置Offering.findOffering(id)的返回值 Offering offering = offeringDod.getNewTransientOffering(1); offering.setId(1L); Offering.findOffering(1L); AnnotationDrivenStaticEntityMockingControl.expectReturn(offering); //设置Student.findStudent(id)的返回值 List<Long> ids = new ArrayList<Long>(); List<Student> testStudents = new ArrayList<Student>(); for (int id = 0; id < 10; id++) { Student.findStudent((long)id); Student student = studentDod.getNewTransientStudent(id); student.setId(Long.valueOf(id)); ids.add(Long.valueOf(id)); AnnotationDrivenStaticEntityMockingControl.expectReturn(student); } //进入playback模式 AnnotationDrivenStaticEntityMockingControl.playback(); //进行测试 //Offering.findOffering(id)和Student.findStudent(id)在completeRegistration()中被调用,返回预设的值 registrationServiceDefaultImplBean.completeRegistration(offering.getId(), ids); Set<Registration> registrations = offering.getRegistrations(); Assert.assertEquals(10, registrations.size()); }
至此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.java
、EntityIntegrationTest_Roo_Configurable.aj
和EntityIntegrationTest_Roo_IntegrationTest.aj
文件。其中EntityIntegrationTest_Roo_IntegrationTest.aj
文件如下所示:
privileged aspect SensorIntegrationTest_Roo_IntegrationTest {
declare @type: SensorIntegrationTest: @RunWith(SpringJUnit4ClassRunner.class);
declare @type: SensorIntegrationTest: @ContextConfiguration(locations = "classpath:/META-INF/spring/applicationContext*.xml");
declare @type: SensorIntegrationTest: @Transactional;
@Autowired
SensorDataOnDemand SensorIntegrationTest.dod;
@Test
public void SensorIntegrationTest.testCountSensors() {
Assert.assertNotNull("Data on demand for 'Sensor' failed to initialize correctly", dod.getRandomSensor());
long count = Sensor.countSensors();
Assert.assertTrue("Counter for 'Sensor' incorrectly reported there were no entries", count > 0);
}
@Test
public void SensorIntegrationTest.testFindSensor() {...}
@Test
public void SensorIntegrationTest.testFindAllSensors() {...}
@Test
public void SensorIntegrationTest.testFindSensorEntries() {...}
@Test
public void SensorIntegrationTest.testFlush() {...}
...
}
可以看出,Roo对每个通过Roo命令生成的方法(finder,CRUD等)都生成了测试方法。
手写Integration Test
你可以采用这个命令:class --class ~.web.***Test --path SRC_TEST_JAVA
为你的测试目标生成一个测试类。然后你要为这个测试类添加如下注解:
@ContextConfiguration(locations = "classpath:/META-INF/spring/applicationContext*.xml")
@RunWith(SpringJUnit4ClassRunner.class)
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
,自动生成如下文件:
Updated SRC_MAIN_WEBAPPWEB-INFi18napplication.properties
Created SRC_MAIN_WEBAPPselenium
Created SRC_MAIN_WEBAPPseleniumtest-sensor.xhtml
Created SRC_MAIN_WEBAPPseleniumtest-suite.xhtml
Updated SRC_MAIN_WEBAPPWEB-INFviewsmenu.jspx
Updated ROOTpom.xml [added plugin org.codehaus.mojo:selenium-maven-plugin:2.3]
可以知道,Roo自动地在menu.jspx
中添加了链接到test-suite.xhtml
的menu项,在pom.xml
添加了selenium的依赖包,在src/main/webapp/selenium
文件夹下新建了test-suite.xhtml
和test-sensor.xhtml
两个文件。
打开menu.jspx
文件,看到Roo添加了如下代码:
<menu:category id="c_seleniumtests" z="PPLMd2yNB6/LSbjIWogAacRPU+Q=">
<menu:item id="si_seleniumtests_test" messageCode="selenium_menu_test_suite" url="/resources/selenium/test-suite.xhtml" z="wwunW/IPDcunN97u41MvMJZQ2to="/>
</menu:category>
可知道该menu项访问了/resources/selenium/test-suite.xhtml
这个文件,但为什么可以直接访问程序的资源路径呢?所有url不是转发给controller了吗?打开webmvc-config.xml
文件,看到如下代码:
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources -->
<mvc:resources location="/, classpath:/META-INF/web-resources/" mapping="/resources/**"/>
可以看到,改代码声明了一个允许用get方法访问项目资源路径的配置,将/resources/**
映射到/, classpath:/META-INF/web-resources/
实际路径。所以我们可以直接访问上述文件。
再打开test-suite.xhtml
文件,有如下代码:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<title>Test suite for MonitorControlServiceDemoproject</title>
</head>
<body>
<table>
<tr>
<td>
<b>Suite Of Tests</b>
</td>
</tr>
<tr>
<td>
<a href="http://localhost:8088/MonitorControlServiceDemo/resources/selenium/test-sensor.xhtml">Selenium test for SensorController</a>
</td>
</tr>
</table>
</body>
</html>
可以看出,这个文件是各个selenium test的总入口,它以列表的形式提供了各个test-***.xhtml
文件的链接。再打开test-sensor.xhtml
文件,部分代码如下所示:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
<link href="http://localhost:8080/" rel="selenium.base"/>
<title>Selenium test for SensorController</title>
</head>
<body>
<table border="1" cellpadding="1" cellspacing="1">
<thead>
<tr>
<td colspan="3" rowspan="1">Selenium test for SensorController</td>
</tr>
</thead>
<tbody>
<tr>
<td>open</td>
<td>/MonitorControlServiceDemo/sensors?form&lang=zh_CN</td>
<td> </td>
</tr>
<tr>
<td>type</td>
<td>_identifier_id</td>
<td>someIdentifier1</td>
</tr>
...
<tr>
<td>clickAndWait</td>
<td>//input[@id = 'proceed']</td>
<td> </td>
</tr>
...
<tr>
<td>verifyText</td>
<td>_s_demo_imlab_ims_entity_Sensor_min_min_id</td>
<td>1.0</td>
</tr>
...
</tbody>
</table>
</body>
</html>
看上去像一个不知所云的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来模拟用户的操作和判断结果的逻辑。