单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
可以说,单元测试(模块测试)是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。通常而言,一个单元测试是用于判断某个特定条件(或者场景)下某个特定函数的行为。可见,单元测试是由程序员自己来完成,最终受益的也是程序员自己。可以这么说,程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。
对于程序员来说,如果养成了对自己写的代码进行单元测试的习惯,不但可以写出高质量的代码,而且还能提高编程水平。
接下来主要介绍Junit3,Junit4单元测试方案,以及mock测试框架,Spring单元测试框架。我们先来看Junit3单元测试方案,代码如下:
package com.itszt.domain;
import java.util.Date;
/**
* 单元测试用的实体类
*/
public class Order {
private int orderId;//订单order的id
private Date orderTime;//下订单时间
public Order(int orderId, Date orderTime) {
this.orderId = orderId;
this.orderTime = orderTime;
}
@Override
public String toString() {
return "Order{" +
"orderId=" + orderId +
", orderTime=" + orderTime +
'}';
}
public Order() {
}
public int getOrderId() {
return orderId;
}
public void setOrderId(int orderId) {
this.orderId = orderId;
}
public Date getOrderTime() {
return orderTime;
}
public void setOrderTime(Date orderTime) {
this.orderTime = orderTime;
}
}
-------------------------------------------------------------
package com.itszt.dao;
import com.itszt.domain.Order;
import java.util.*;
/**
* 测试单元,操作订单的类
*/
public class OrderDao {
private static int moneyNum=100;
private static Map<String,List<Order>> allOrders=new HashMap<>();
private static int id=1;
static {
List<Order> orders=new ArrayList<>();
orders.add(new Order(id++,new Date()));
orders.add(new Order(id++,new Date()));
orders.add(new Order(id++,new Date()));
allOrders.put("张三",orders);//属于张三的订单
}
public int queryOrderCount(String username){
//用户所下订单的数量
return allOrders.get(username).size();
}
public List<Order> queryOrders(String username){
//用户下过的订单
return allOrders.get(username);
}
public int queryMoney(){
return moneyNum;
}
public void addMoney(int num){
moneyNum=moneyNum+num;
}
public void dropMoney(int num){
moneyNum=moneyNum-num;
}
}
------------------------------------------------------
package test1;
import com.itszt.dao.OrderDao;
import com.itszt.domain.Order;
import junit.framework.TestCase;
import java.util.List;
/**
* Junit3,创建单元测试用例
* 注:Junit4用注解完成
*/
public class TestOrderDao extends TestCase{
private OrderDao orderDao;
private static int num=0;
@Override
public void setUp() throws Exception {
System.out.println((++num)+"--------========================");
// setUp:在每个测试方法前都会执行,做通用初始化
// super.setUp();
orderDao=new OrderDao();
System.out.println("test1.TestOrderDao.setUp");
}
@Override
public void tearDown() throws Exception {
// super.tearDown();
System.out.println("test1.TestOrderDao.tearDown");
}
public void testQueryMoney(){
System.out.println("test1.TestOrderDao.testQueryMoney");
int queryMoney = orderDao.queryMoney();
System.out.println("queryMoney = " + queryMoney);
//传入一个期望值,再传入一个真实值,看两者是否相等
assertEquals(100,queryMoney);
}
public void testAddMoney(){
System.out.println("test1.TestOrderDao.testAddMoney");
orderDao.addMoney(100);
int queryMoney = orderDao.queryMoney();
System.out.println("queryMoney = " + queryMoney);
assertEquals(200,queryMoney);
}
public void testDropMoney(){
System.out.println("TestOrderDao.testDropMoney");
orderDao.dropMoney(35);
int queryMoney = orderDao.queryMoney();
System.out.println("queryMoney = " + queryMoney);
assertEquals(165,queryMoney);
}
public void testQueryOrderCount(){
System.out.println("TestOrderDao.testQueryOrderCount");
int orderCount = orderDao.queryOrderCount("张三");
System.out.println("orderCount = " + orderCount);
assertEquals(3,orderCount);
}
public void testQueryOrders(){
System.out.println("TestOrderDao.testQueryOrders");
List<Order> orderList = orderDao.queryOrders("张三");
System.out.println("orderList = " + orderList);
assertEquals(orderList,orderList);
}
}
-------------------------------------------------------
package test1;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
/**
*单元测试套件,运行测试用例
*/
public class TestAll extends TestCase{
public static Test suite(){
TestSuite testSuite=new TestSuite();
testSuite.addTestSuite(TestOrderDao.class);
return testSuite;
}
}
上述代码执行结果如下:
1--------========================
test1.TestOrderDao.setUp
TestOrderDao.testQueryOrders
orderList = [Order{orderId=1, orderTime=Wed Mar 21 14:43:51 CST 2018}, Order{orderId=2, orderTime=Wed Mar 21 14:43:51 CST 2018}, Order{orderId=3, orderTime=Wed Mar 21 14:43:51 CST 2018}]
test1.TestOrderDao.tearDown
2--------========================
test1.TestOrderDao.setUp
test1.TestOrderDao.testQueryMoney
queryMoney = 100
test1.TestOrderDao.tearDown
3--------========================
test1.TestOrderDao.setUp
test1.TestOrderDao.testAddMoney
queryMoney = 200
test1.TestOrderDao.tearDown
4--------========================
test1.TestOrderDao.setUp
TestOrderDao.testDropMoney
queryMoney = 165
test1.TestOrderDao.tearDown
5--------========================
test1.TestOrderDao.setUp
TestOrderDao.testQueryOrderCount
orderCount = 3
test1.TestOrderDao.tearDown
总的来说,在使用Junit3单元测试时,步骤如下:
先导入相关jar包;
新建单元测试用例,如:public class TestUserDao extends TestCase;
其中,setUp:在每个测试方法前都会执行,做通用初始化;
tearDown:在每个测试方法后都会执行,做通用资源释放。
写测试方案:其实就是一堆方法,这些方法通常以test开头即可;
怎么判断功能是否正常:基于Assert 断言完成;
测试结果通常是3种: 1.成功 2.失败 3.异常报错
单元测试套件:一次执行多个测试类
上面用的是Junit3测试方案,Junit4与Junit3不同的是,采用了注解方式,从而使得测试代码更为简洁。我们接下来看Junit4测试方案:
package com.itszt.dao;
/**
* 测试单元
*/
public class UserDao {
private static int moneyNum=100;
public int queryMoney(){
System.out.println(10/0);//模拟出现异常
return moneyNum;
}
public void addMoney(int num){
moneyNum+=num;
}
public void dropMoney(int num){
moneyNum-=num;
}
}
----------------------------------------------------------
package test2;
import com.itszt.dao.UserDao;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
/**
* 测试用例,通过注解声明
*/
public class TestUserDao {
private UserDao userDao;
@Before
public void init(){
userDao=new UserDao();
System.out.println("TestUserDao.init");
}
@After
public void tearDown(){
System.out.println("TestUserDao.tearDown");
}
@Test
public void testQueryMoney(){
System.out.println("TestUserDao.testQueryMoney "+userDao.hashCode());
int money = userDao.queryMoney();
Assert.assertEquals(100,money);
}
@Test
public void testAddMoney(){
System.out.println("TestUserDao.testAddMoney "+userDao.hashCode());
userDao.addMoney(300);
int money = userDao.queryMoney();
Assert.assertEquals(400,money);
}
}
----------------------------------------------------
package test2;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
/**
* 测试套件
* 测试用例多于一个时,中间以英文逗号分割
*/
@Suite.SuiteClasses({TestUserDao.class})
@RunWith(Suite.class)
public class TestAll {
}
上述代码执行结果如下:
TestUserDao.init
TestUserDao.testQueryMoney 25282035
TestUserDao.tearDown
java.lang.ArithmeticException: / by zero
at com.itszt.dao.UserDao.queryMoney(UserDao.java:9)
at test2.TestUserDao.testQueryMoney(TestUserDao.java:28)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runners.Suite.runChild(Suite.java:128)
at org.junit.runners.Suite.runChild(Suite.java:27)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:237)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
TestUserDao.init
TestUserDao.testAddMoney 4959864
TestUserDao.tearDown
java.lang.ArithmeticException: / by zero
at com.itszt.dao.UserDao.queryMoney(UserDao.java:9)
at test2.TestUserDao.testAddMoney(TestUserDao.java:35)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runners.Suite.runChild(Suite.java:128)
at org.junit.runners.Suite.runChild(Suite.java:27)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:237)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
可见,代码出现异常后,单元测试随之会给出相应的异常提示。
我们接下来用mock方法执行单元测试。首先,我们要在项目中添加easymock-3.4.jar包的依赖。测试代码如下:
package com.itszt.domain;
/**
* 测试用的实体类
*/
public class Org {
private String orgName,orgLoaction,orgType;
public Org(String orgName, String orgLoaction, String orgType) {
this.orgName = orgName;
this.orgLoaction = orgLoaction;
this.orgType = orgType;
}
public Org() {
}
public String getOrgName() {
return orgName;
}
public void setOrgName(String orgName) {
this.orgName = orgName;
}
public String getOrgLoaction() {
return orgLoaction;
}
public void setOrgLoaction(String orgLoaction) {
this.orgLoaction = orgLoaction;
}
public String getOrgType() {
return orgType;
}
public void setOrgType(String orgType) {
this.orgType = orgType;
}
@Override
public String toString() {
return "Org{" +
"orgName='" + orgName + '\'' +
", orgLoaction='" + orgLoaction + '\'' +
", orgType='" + orgType + '\'' +
'}';
}
}
----------------------------------------------------
package com.itszt.dao;
import com.itszt.domain.Org;
/**
* 操作Org对象的接口
*/
public interface OrgDao {
//1.查重
public Org queryOrgByName(String orgName);
}
--------------------------------------------------------
package com.itszt.service;
/**
* 操作Org业务的接口
*/
public interface OrgService {
public boolean regOrg(String orgName,String orgLoaction,String orgType);
}
----------------------------------------------------------
package com.itszt.service;
import com.itszt.dao.OrgDao;
import com.itszt.domain.Org;
/**
* OrgService的实现类
*/
public class OrgServiceImpl implements OrgService {
private OrgDao orgDao;
public void setOrgDao(OrgDao orgDao) {
this.orgDao = orgDao;
}
@Override
public boolean regOrg(String orgName, String orgLoaction, String orgType) {
Org org = orgDao.queryOrgByName(orgName);
if (org == null) {
System.out.println(orgName+"不存在,可以执行插入动作。");
} else {
System.out.println(orgName+"已存在,不能再插入");
return false;
}
return true;
}
}
-------------------------------------------------------
package test4;
import com.itszt.dao.OrgDao;
import com.itszt.domain.Org;
import com.itszt.service.OrgServiceImpl;
import org.easymock.EasyMock;
import org.junit.Assert;
import org.junit.Test;
/**
* 用mock方案实现单元测试
*/
public class TestOrgService {
@Test
public void testRegOrg(){
OrgServiceImpl orgService=new OrgServiceImpl();
//用mock模拟一个UserDao的实现类
OrgDao orgDao = EasyMock.createMock(OrgDao.class);
orgService.setOrgDao(orgDao);
//待测试的一个实体类
Org org = new Org("曹操公司", "许昌", "魏国");
//当orgDao调用queryOrgByName方法,并且传入参数为"曹操公司"时,则返回org对象,该模拟对象orgDao可使用次数为10
//传入期望值,实际结果为Org对象或null
EasyMock.expect(orgDao.queryOrgByName("曹操公司")).andReturn(org).times(10);
EasyMock.expect(orgDao.queryOrgByName("刘备公司")).andReturn(null).times(10);
//让我们模拟的特性生效
EasyMock.replay(orgDao);
boolean boo1 = orgService.regOrg("曹操公司", "许昌", "魏国");
Assert.assertFalse(boo1);
boolean boo2 = orgService.regOrg("刘备公司", "成都", "蜀汉");
Assert.assertTrue(boo2);
}
}
上述代码执行如下:
曹操公司已存在,不能再插入
刘备公司不存在,可以执行插入动作。
我们接下来再写一个基于mock的单元测试方案:
package com.itszt.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* 测试单元
*/
public class UserController {
public void showUser(HttpServletRequest request, HttpSession session) {
String username = request.getParameter("username");
System.out.println("简单使用一下---username = " + username);
String userpwd = request.getAttribute("userpwd").toString();
System.out.println("简单使用一下---userpwd = " + userpwd);
String userNow = session.getAttribute("userNow").toString();
System.out.println("简单使用一下---userNow = " + userNow);
}
}
-------------------------------------------------
package test3;
import com.itszt.controller.UserController;
import org.easymock.EasyMock;
import org.junit.Test;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* mock单元测试方案
*/
public class TestUserController {
@Test
public void testShow(){
UserController userController=new UserController();
HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
HttpSession session = EasyMock.createMock(HttpSession.class);
String username="小明";
String userpwd="123456";
String userNow=username;
EasyMock.expect(request.getParameter("username")).andReturn(username).times(1);
EasyMock.expect(request.getAttribute("userpwd")).andReturn(userpwd).once();
EasyMock.expect(session.getAttribute("userNow")).andReturn("正在登录的是:"+userNow).once();
EasyMock.replay(request);
EasyMock.replay(session);
userController.showUser(request,session);
}
}
上述代码执行结果如下:
简单使用一下---username = 小明
简单使用一下---userpwd = 123456
简单使用一下---userNow = 正在登录的是:小明
最后,我们在使用Spring框架时,Spring也为我们提供了单元测试方案。在使用时,我们需要添加Spring框架中相应的jar包(spring-test-4.3.11.RELEASE.jar)依赖。
由于我们在案例中使用了注解,所以在spring-config.xml配置文件中要添加注解支持,配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--添加注解支持-->
<context:annotation-config></context:annotation-config>
<context:component-scan base-package="test4"></context:component-scan>
</beans>
接下来是基于Spring框架的单元测试代码:
package test4;
import org.springframework.stereotype.Component;
/**
* 测试单元
*/
@Component
public class UserService {
public int queryScore(){
System.out.println("100就对了");
return 100;
}
}
---------------------------------------------------
package test4;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* 测试用例与套件
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:config/spring-config.xml")
public class TestUserService {
@Autowired
private UserService userService;
@Before
public void before(){
System.out.println("运行前测试....");
}
@After
public void after(){
System.out.println("运行完成后测试...");
}
@Test
public void testQueryScore(){
System.out.println("TestUserService.testQueryScore");
int queryScore = userService.queryScore();
org.junit.Assert.assertEquals(100,100);
}
}
上述代码运行如下:
运行前测试....
TestUserService.testQueryScore
100就对了
运行完成后测试...