浅谈单元测试

单元测试有什么用

提高整个团队的工作效率。

有质量的代码是对整个团队的贡献

提高程序员的责任心和代码的质量。

通过单元测试,保证自己的代码是可靠的和完整的,这是对自己负责,也是对团队负责

获得一个底层模块的回归测试工具

在不破坏现有功能的基础上持续改进设计

逼着你在写代码之前做好计划,促成更好的设计,包括代码编写水平。

降低Bug修复的成本

名词解释

单元测试:是对软件基本组成单元进行的测试,是属于白盒测试的范畴,通过对代码的逻辑结构进行分析来设计测试用例。

回归测试:在发生修改之后重新测试先前的测试以保证修改的正确性。

冒烟测试:由开发人员花费较少的时间保证功能至少能运行。

桩模块(Stub):模拟第三方的“替身”模块,参数及返回信息。

驱动模块:模拟用户操作,及产生的数据,启用被测模块将数据传送给被测模块,对返回信息结果做预期判断。

非渐增式测试:单独对每个模块进行测试, 最后在将所有模块连接到一起来测试。

渐增式测试:将要测试的模块和已经测试的模块放在一起来测试。

单元测试要求

测试需求文档。

单元测试由最熟悉代码的人(程序的编写人)来写。

保证单元测试的独立性,即测试用例不能相互依赖。

单元测试要保证产生的测试数据不会对系统造成影响,一般建议对测试中产生的数据进行回滚,以免影响系统正常数据。

测试粒度,一般要求对所有方法进行测试(不包括get,set方法)。

必须在测试中使用断言来判定结果是否达到预期目标。

需记录测试结果,有结果文档,根据测试需求的不同,有选择性的记录,并对结果进行分析,并进行调试。

在需求变更时,需做回归测试,以保证不产生新的bug。

在和第三方牵扯时,需编写第三方的桩模块,参数、逻辑、数据、返回值进行模拟,达到相同的效果。

个人建议在方法参数及名称定义下来后先编写测试用例,再去实现功能逻辑。

规范说明

测试类名称:要被测试类的列名称+Test ,如—> 被测类名称为BaseBiz,则测试类名称为BaseBizTest

测试方法名:test+被测方法名称, 如----> 被测方法名称为getUser,则测试方法名称为testGetUser

测试包名称:被测试类的包名是com.yunat.channel, 测试类的包名就为test.com.yunat.channel, maven 管理的工程就在src/test/java下建立相同包路径。

选择测试框架

根据不同的要求选择,如果是对数据库做测试的,可以选择dbunit、junit, 业务框架测试选择spring-test、junit 测试框架,下面是pom依赖:

<dependency>
	<groupId>junit</groupId>
	<artifactId>junit</artifactId>
	<version>4.10</version>
	<scope>test</scope>
</dependency>
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-test</artifactId>
	<version>3.1.1.RELEASE</version>
	<scope>test</scope>
</dependency>

<dependency>
	<groupId>org.dbunit</groupId>
	<artifactId>dbunit</artifactId>
	<version>2.4.7</version>
	<scope>test</scope>
</dependency>

介绍junit4.10框架中常用的annotation

@BeforeClass: 被其标注的方法必须为static方法,在测试类对象加载时就执行。

@AfterClass:被其标注的方法必须为static方法,在测试类所有方法执行完毕后执行。

@Before:在每个测试方法前执行。

@After:在每个测试方法之后执行。

@Ignore:标注该测试方法被忽略执行。

@Test:测试方法标注为驱动。

@RunWith:测试的运行器,集合运行器Suite,参数化运行器等。

@SuiteClasses:测试运行器的类型参数,放自己测试类,综合运行, 即集合测试。

较为详细的介绍请搜索:junit_使用指南及作业规范(对比Junit3与4).pdf。

Parameterized 参数化运行器使用

Parameterized 的使用demo:

被测试类:

public class WordDealUtil {
    public static Object wordFormat4DB(String target) {
        if (target==null) return null;
        if ("employeeInfo".equals(target)) return "employee_info";
        if ("".equals(target)) return "";
        if ("EmployeeInfo".equals(target)) return "employee_info";
        if ("employeeInfoA".equals(target)) return "employee_info_a";
        if ("employeeAInfo".equals(target)) return "employee_a_info";
        return null;
    }
}

测试用例:

/**
 * 针对不同参数产生不同结果
 *
 * @author ming.peng
 * @date 2012-12-27
 */
@RunWith(Parameterized.class)
public class WordDealUtilTest {
	private String expected;
	private String target;
	@Parameters
	public static Collection<Object[]> words() {
		return Arrays.asList(new Object[][] {
				{ "employee_info", "employeeInfo" }, // 测试一般的处理情况
				{ null, null }, // 测试 null 时的处理情况
				{ "", "" }, // 测试空字符串时的处理情况
				{ "employee_info", "EmployeeInfo" }, // 测试当首字母大写时的情况
				{ "employee_info_a", "employeeInfoA" }, // 测试当尾字母为大写时的情况
				{ "employee_a_info", "employeeAInfo" } // 测试多个相连字母大写时的情况
				});
	}
	/**
	 * 参数化测试必须的构造函数
	 *
	 * @param expected
	 *            期望的测试结果,对应参数集中的第一个参数
	 * @param target
	 *            测试数据,对应参数集中的第二个参数
	 */
	public WordDealUtilTest(String expected, String target) {
		this.expected = expected;
		this.target = target;
	}
	/**
	 * 测试将 Java 对象名称到数据库名称的转换
	 */
	@Test
	public void wordFormat4DB() {
		assertEquals(expected, WordDealUtil.wordFormat4DB(target));
	}
}

8、spring-test 框架的使用

由于我们的系统框架,大部分是使用spring的框架,而junit对框架的测试不是很方便,所以选择spring-test与junit测试框架并用,spring-test 是在junit框架基础上做的拓展。

classpath两种用法:

  1. classpath*:applicationContext.xml会加入本项目及所有jar包根目录下的 applicationContext.xml文件,跨jar包依赖时使用。

  2. classpath:applicationContext.xml只加入本项目根目录下的
    applicationContext.xml文件,不依赖其它jar包的配置文件时推荐这样写,以避免冲突。

其中常用的annotation

@ContextConfiguration:配置spring的读取资源

@Transactional: 事务声明

@TransactionConfiguration:事务配置

@Rollback:事务回滚配置

spring业务组件测试示例

package com.yunat.devtwo.biz;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.yunat.devtwo.dao.BaseDao;
/**
 * 业务测试组件
 *
 * @author ming.peng
 * @date 2012-12-26
 * @since 2.2.0
 */
@Component("BaseBiz")
public class BaseBiz {
	@Autowired
	private BaseDao baseDao;
	public Map<String, Object> getUser(String userName){
		Map<String, Object> parameter = new HashMap<String, Object>(1);
		parameter.put("userName", userName);
		Map<String, Object> user = baseDao.getSingleRow("Common.getUser", parameter);
		return user;
	}
	public Integer saveSysConfig(){
		Map<String, Object> sysConfig = new HashMap<String, Object>();
		sysConfig.put("name", "gateway_balance");
		sysConfig.put("desc", "通道账户初始化金额");
		sysConfig.put("value", "10000");
		return baseDao.save("Common.saveSysConfig", sysConfig);
	}
}

对上面的业务组件在使用annotation配置测试用例的时候,非常简单,如下:

package com.yunat.devtow.biz;
import java.util.Map;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Transactional;
import com.yunat.devtwo.biz.BaseBiz;
/**
 * 业务组件测试
 *
 *
 * @author ming.peng
 * @date 2012-12-27
 * @since 2.2.0
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:spring/applicationContext.xml" })
@Transactional
@TransactionConfiguration(defaultRollback=false, transactionManager="txManager")
public class BaseBizTest {
	protected Logger logger = LoggerFactory.getLogger(this.getClass());
	@Autowired
	private BaseBiz baseBiz;
	@Test
	public void testGetUser(){
		Map<String, Object> user = baseBiz.getUser("qiushi");
		logger.info(user.toString());
		Assert.assertEquals(77800, user.get("group_id"));
	}
	@Test
	public void testSaveSysConfig(){
		Integer result = baseBiz.saveSysConfig();
		Assert.assertEquals(result.intValue(), 1);
	}
}

spring-test提供了对web层非常简便的测试, 下面针对spring-mvc 的测试用例:

package com.yunat.devtwo.controller;
import java.io.UnsupportedEncodingException;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import com.yunat.devtwo.biz.BaseBiz;

/**
 *
 * spring mvc controller
 *
 * @author ming.peng
 * @date 2012-12-27
 * @since 2.2.0
 */
@Controller
@RequestMapping("ccmsController")
public class CcmsController {
	private Logger logger = Logger.getLogger(this.getClass());
	@Autowired
	private BaseBiz baseBiz;
	/**
	 * 进去账户管理界面
	 * @param userName
	 * @param userInfo
	 * @return
	 */
	@RequestMapping("accountManager")
	public ModelAndView accountManager(String userName, String userInfo, HttpSession session) {
		logger.info("===================>>>>进入ccms账户管理中心,用户名:"+userName);
		ModelAndView mav = new ModelAndView("ccms/account");
		mav.addObject("userName", userName);
		mav.addObject("userInfo", userInfo);
		System.out.println(session.getAttribute("abc"));
		Map<String, Object> user = baseBiz.getUser(userName);
		logger.info("user:" + user);
		baseBiz.saveSysConfig();
		logger.info("===================>>>>正常进入ccms账户管理中心,用户名:"+userName);
		session.setAttribute("userName", userName);
		return mav;
	}
}

对web层组件在使用annotation配置测试的时候,也是非常简单,如下:

package com.yunat.devtow.controller;
import javax.servlet.http.HttpSession;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
 * CcmsControlle 测试
 *
 *
 * @author ming.peng
 * @date 2012-12-27
 * @since 2.2.0
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:spring/applicationContext.xml", "classpath:spring/springmvc-servlet.xml" })
@Transactional
@TransactionConfiguration(defaultRollback = false, transactionManager = "txManager")
public class CcmsControllerTest {
	@Autowired
	private RequestMappingHandlerAdapter handlerAdapter;
	@Autowired
	private RequestMappingHandlerMapping handlerMapping;
	private static MockHttpServletRequest request;
	private static MockHttpServletResponse response;
	@BeforeClass
	public static void before() {
		request = new MockHttpServletRequest();
		request.setCharacterEncoding("UTF-8");
		response = new MockHttpServletResponse();
	}
	@Test
	@Rollback(true)
	public void testAccountManager() {
		request.setRequestURI("/ccmsController/accountManager");
		request.setMethod(HttpMethod.POST.name());
		request.setParameter("userName", "qiushi");
		request.setParameter("userInfo", "abjiozjdlfjaosdifjoaisdfji");
		HttpSession session = request.getSession();
		session.setAttribute("abc", "dbc");
		ModelAndView mv = null;
		try {
			Object handler = handlerMapping.getHandler(request).getHandler();
			mv = handlerAdapter.handle(request, response, handler);
		} catch (Exception e) {
			e.printStackTrace();
		}
		Assert.assertNotNull(mv);
		Assert.assertEquals(response.getStatus(), 200);
		Assert.assertEquals(mv.getViewName(), "ccms/account");
	}
}

这里框架的选择主要是针对我们系统常用的一些框架选择专门的测试框架已简化我们日常测试工作,提高工作效率。

最后

希望文章有帮助到大家,如有其他问题,也欢迎大家前来交流和补充(关注微信公众号:程序媛木子来领取海量软件测试资源(有清晰的思路,有的时候比确切的答案更重要),分享更多技术、面试资料,大家也可以加入qq(644956177)群里还有同行一起交流技术。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值