Java单元测试
单元测试(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就对了 运行完成后测试...