本文基于SpringMVC搭建测试框架
通常情况我们可以借助easyMock及powerMock进行单元测试,但有时我们希望进行集成测试,可以通过发送http请求,测试某功能的完整性。
一般情况我们可以通过MockMvc模拟post或get请求,完成测试。但是当碰到delete或update进行测试时,容易对数据库造成污染,这时我们可以借助dbunit,对数据库进行测试数据的准备,测试完成后对事务进行回滚,方便下次测试。
1. maven集成测试组件
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.10.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!--数据库单元测试:消除单元测试对数据库的污染 start-->
<dependency>
<groupId>com.github.springtestdbunit</groupId>
<artifactId>spring-test-dbunit</artifactId>
<version>1.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.dbunit</groupId>
<artifactId>dbunit</artifactId>
<version>2.4.9</version>
<scope>test</scope>
</dependency>
<!--数据库单元测试:消除单元测试对数据库的污染 end-->
2. 定义测试基类,包括SpringMVC框架的集成,junit集成
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@PropertySource("classpath:common.properties")
@TestPropertySource("classpath:common.properties")
@TestExecutionListeners({DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class,
ServletTestExecutionListener.class,
DbUnitTestExecutionListener.class}) //@1
@ContextConfiguration(
{"classpath*:/spring-context.xml", "classpath*:/spring-mvc.xml", "classpath*:/spring-mybatis.xml"})
@TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true)//@2
@Transactional
public abstract class BaseSpringJUnitTest {
public static Logger logger = LoggerFactory.getLogger(BaseSpringJUnitTest.class);
@Autowired
private UserInfoService userInfoService;
private static boolean inited = false;
/**
* junit模拟用户名
*/
private final static String USER_NAME = "admin";
/**
* junit模拟验证码
*/
private final static String VALIDATE_CODE = "1234";
/**
* junit模拟密码
*/
private final static String PASSWORD = "Admin123456";
public static String token = "";
protected MockMvc mockMvc; //@3
@Before //@4
public void setUp() throws Exception {
if (!inited) {
String code = userInfoService.getValidateKey().get("validateKey").toString();
RedisUtils.set("validCode:" + code, VALIDATE_CODE);
UserInfoRequestParams params = new UserInfoRequestParams();
params.setLoginName(USER_NAME);
params.setPassword(MD5Util.encoderHexByMd5(PASSWORD));
params.setValidateKey(code);
params.setValidateCode(VALIDATE_CODE);
JSONOutputObject result = userInfoService.webLogin(params);
token = result.get("token").toString();
TestCase.assertEquals(RsmsConstant.RESULT_SUCCESS_CODE, result.get(RsmsConstant.RESULT_CODE));
inited = true;
}
}
}
@1:ServletTestExecutionListener 用于设置spring web框架启动时的RequestContextHolder本地线程变量。我们的项目比较特殊,在service层中是通过RequestContextHolder获取httpRequest对象(并非通过controller透传),如果不设置,则在service中或者切面中无法获取到request对象
@2:添加事务回滚,避免对数据库的污染
@3:定义MockMvc对象,模拟客户端的http请求
@4:初始化登录信息,根据自身需要设置,有些集成测试场景可能需要登录信息
3.编写测试类
public class UserInfoControllerTest extends BaseSpringJUnitTest {
@Test
@DatabaseSetup(type = DatabaseOperation.INSERT, value = {"/data/user-info.xml"})//@1
public void testDeleteUsers() throws Exception {
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
//@2
String responseString = mockMvc.perform(post("/user/deleteUsers")
.contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
.content("{\"idList\":10007}")
.header("Current-Menu-Id", "102100")
.header("Menu-Id", "102100")
.accept(MediaType.ALL_VALUE)
.header("token", token)
.header("User-Agent", "Windows NT")
).andExpect(status().isOk())
.andReturn().getResponse().getContentAsString();
JSONObject jsonObject = JSONObject.parseObject(responseString);
Assert.assertEquals(jsonObject.get("resultCode"), "0000");
}
}
@1:准备测试数据,因为在测试时,如果不自己准备数据,依赖数据库数据,那么数据库数据有可能被其他人误删或者数据库做了迁移或更新之后,我们的测试用例将无法跑通。
@2:通过mockMvc发送http请求,并解析请求结果,判断测试结果的正确性
4.准备测试数据(resource/data/user_info.xml)
<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
<t_user_info id="10007" login_name="admin_junit" password="" salt="" user_name="admin_junit" user_name_spell=""
user_name_initial="" eid="" cellphone="" company_id="200" org_id="200" position=""/>
<t_user_role id="1000" user_id="10007" role_id="1" />
</dataset>
5.jenkins集成,并统计单元测试覆盖率
<!-- 统计junit覆盖率 -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.1</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
jenkins添加普通的maven构建任务,设置好git地址
插件管理中安装jacoco插件
build项设置如下
Root POM pom.xml
Goals and options test -Dmaven.repo.local=/opt/repository
构建后操作添加:Record JaCoCo coverage report
统计结果效果如下
6.配置邮件发送
邮件发送本人是集成了cobertura(和jacoco共一样,都是用于覆盖率的统计)
pom集成
<!-- cobertura统计junit覆盖率 -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>cobertura-maven-plugin</artifactId>
<version>2.7</version>
<configuration>
<formats>
<format>html</format>
<format>xml</format>
</formats>
</configuration>
</plugin>
jenkins全局配置:jenkins->系统管理->系统设置
系统管理员地址:*********@qq.com
Extended E-mail Notification中配置如下
SMTP:smtp.qq.com
后缀@qq.com
点开高级
使用SMTP认证
用户名:*********@qq.com
密码:qq给的授权码(非邮箱的登录密码,授权码的获取:登录QQ邮箱:设置-SMTP设置-开启,需要发送短信,发送短信后,页面会显示授权码)
jenkins构建任务配置邮件发送
构建后操作:增加Editable Email Notification,点开高级,一定要设置triggers,否则无法触发
邮件发送我用的是jelly模板,使用的时候只需要填写模板文件名称(.jelly后缀不需要),模板文件放在jenkins服务器上,具体模板代码见附件一,见下图
7.邮件发送最终效果
邮件主要内容:构建状态、覆盖率整体情况、包覆盖率情况、UT趋势、变更集等等
附件一:jelly邮件模板
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define">
<html>
<head>
<title>${project.name}</title>
<style>
body table, td, th, p, h1, h2 {
margin:0;
font:normal normal
100% Georgia, Serif;
background-color: #ffffff;
}
h1, h2 {
border-bottom:dotted 1px #999999;
padding:5px;
margin-top:10px;
margin-bottom:10px;
color: #000000;
font: normal bold 130%
Georgia,Serif;
background-color:#f0f0f0;
}
tr.gray {
background-color:#f0f0f0;
}
h2 {
padding:5px;
margin-top:5px;
margin-bottom:5px;
font: italic bold 110% Georgia,Serif;
}
.bg2 {
color:black;
background-color:#E0E0E0;
font-size:110%
}
th {
font-weight: bold;
}
tr, td, th {
padding:2px;
}
td.test_passed {
color:blue;
}
td.test_failed {
color:red;
}
td.center {
text-align: center;
}
td.test_skipped {
color:grey;
}
.console {
font: normal normal 90% Courier New,
monotype;
padding:0px;
margin:0px;
}
div.content, div.header {
background: #ffffff;
border: dotted
1px #666;
margin: 2px;
content:
2px;
padding: 2px;
}
table.border, th.border, td.border {
border:
1px solid black;
border-collapse:collapse;
}
</style>
</head>
<body>
<div class="header">
<j:set var="spc" value="&nbsp;&nbsp;" />
<!-- GENERAL INFO -->
<table>
<tr class="gray">
<td align="right">
<j:choose>
<j:when test="${build.result=='SUCCESS'}">
<img src="${rooturl}static/e59dfe28/images/32x32/blue.gif" />
</j:when>
<j:when test="${build.result=='FAILURE'}">
<img src="${rooturl}static/e59dfe28/images/32x32/red.gif" />
</j:when>
<j:otherwise>
<img
src="${rooturl}static/e59dfe28/images/32x32/yellow.gif" />
</j:otherwise>
</j:choose>
</td>
<td valign="center">
<b style="font-size: 200%;">BUILD ${build.result}</b>
</td>
</tr>
<tr>
<td>构建地址</td>
<td>
<a href="${rooturl}${build.url}">${rooturl}${build.url}</a>
</td>
</tr>
<tr>
<td>项 目:</td>
<td>${project.name}</td>
</tr>
<tr>
<td>构建日期:</td>
<td>${it.timestampString}</td>
</tr>
<tr>
<td>构建时长:</td>
<td>${build.durationString}</td>
</tr>
<tr>
<td>Build cause:</td>
<td>
<j:forEach var="cause" items="${build.causes}">${cause.shortDescription}
</j:forEach>
</td>
</tr>
<tr>
<td>Build description:</td>
<td>${build.description}</td>
</tr>
<tr>
<td>Built on:</td>
<td>
<j:choose>
<j:when test="${build.builtOnStr!=''}">${build.builtOnStr}</j:when>
<j:otherwise>master</j:otherwise>
</j:choose>
</td>
</tr>
</table>
</div>
<!-- COBERTURA TEMPLATE -->
<j:set var="coberturaAction" value="${it.coberturaAction}" />
<j:if test="${coberturaAction!=null}">
<div class="content">
<j:set var="coberturaResult" value="${coberturaAction.result}" />
<j:if test="${coberturaResult!=null}">
<a href="${rooturl}${build.url}/cobertura">
<h1>Cobertura报告</h1>
</a>
<h2>趋势</h2>
<img src="http://*.*.*.*:****/${build.url}../cobertura/graph" width="600" height="200px" />
<h2>项目覆盖率汇总</h2>
<table class="border">
<tr>
<th class="border">Name</th>
<j:forEach var="metric" items="${coberturaResult.metrics}">
<th class="border">${metric.name}</th>
</j:forEach>
</tr>
<tr>
<td class="border">${coberturaResult.name}</td>
<j:forEach var="metric" items="${coberturaResult.metrics}">
<!--
<td class="border"
data="${coberturaResult.getCoverage(metric).percentageFloat}">${coberturaResult.getCoverage(metric).percentage}%
(${coberturaResult.getCoverage(metric)})
</td>
-->
<td class="border">
<div style="background-color:#ff9090;width:100px;height:20px;">
<div style="background-color:#80ff80;width: ${coberturaResult.getCoverage(metric).percentage}px;height:20px;">
<span style="text-align:center;position:absolute;width:100px;" vertical-align="middle">
${coberturaResult.getCoverage(metric).percentage}%(${coberturaResult.getCoverage(metric)})
</span>
</div>
</div>
</td>
</j:forEach>
</tr>
</table>
<j:if test="${coberturaResult.sourceCodeLevel}">
<h2>Source</h2>
<j:choose>
<j:when test="${coberturaResult.sourceFileAvailable}">
<div style="overflow-x:scroll;">
<table class="source">
<thead>
<tr>
<th colspan="3">${coberturaResult.relativeSourcePath}
</th>
</tr>
</thead>
${coberturaResult.sourceFileContent}
</table>
</div>
</j:when>
<j:otherwise>
<p>
<i>Source code is unavailable</i>
</p>
</j:otherwise>
</j:choose>
</j:if>
<j:forEach var="element" items="${coberturaResult.childElements}">
<j:set var="childMetrics"
value="${coberturaResult.getChildMetrics(element)}" />
<h2>Coverage Breakdown by ${element.displayName}</h2>
<table class="border">
<tr>
<th class="border">Name</th>
<j:forEach var="metric" items="${childMetrics}">
<th class="border">${metric.name}</th>
</j:forEach>
</tr>
<j:forEach var="c" items="${coberturaResult.children}">
<j:set var="child" value="${coberturaResult.getChild(c)}" />
<tr>
<td class="border">
${child.xmlTransform(child.name)}
</td>
<j:forEach var="metric" items="${childMetrics}">
<j:set var="childResult" value="${child.getCoverage(metric)}" />
<j:choose>
<j:when test="${childResult!=null}">
<!--
<td class="border" data="${childResult.percentageFloat}">${childResult.percentage}%
(${childResult})
</td>
-->
<td class="border" data="${childResult.percentageFloat}">
<div style="background-color:#ff9090;width:100px;height:20px;">
<div style="background-color:#80ff80;width: ${childResult.percentage}px;height:20px;">
<span style="text-align:center;position:absolute;width:100px;">
${childResult.percentage}%(${childResult})
</span>
</div>
</div>
</td>
</j:when>
<j:otherwise>
<td class="border" data="101">N/A</td>
</j:otherwise>
</j:choose>
</j:forEach>
</tr>
</j:forEach>
</table>
</j:forEach>
</j:if>
<br />
</div>
</j:if>
<!-- 覆盖率趋势统计 -->
<!--
<j:set var="coberturaAction" value="${it.coberturaAction}" />
<j:if test="${coberturaAction!=null}">
<div class="content">
<j:set var="coberturaResult" value="${coberturaAction.result}" />
<j:if test="${coberturaResult!=null}">
<a href="${rooturl}${build.url}/cobertura">
<h1>覆盖率趋势</h1>
</a>
<j:forEach var="metric" items="${coberturaResult.metrics}">
<table style="padding: 0 10px; width:480px;">
<tbody>
<tr>
<th align="left">${metric.name}</th>
<td align="right"
data="${coberturaResult.getCoverage(metric).percentageFloat}">${coberturaResult.getCoverage(metric).percentage}%
(${coberturaResult.getCoverage(metric)})
</td>
</tr>
</tbody>
</table>
<table style="height: 3px; padding: 0 10px; width:480px;">
<tbody>
<tr>
<td width="${coberturaResult.getCoverage(metric).percentage}%" height="20" style="background-color:#bfb;">
</td>
<td style="background-color:#fdd;">
</td>
</tr>
</tbody>
</table>
</j:forEach>
</j:if>
<br />
</div>
</j:if>
-->
<!-- HEALTH TEMPLATE -->
<div class="content">
<j:set var="healthIconSize" value="16x16" />
<j:set var="healthReports" value="${project.buildHealthReports}" />
<j:if test="${healthReports!=null}">
<h1>健康报告</h1>
<table>
<tr>
<th>W</th>
<th>描述</th>
<th>分数</th>
</tr>
<j:forEach var="healthReport" items="${healthReports}">
<tr>
<td>
<img
src="${rooturl}${healthReport.getIconUrl(healthIconSize)}" />
</td>
<td>${healthReport.description}</td>
<td>${healthReport.score}</td>
</tr>
</j:forEach>
</table>
<br />
</j:if>
</div>
<!-- CHANGE SET -->
<div class="content">
<j:set var="changeSet" value="${build.changeSet}" />
<j:if test="${changeSet!=null}">
<j:set var="hadChanges" value="false" />
<a href="${rooturl}${build.url}/changes">
<h1>变更集</h1>
</a>
<j:forEach var="cs" items="${changeSet.logs}"
varStatus="loop">
<j:set var="hadChanges" value="true" />
<h2>${cs.msgAnnotated}</h2>
<p>
by
<em>${cs.author}</em>
</p>
<table>
<j:forEach var="p" items="${cs.affectedFiles}">
<tr>
<td width="10%">${spc}${p.editType.name}</td>
<td>
<tt>${p.path}</tt>
</td>
</tr>
</j:forEach>
</table>
</j:forEach>
<j:if test="${!hadChanges}">
<p>无</p>
</j:if>
<br />
</j:if>
</div>
<!-- ARTIFACTS -->
<j:set var="artifacts" value="${build.artifacts}" />
<j:if test="${artifacts!=null and artifacts.size()>0}">
<div class="content">
<h1>Build Artifacts</h1>
<ul>
<j:forEach var="f" items="${artifacts}">
<li>
<a href="${rooturl}${build.url}artifact/${f}">${f}</a>
</li>
</j:forEach>
</ul>
</div>
</j:if>
<!-- MAVEN ARTIFACTS -->
<j:set var="mbuilds" value="${build.moduleBuilds}" />
<j:if test="${mbuilds!=null}">
<div class="content">
<h1>Build Artifacts</h1>
<j:forEach var="m" items="${mbuilds}">
<h2>${m.key.displayName}</h2>
<j:forEach var="mvnbld" items="${m.value}">
<j:set var="artifacts" value="${mvnbld.artifacts}" />
<j:if test="${artifacts!=null and artifacts.size()>0}">
<ul>
<j:forEach var="f" items="${artifacts}">
<li>
<a href="${rooturl}${mvnbld.url}artifact/${f}">${f}</a>
</li>
</j:forEach>
</ul>
</j:if>
</j:forEach>
</j:forEach>
<br />
</div>
</j:if>
<!-- JUnit TEMPLATE -->
<j:set var="junitResultList" value="${it.JUnitTestResult}" />
<j:if test="${junitResultList.isEmpty()!=true}">
<div class="content">
<a href="${rooturl}${build.url}/testReport">
<h1>单元测试</h1>
</a>
<table class="border">
<tr>
<th class="border">包路径</th>
<th class="border">失败</th>
<th class="border">通过</th>
<th class="border">跳过</th>
<th class="border">总计</th>
</tr>
<j:forEach var="junitResult" items="${it.JUnitTestResult}">
<j:forEach var="packageResult" items="${junitResult.getChildren()}">
<tr>
<td class="border">
<tt>${packageResult.getName()}</tt>
</td>
<td class="border test_failed">${packageResult.getFailCount()}</td>
<td class="border test_passed">${packageResult.getPassCount()}</td>
<td class="border test_skipped">${packageResult.getSkipCount()}</td>
<td class="border">
<b>${packageResult.getPassCount()+packageResult.getFailCount()+packageResult.getSkipCount()}
</b>
</td>
</tr>
<j:forEach var="failed_test"
items="${packageResult.getFailedTests()}">
<tr>
<td class="test_failed" colspan="5">
<tt>${failed_test.getFullName()}</tt>
</td>
</tr>
</j:forEach>
</j:forEach>
</j:forEach>
</table>
<br />
</div>
</j:if>
<!-- Static Analysis -->
<j:set var="actions" value="${it.staticAnalysisActions}" />
<j:if test="${!actions.isEmpty()}">
<div class="content">
<h1>Static Analysis Results</h1>
<table>
<tr>
<th></th>
<th>Name</th>
<th>Result</th>
<th>Total</th>
<th>High</th>
<th>Normal</th>
<th>Low</th>
</tr>
<j:forEach var="action" items="${actions}">
<tr>
<td>
<img src="${rooturl}${action.smallImageName}" />
</td>
<td>
<a href="${rooturl}${build.url}/${action.urlName}">${action.displayName}</a>
</td>
<td class="center">
<j:choose>
<j:when test="${action.result.pluginResult=='SUCCESS'}">
<img src="${rooturl}static/e59dfe28/images/16x16/blue.gif" />
</j:when>
<j:when test="${action.result.pluginResult=='FAILURE'}">
<img src="${rooturl}static/e59dfe28/images/16x16/red.gif" />
</j:when>
<j:otherwise>
<img src="${rooturl}static/e59dfe28/images/16x16/yellow.gif" />
</j:otherwise>
</j:choose>
</td>
<td class="center">${action.result.numberOfAnnotations} </td>
<td class="center">${action.result.getNumberOfAnnotations('HIGH')} </td>
<td class="center">${action.result.getNumberOfAnnotations('NORMAL')} </td>
<td class="center">${action.result.getNumberOfAnnotations('LOW')} </td>
</tr>
</j:forEach>
</table>
</div>
</j:if>
</body>
</html>
</j:jelly>