测试是一种思维,包括情感思维和智力思维,情感思维主要体现在一句俗语:思想决定行动上(要怀疑一切),智力思维主要体现在测试用例的设计上。具有了这样的思想,就会找出更多的bug。(^_^个人认为,不代表官方立场)
对于一个web网站来说,主要从这么几个大的方面来进行测试:
1、 功能测试;2、 界面测试;3、 易用性测试;4、兼容性测试;5、 链接测试;6、 业务流程测试;7、 安全性测试
下面主要从以上七个方面进行叙述:
一、功能测试
测试用例是测试的核心,测试用例的设计是一种思维方式的体现,在用例的设计中,用的比较多的方法是边界值分析法和等价类划分法,下面主要从输入框,搜索功能,添加、修改功能,删除功能,注册、登录功能以及上传图片功能等11个方面进行总结说明。
1、输入框
输入框是测试中最容易出现bug的地方,所以在测试时,一定要多加注意。
2、搜索功能
(1)比较长的名称是否能查到?
(2)空格 或空
(3)名称中含有特殊字符,如:' $ % & *以及空格等
(4)关键词前面或后面有空格
(5)如果支持模糊查询,搜索名称中任意一个字符是否能搜索到
(6)输入系统中不存在与之匹配的条件
(7)两个查询条件是否为2选1,来回选择是否出现页面错误
(8)输入脚本语言,如:<script>alter(“abc”)</script>等
3、添加、修改功能
(1)是否支持tab键
3、添加、修改功能
(1)是否支持tab键
(2)是否支持enter键
(3)不符合要求的地方是否有错误提示
(4)保存后,是否也插入到数据库中?
(5)字段唯一的,是否可以重复添加
(6)对编辑页列表中的每个编辑项进行修改,点击保存,是否保存成功?
(7)对于必填项,修改为空、空格或其他特殊符号,是否可以编辑成功
(8)在输入框中,直接回车
(9)是否能够连续添加
(10)在编辑的时候,要注意编辑项的长度限制,有时,添加时有长度限制,但编辑时却没有(添加和修改规则是否一致)
(11)添加时,字段是唯一的,不允许重复,但有时,编辑时,却可以修改为相同字段(相同字段包括是否区分大小写以及在输入内容的前后输入空格)
(12)添加含有特殊符号或空格的内容
(13)对于有图片上传功能的编辑框,对于没有上传的图片,查看编辑页面时,是否显示默认图片,如果上传了图片,是否显示为上传图片?
4、删除功能
(1)输入正确数据前加空格,看是否能正确删除?
(2)是否支持enter键
(3)是否能连续删除多个产品?当只有一条数据时,能否成功删除?
(4)删除一条数据后,能否再添加相同的数据?
(5)当提供能一次删除多条信息的功能时,注意,删除的数据是否正确?
(6)不选择任何信息,直接点击删除按钮,看有什么错误提示?
(7)删除某条信息时,应该有错误提示信息
前言
考察目前关于单元测试和JUnit的文章,要么是介绍单元测试的理论,要么是通过一个简单的HelloWorld例子介绍工具的使用。这样很容易使读者在实际应用中无从下手。因为只有工具而没有理论的指导,将严重消弱了工具的作用,最终只能是沙滩建楼,达不到预期的目标;只有理论而没有工具的支持,也使得理论难有很好的着力点,最终使理论流于空泛。本文试图通过先讲解单元测试理论,进而将这些理论结合到JUnit的使用当中,最后通过对一个实用的、可以重用的时间操作类采用JUnit进行单元测试来完整阐述单元测试的思想、方法、以及工具的使用。作者相信,只有通过这样,才能让读者真正把单元测试做好。
1.简介
1.1. 为什么要进行单元测试
一个特定的开发组织或软件应用系统的测试水平取决于对那些未发现的Bug的潜在后果的重视程度。这种后果一方面常常会被软件的开发人员所忽视,而另一方面却有可能损害组织的信誉,并且会导致对未来的市场产生负面的影响。相反地,一个可靠的软件系统的良好的声誉将有助于一个开发组织获取未来的市场。
很多研究成果表明,无论什么时候作出修改都要进行完整的回归测试,在生命周期中尽早地对软件产品进行测试将使效率和质量得到最好的保证。Bug发现得越晚,修改它所需的费用就越高,因此从经济角度来看,应该尽可能早的查找和修改Bug。在修改费用变得过高之前,单元测试是一个在早期抓住Bug的机会。
相比后阶段的测试,单元测试的创建更简单、维护更容易,并且可以更方便的进行重复。 从全程的费用来考虑,相比起那些复杂且旷日持久的集成测试,或是不稳定的软件系统来说, 单元测试所需的费用是很低的。研究显示高达50%的维护工作量被花在那些总是会有的Bug的修改上面。如果这些Bug在开发阶段被排除掉的话,那么工作量就可以节省下来。当考虑到软件维护费用可能会比最初的开发费用高出数倍的时候,这种潜在的对50%软件维护费用的节省将对整个软件生命周期费用产生重大的影响。
1.2. 什么是单元测试
单元测试是对最小的可测试软件元素(单元)实施的测试,它所测试的内容包括内部结构(如逻辑和数据流)以及单元的功能和可观测的行为。这里的单元不一定是指一个具体的函数或一个类的方法,“单元”是:
(1)可测试的、最小的、不可再分的程序模块。
(2)有明确的功能、规格定义。
(3)有明确的接口定义,清晰地与同一程序的其他单元划分开来。
在具体实现时,单元测试也可能对应的是多个程序文件中的一组函数。在一种传统的结构化编程语言中,比如C,要进行测试的单元一般是函数或子过程。在象C++这样的面向对象的语言中,要进行测试的基本单元是类。单元测试的原则同样被扩展到第四代语言(4GL)的开发中,在这里基本单元被典型地划分为一个菜单或显示界面。
1.3. 单元测试的一般方法
单元测试的方法一般分为两类:白盒方法和黑盒方法。白盒方法通常是分析单元内部结构后通过对单元输入输出的用例构造,达到单元内程序路径的最大覆盖,尽量保证单元内部程序运行路径处理正确,它侧重于单元内部结构的测试,依赖于对单元实施情况的了解。
黑盒方法通过对单元输入输出的用例构造验证单元的特性和行为,侧重于核实单元的可观测行为和功能,并不依赖于对单元实施情况的了解。进行单元测试必须综合使用上述两个方法,否则,单元测试很可能就是不成功、不完整和不彻底的。
1.4. 单元测试的目标
单元测试要达到的目标,总体来说就是保证单元内部的处理是正确的、没有遗漏和多余功能。细分而言,单元测试要达到以下几个目标:
(1)信息能否正确地流入和流出单元。
(2)在单元工作过程中,其内部数据能否保持其完整性,包括内部数据的形式、内容及相互关系不发生错误,也包括全局变量在单元中的处理和影响。
(3)在为限制数据加工而设置的边界处,能否正确工作。
(4)单元的运行能否做到满足特定的逻辑覆盖。
(5)单元中发生了错误,其中的出错处理措施是否有效。
1.5. 为什么要使用JUnit进行单元测试
1.5.1. 什么是JUnit
JUnit就是对程序代码进行单元测试的一种Java框架。通过每次修改程序之后测试代码,程序员就可以保证代码的的少量变动不会破坏整个系统。官方对JUnit的定义是“JUnit is a simple framework to write repeatable tests.”。
1.5.2. 自己编写测试框架的弊病
自己编写测试框架进行单元测试一般有两个方法。第一种方法是在要测试的类的main()方法中编写测试代码。随着程序越变越大,这种开发方法很快就开始显现出了缺陷:
(1)混乱。类接口越大,main() 就越大。类可能仅仅因为正常的测试就变得非常庞大。
(2)代码膨胀。由于加入了测试,所以产品代码比所需要的要大。
(3)测试不可靠。main() 是代码的一部分,main() 就对其他开发者通过类接口无法访问的私有成员和方法享有访问权。出于这个原因,这种测试方法很容易出错。
(4)很难自动测试。要进行自动测试,必须创建另一程序来将参数传递给 main()。第二种方法是编写一个测试类框架,它虽然能够克服上个方法的缺陷,但增加了开发组织维护这个测试类框架的工作量,为立即大规模的重用设置障碍。而且,由于这个测试框架是内部开发的,存在着与业界难于交流和沟通的弊病。
1.5.3. JUnit的优势
(1)需要编写自己的框架。
(2)它是开放源代码,因此不需要购买框架。
(3)开放源代码社区中的其他开发者会使用它,因此可以找到许多示例。
(4)可以将测试代码与产品代码分开。
(5)易于集成到构建过程中。
2. 单元测试设计
2.1. 单元测试的一般过程
单元测试过程分为计划、设计、实现、执行、评估等几个步骤,各步骤的任务如下:
2.1.1. 计划
单元测试计划需明确如下目标:
(1)明确单元测试的测试对象,确定测试需求及测试通过的标准,明确活动的输出。
(2)明确测试方法和需要运行的工具需求。
(3)对工作量进行估计,确定测试所用资源(包括人力资源和设备资源),创建测试任务的时间表,必要时需将一个单元测试任务分解成更细化的子任务进行明确。
(4)对测试风险进行分析,制定相应的应急措施。
(5)明确测试优先级,制定测试取舍策略。
(6)输出单元测试计划文档。
2.1.2. 设计
单元测试的设计主要是完成方案和模型的确认,包括如下几方面内容:
(1)测试需求的进一步细化,必要时需追溯到详细设计文档中的单元设计目标。
(2)设计单元测试模型,包括与模型相关的工具的选用。
(3)制定测试方案,包括模型的设计和实现、定义测试规程和用例的实现和组织。
(4)输出单元测试方案文档。
2.1.3. 实现
单元测试实现主要是针对用例的实现,包括如下几个方面:
(1)参考测试模型和测试方案,制定具体的测试用例,创建可重用的测试脚本。
(2)输出单元用例文档。
2.1.4. 执行
根据单元测试的方案、用例对单元进行测试,验证测试的结果并记录测试过程中出现的缺陷,主要保留执行过程数据以备问题定位的回归对比。
2.1.5. 评估
对单元测试的结果进行评估,主要有如下几个方面:
(1)实际测试过程的记录,描述与计划的差异和原因,包括补充或裁剪的测试项目清单。
(2)对测试过程完备性以及被测单元质量的评价,包括用例执行情况清单和汇总分析。
(3)主要从需求覆盖和代码覆盖的角度进行测试完备性的评估。
(4)遗留问题记录和可能的分析。
(5)输出单元测试报告。
2.2. 单元测试用例设计方法
测试用例的设计在单元测试中占有非常重要的地位,测试用例设计的好坏直接影响到测试的效果。确定测试用例之所以很重要,原因有以下几方面:
(1)测试用例构成了设计和制定测试过程的基础。
(2)测试的“深度”与测试用例的数量成比例。由于每个测试用例反映不同的场景、条件或经由产品的事件流,因而,随着测试用例数量的增加,对产品质量和测试流程也就越有信心。判断测试是否完全的一个主要评测方法是基于需求的覆盖,而这又是以确定、实施和/或执行的测试用例的数量为依据的。
(3)测试工作量与测试用例的数量成比例。根据全面且细化的测试用例,可以更准确地估计测试周期各连续阶段的时间安排。
(4)测试设计和开发的类型以及所需的资源主要都受控于测试用例。测试用例通常根据它们所关联关系的测试类型或测试需求来分类,而且将随类型和需求进行相应地改变。
最佳方案是为每个测试需求至少编制两个测试用例:
(1)一个测试用例用于证明该需求已经满足,通常称作正面测试用例。
(2)另一个测试用例反映某个无法接受、反常或意外的条件或数据,用于论证只有在所需条件下才能够满足该需求,这个测试用例称作负面测试用例。
单元测试既可以是白盒测试也可以是黑盒测试。白盒测试主要是检查程序的内部结构、逻辑、循环和路径。其常用测试用例设计方法有:逻辑覆盖和基本路径测试。根据覆盖测试的目标不同,逻辑覆盖又可分为:语句覆盖,判定覆盖,判定-条件覆盖,条件组合覆盖及路径覆盖等。白盒测试用例设计还可用到:状态转移测试、数据定义-使用测试、等价类划分、边界值分析等。黑盒测试注重对程序功能方面的要求,它只用到程序的规格说明,没有用到程序的内部结构。其常用测试用例方法有:规范(规格)导出、等价类划分、边界值分析法、错误推测法和因果图分析方法。下面将简要介绍各个方法,更详细的说明请读者自行参考相关的测试理论书籍。
2.2.1. 语句覆盖
语句覆盖就是设计若干个测试用例,运行所测程序,使得每一可执行语句至少执行一次。
2.2.2. 判定覆盖
判定覆盖就是设计若干个测试用例,运行所测程序,使得程序中每个判断的取TURE分支和取FALSE分支至少经历一次。
2.2.3. 条件覆盖
条件覆盖就是设计若干个测试用例,运行所测程序,使得程序中每个判断的每个条件的可能取值至少执行一次。
2.2.4. 判定-条件覆盖
判定-条件覆盖就是设计足够的测试用例,使得判断中每个条件的所有可能取值至少执行一次,同时每个判断的所有可能判断结果至少执行一次。也就是说要求各个判断的所有可能的条件取值组合至少执行一次。
2.2.5. 条件组合覆盖
条件组合覆盖就是设计足够的测试用例,运行所测程序,使得每个判断得所有可能得条件取值组合至少执行一次。
2.2.6. 路径覆盖
路径测试就是设计足够的测试用例,覆盖程序中所有可能的路径。
2.2.7. 规范(规格)导出法
规范导出法是根据相关的规范描述来设计测试用例。每一个测试用例用来测试一个或多个规范陈述语句。一个比较实际的方法是根据陈述规范所用语句的顺序来相应地为被测单元设计测试用例。
2.2.8. 状态转移测试法
对于那些以状态机作为模型或设计为状态机的软件,状态转移测试是合适的测试方法。测试用例通过能导致状态迁移的事件来测试状态之间的转换。
2.2.9. 数据定义-使用测试法
数据定义是指数据项被赋值的地方,数据使用是指数据项被读或使用的地方。目的是设计测试用例以驱动执行通过数据定义于使用之间的路径。
3. 使用JUnit进行单元测试的一般步骤
3.1. 获得Junit
下载得到JUnit的安装软件包。
3.2. 安装JUnit安装JUnit只需要很简单的两个步骤,下面是安装Junit的步骤:
(1)解开DownLoad下来的junit.zip文件。
(2)增加junit.jar到classpath中。例如,set classpath = %classpath%; INSTALL_DIR\Junit3.7\junit.jar经过这两步,就可以开始使用JUnit了。
3.3. 使用JUnit编写测试代码的一般步骤
使用JUnit编写测试代码的一般步骤是:
(1)定义测试类名称,一般是将要测试的类名后附加Test。
(2)引入JUnit框架包。import junit.framework.*。
(3)测试类继承JUnit的TestCase类。
(4)实现类的构造方法,可以在构造方法中简单的调用super(name)即可。
(5)实现类的main()方法,在main()方法中简单调用junit.textui.TestRunner.run(DateUtilTest.class)来指定执行测试类。
(6)重载setUp()和tearDown()方法,setUp()方法用于执行每个测试用例时进行环境的初始化工作(比如打开数据库连接),tearDown()方法用于执行每个测试用例后清除环境(比如关闭数据库连接)。
(7)编写每个测试用例,一般是要测试的方法前附加test。
完整的代码框架如下所示:
import junit.framework.*; public class DateUtilTest extends TestCase { /** * 构造函数 */ public DateUtilTest(String name) { super(name); } /** * 主方法 */ public static void main(String args[]) { junit.textui.TestRunner.run(DateUtilTest.class); } /** * 测试前的初始化 */ protected void setUp() { } /** * 清除测试环境 */ protected void tearDown(){ } /** * 测试用例1 */ public void testGetDateFormat() { } } |
4. 使用JUnit进行单元测试Java应用一例
4.1. 定义接口
按照JUnit的思想,“先有测试代码,后有实现代码”,在编写代码之前,首先应该确定接口。本样例的接口定义如下:
/** * <p>Title: 时间和日期的工具类</p> * <p>Description: DateUtil类包含了标准的时间和日期格式,以及这些格式在字符串及日期之间转 换的方法</p> * <p>Copyright: Copyright (c) 2002</p> * <p>Company: </p> * @author kzx * @version 1.0 */ import java.text.*; import java.util.*; public abstract class DateUtil { /** * 标准日期格式 */ private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MM/dd/yyyy"); /** * 标准时间格式 */ private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("MM/dd/yyyy HH:mm"); /** * 带时分秒的标准时间格式 */ private static final SimpleDateFormat DATE_TIME_EXTENDED_FORMAT = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss"); /** * ORA标准日期格式 */ private static final SimpleDateFormat ORA_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd"); /** * ORA标准时间格式 */ private static final SimpleDateFormat ORA_DATE_TIME_FORMAT = new SimpleDateFormat("yyyyMMddHHmm"); /** * 带时分秒的ORA标准时间格式 */ private static final SimpleDateFormat ORA_DATE_TIME_EXTENDED_FORMAT = new SimpleDateFormat("yyyyMMddHHmmss"); /** * 创建一个标准日期格式的克隆 * * @return 标准日期格式的克隆 */ public static synchronized DateFormat getDateFormat() { /** * 详细设计: * 1.返回DATE_FORMAT */ return null; } /** * 创建一个标准时间格式的克隆 * * @return 标准时间格式的克隆 */ public static synchronized DateFormat getDateTimeFormat() { /** * 详细设计: * 1.返回DATE_TIME_FORMAT */ return null; } |
/** * 创建一个标准ORA日期格式的克隆 * * @return 标准ORA日期格式的克隆 */ public static synchronized DateFormat getOraDateFormat() { /** * 详细设计: * 1.返回ORA_DATE_FORMAT */ return null; } /** * 创建一个标准ORA时间格式的克隆 * * @return 标准ORA时间格式的克隆 */ public static synchronized DateFormat getOraDateTimeFormat() { /** * 详细设计: * 1.返回ORA_DATE_TIME_FORMAT */ return null; } /** * 将一个日期对象转换成为指定日期、时间格式的字符串。 * 如果日期对象为空,返回一个空字符串,而不是一个空对象。 * * @param theDate 要转换的日期对象 * @param theDateFormat 返回的日期字符串的格式 * @return 转换结果 */ public static synchronized String toString(Date theDate, DateFormat theDateFormat) { /** * 详细设计: * 1.theDate为空,则返回"" * 2.否则使用theDateFormat格式化 */ return null; } /** * 将日期对象转换成为指定日期、时间格式的字符串形式。如果日期对象为空,返回 * 一个空字符串对象,而不是一个空对象。 * * @param theDate 将要转换为字符串的日期对象。 * @param hasTime 如果返回的字符串带时间则为true * @return 转换的结果 */ public static synchronized String toString(Date theDate, boolean hasTime) { /** * 详细设计: * 1.如果有时间,则设置格式为getDateTimeFormat的返回值 * 2.否则设置格式为getDateFormat的返回值 * 3.调用toString(Date theDate, DateFormat theDateFormat) */ return null; } |
/** * 将日期对象转换成为指定ORA日期、时间格式的字符串形式。如果日期对象为空,返回 * 一个空字符串对象,而不是一个空对象。 * * @param theDate 将要转换为字符串的日期对象。 * @param hasTime 如果返回的字符串带时间则为true * @return 转换的结果 */ public static synchronized String toOraString(Date theDate, boolean hasTime) { /** * 详细设计: * 1.如果有时间,则设置格式为getOraDateTimeFormat()的返回值 * 2.否则设置格式为getOraDateFormat()的返回值 * 3.调用toString(Date theDate, DateFormat theDateFormat) */ return null; } /** * 取得指定日期的所处月份的第一天 * * @param date 指定日期。 * @return 指定日期的所处月份的第一天 */ public static java.util.Date getFirstDayOfMonth(java.util.Date date){ /** * 详细设计: * 1.设置为1号 */ return null; } /** * 取得指定日期的所处月份的最后一天 * * @param date 指定日期。 * @return 指定日期的所处月份的最后一天 */ public static synchronized java.util.Date getLastDayOfMonth(java.util.Date date){ /** * 详细设计: * 1.如果date在1月,则为31日 * 2.如果date在2月,则为28日 * 3.如果date在3月,则为31日 * 4.如果date在4月,则为30日 * 5.如果date在5月,则为31日 * 6.如果date在6月,则为30日 * 7.如果date在7月,则为31日 * 8.如果date在8月,则为31日 * 9.如果date在9月,则为30日 * 10.如果date在10月,则为31日 * 11.如果date在11月,则为30日 * 12.如果date在12月,则为31日 * 1.如果date在闰年的2月,则为29日 */ return null; } /** * 取得指定日期的所处星期的第一天 * * @param date 指定日期。 * @return 指定日期的所处星期的第一天 */ public static synchronized java.util.Date getFirstDayOfWeek(java.util.Date date){ /** * 详细设计: * 1.如果date是星期日,则减0天 * 2.如果date是星期一,则减1天 * 3.如果date是星期二,则减2天 * 4.如果date是星期三,则减3天 * 5.如果date是星期四,则减4天 * 6.如果date是星期五,则减5天 * 7.如果date是星期六,则减6天 */ return null; } |
/** * 取得指定日期的所处星期的最后一天 * * @param date 指定日期。 * @return 指定日期的所处星期的最后一天 */ public static synchronized java.util.Date getLastDayOfWeek(java.util.Date date){ /** * 详细设计: * 1.如果date是星期日,则加6天 * 2.如果date是星期一,则加5天 * 3.如果date是星期二,则加4天 * 4.如果date是星期三,则加3天 * 5.如果date是星期四,则加2天 * 6.如果date是星期五,则加1天 * 7.如果date是星期六,则加0天 */ return null; } /** * 取得指定日期的下一天 * * @param date 指定日期。 * @return 指定日期的下一天 */ public static synchronized java.util.Date getNextDay(java.util.Date date){ /** * 详细设计: * 1.指定日期加1天 */ return null; } /** * 取得指定日期的下一个星期 * * @param date 指定日期。 * @return 指定日期的下一个星期 */ public static synchronized java.util.Date getNextWeek(java.util.Date date){ /** * 详细设计: * 1.指定日期加7天 */ return null; } /** * 取得指定日期的下一个月 * * @param date 指定日期。 * @return 指定日期的下一个月 */ public static synchronized java.util.Date getNextMonth(java.util.Date date){ /** * 详细设计: * 1.指定日期的月份加1 */ return null; } /** * 取得指定日期的下一个星期的第一天 * * @param date 指定日期。 * @return 指定日期的下一个星期的第一天 */ public static synchronized java.util.Date getFirstDayOfNextWeek(java.util.Date date){ /** * 详细设计: * 1.调用getNextWeek设置当前时间 * 2.以1为基础,调用getFirstDayOfWeek */ return null; } /** * 取得指定日期的下一个月的第一天 * * @param date 指定日期。 * @return 指定日期的下一个月的第一天 */ public static synchronized java.util.Date getFirstDayOfNextMonth(java.util.Date date){ /** * 详细设计: * 1.调用getNextMonth设置当前时间 * 2.以1为基础,调用getFirstDayOfMonth */ return null; } /** * 取得指定日期的下一个星期的最后一天 * * @param date 指定日期。 * @return 指定日期的下一个星期的最后一天 */ public static synchronized java.util.Date getLastDayOfNextWeek(java.util.Date date){ /** * 详细设计: * 1.调用getNextWeek设置当前时间 * 2.以1为基础,调用getLastDayOfWeek */ return null; } /** * 取得指定日期的下一个月的最后一天 * * @param date 指定日期。 * @return 指定日期的下一个月的最后一天 */ public static synchronized java.util.Date getLastDayOfNextMonth(java.util.Date date){ /** * 详细设计: * 1.调用getNextMonth设置当前时间 * 2.以1为基础,调用getLastDayOfMonth */ return null; } /** * 判断指定日期的年份是否是闰年 * * @param date 指定日期。 * @return 是否闰年 */ public static synchronized boolean isLeapYear(java.util.Date date){ /** * 详细设计: * 1.被400整除是闰年,否则 * 2.不能被4整除则不是闰年 * 3.能被4整除同时不能被100整除则是闰年 * 3.能被4整除同时能被100整除则不是闰年 */ return false; } /** * 得到指定日期的后一个工作日 * * @param date 指定日期。 * @return 指定日期的后一个工作日 */ public static synchronized java.util.Date getNextWeekDay(java.util.Date date){ /** * 详细设计: * 1.如果date是星期五,则加3天 * 2.如果date是星期六,则加2天 * 3.否则加1天 */ return null; } /** * 得到指定日期的前一个工作日 * * @param date 指定日期。 * @return 指定日期的前一个工作日 */ public static synchronized java.util.Date getPreviousWeekDay(java.util.Date date){ /** * 详细设计: * 1.如果date是星期日,则减3天 * 2.如果date是星期六,则减2天 * 3.否则减1天 */ return null; } } |
说起单元测试,多数同学应该都知道或听过,可能不少同学认为自己也写过,甚至觉得单元测试很简单有什么好培训的?其实这个事情还真没想象的那么简单!我基本可以比较负责任的说,你若没深入对单元测试做过研究,不知道Mock对象为何物的话,那么可能你以前写过的单元测试压根就不是单元测试。
单元测试是什么?
这个问题其实并不太容易一两句话说得特别清楚。先借用下百度百科的定义:
单元测试是在软件开发过程中要进行的最低级别的测试活动,在单元测试活动中,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
从以上这句定义我们可以看到,两个提取到到两个非常关键的字:最小粒度、隔离
● 单元测试是测试的最小单位,必须可信任的,可重复执行的。
● 如果测试的范围轻易的就会扩展到其他类或同类的其他方法,那就不再是最小单位,也就不是单元测试了!
例如:
类A中的方法CallMethod中调用了类B中的方法DoMethod,如果在编写测试的时候不把B类中的DoMethod隔离出来,造成单元测试CallMethod方法时,实际实行了DoMethod方法,那么这个测试方法不能算作是单元测试。(如何隔离会在后文详解)
单元测试的目的是什么?
有人曾给我一段非常简单的代码片段:一个方法,里面只是调用若干个其他方法,甚至也都没有任何返回值,然后问我这种代码写单元测试有任何价值?!完全是浪费体力!!
public class ClassA { public void CallMethod() { DoSomethingForYou(); DoSomethingForThem(); DoSomethingForMe(); } } |
其实提出这个疑问主要有两个原因导致:未能理解单元测试的目的是什么以及这段代码的可测试性并不高。(可测试性以及如何提高将在后面章节介绍)
单元测试的目的是用来确保程式的逻辑如你预期的方式执行,而并不是用来验证是否符合客户的需求的!通过单元测试来建立一道坚实的保障,防止代码在日后的修改中不会被破坏掉。
是不是很失望?单元测试并不是用来验证代码是否符合需求的。
事实上,单元测试是白盒测试的一种,而且需要开发人员来完成,最好是谁开发的代码就该谁来编写单元测试代码,因为代码的编写者最熟悉该段代码的目的,进而编写出验证该目的是否达到的单元测试代码。
单元测试并不能用来代替其他测试手段,不过是实践过程中确实会很有效的帮助开发人员自查代码,进而发现一些潜在的BUG。但这只是一个额外的收获,若不是采用TDD那样的测试先行开发方式,那么单元测试的根本目的不是用于也无法检验当前代码是否存在BUG的!
上面有说到单元测试最好是由开发人员自己来编写,用于验证该段代码是否符合开发者开发的预期要求。这里可能会有个疑问,既然开发者自己已经很清楚自己想要 结果是什么,直接运行一遍代码实际跑一次,通过断点调试不是就可以很方便的验证了嘛?再通过编写代码的形式,甚至比开发这个功能本身更多的代码,去验证这 个方法是否符合编写的目的,不是很傻很笨很累的办法么?
也许通过一个大家经常会碰到的实际场景能更好的说明:
一个项目开始,项目经理把需求拆解为若干个模块分发给不同的开发人员去完成。这样每个人可能只熟悉自己的那部分代码。当项目某个阶段开发完成并上线后,可能部分开发人员会离开项目进入别的新项目,留下个别人员继续维护;或者项目下阶段开发新进一大批人员并不熟悉当前项目;当然最常见的是,在修改BUG阶段是无法完全做到谁产生的BUG就安排谁去修改。
那么这时候就会出现一种常见的情况:因为对当前代码要满足的各种目的不熟悉,在修改一个模块或者BUG的时候把原有正确的功能也影响到了!更要命的是,谁也不知道这个BUG出现了,等待测试人员需要去重新发现一遍。
于是项目经理会发现,每次只要做了代码修改,无论是重构还是功能新增修改,还是修改了BUG,都无法知道当前代码的健壮性,以前编写的东西是否依然正确可用?
然而如果这个项目在一开始就编写了单元测试的话,我们可以通过方便的自动化单元测试框架运行所有的单元测试,进而检查在此次修改前的所有被单元测试所覆盖的代码是否依然正常运行(符合以前编写的单元测试期望,如果验证通过,则认为原有代码未受到影响)
由上我们可以看出,单元测试虽然增加了相当大的开发工作量,但对于一个长期不断改进和维护的项目而言,单元测试反而是消减整体成本的一个有效手段,它能及时而准确的发现在代码修改之后,原来对代码要求的功能是否都依然正确满足。
但这里有个严重缺陷:单元测试无法检测到某个方法修改后是否对其他方法造成了影响,只能检测到被修改的方法本身的原有目的是否被影响(这个将在下面的与集成测试的区别中详解)
也因此,个人觉得单元测试最适合的场景是基于TDD开发。若需求发生改变,修改了一个方法,而多数情况下也会去修改单元测试代码,因为预期也发生了改变,这个时候又不能检测到对其他代码的影响,这时单元测试意义确实不大。
单元测试与集成测试的区别是什么?
多数人其实一直不能很好的区分集成测试和单元测试,甚至不少人一直理解的单元测试只能算是集成测试,但其实两者的概念是完全不同的。
单元测试测试的对象是每一个独立的方法,而且尽可能的隔离方法和其他方法以及其他外界依赖项;
集成测试的测试对象是被单元测试检测后的方法与方法之间的调用关系,以及调用执行过程是否符合预期。
● 针对项目的一部份或全部进行测试,可以跨越不同的类别与方法,并可直接存取的外部资源。例如: File I/O, 数据库操作, 网络连接, …
● 通常做集成测试都会需要先设置(Configure)测试所需的环境,测试完毕后通常要清除测试所产生的残留资料,以利下次测试或避免影响其他整合测试的结果。
○ ClassInitialize Attribute
○ ClassCleanup Attribute
○ TestInitialize Attribute
○ TestCleanup Attribute
以上这些属性常用于集成测试,不能出现在单元测试中。
单元测试的三大基本要素(Trustworthiness/Maintainability/Readability)
1、信任你的测试代码结果
1)你是否能信任你的测试结果?
2)当它通过,我们有信心说被测试代码一定工作。
3)当它失败,它一定证明被测试代码是错误的。
4)如果你不断的对测试结果失去信心,那么你也不会继续坚持撰写单元测试。有
5)许多人搞不清楚单元测试与集成测试的差别,以致于感觉自己写的单元测试过于薄弱而不相信测试的结果。
6)如果你因为某些原因导致测试失败,直接去改Code或直接去改Test Code都不是好事,你的首要目的是要能找出测试失败发生的主因,而非只是看错误这件事,这样你才能信任你的测试程式。
2、测试代码的可维护性
1)是否能够持续的维护你的测试程式?
2)如何有效的降低维护测试程式的成本?
PS:透过一些Testable Design Pattern 可以有效提升可维护性。例如: Repository Pattern, Service Pattern……
3、测试代码的可读性
1)你的测试程式的命名是否易于理解?
2)当你测试失败时是否能从测试失败的测试方法(TestMethod)明确看出实际失败的原因?
3)当读取测试数据的人看不懂你的测试,人们就不会执行这些测试、也不会去维护这些测试,久而久之就会越来越恶化。
Test Driven Development & Unit Test
写在本文最后,其实我一直觉得单元测试其实是为了TDD开发模式而诞生的,在这种开发模式下使用单元测试完全是非常顺畅的:
1、根据软件需求文档拆解软件功能,并设计出功能模块划分;
2、根据需要的功能模块设计出单元测试场景用例,因为此时可以很清晰的知道能够提供什么样的数据,以及需要达到什么样的功能,这对设计单元测试用例已经完 全足够了;
3、编写单元测试代码,这个时候可以专注于检验这个方法的是否满足设计的要求,此时甚至实际的代码还根本没开发,而.NET 4.0的Dynamic关键字在这里可以得到充分的发挥:调用那些根本都还不存在的方法,却不会导致编译无法通过。
4、若在编写单元测试过程中,可以预期当前这个方法若需要调用一些其他类或方法的支持,可以通过编写Mock Object来模拟,同样也是无需实现真正的代码,只需要有基本的代码框架或者接口即可。
5、在为这个方法编写好单元测试代码之后,就可以开始编写实际的代码实现了,因为在之前为了满足Testability的需要,代码已经是基于依赖倒置模 式的了,无需再担心其他需要调用的类或方法是否已经实现或正确实现。在编写好本方法的实现之后就可以通过运行之前的单元测试进行验收了。
可以看到,若按照以上这种方式进行开发,首先代码的耦合性是非常低的,其次代码的质量也是很高的,最后还会因为代码之间的耦合度低从而降低在开发过程中, 相互制约进度相互影响的可能性。在追查BUG的时候也很有优势:很容易查到BUG是否蔓延。
反之,对一个Legacy System进行重构使之Testable,再编写单元测试其实工作量不小,实际的收益也不会特别大。
单元测试的基本概念以及价值就基本讲完,下篇文章将开始介绍Visual Studio 2010中的单元测试工具与环境。
做单元测试的过程,我们兴奋过,低落过,冷静过,改革过,现在良性发展中,刚开始做的时候很兴奋,一天只知道写啊写啊,到头来写了很多单元测试用例,但到现在扔掉的也很多。
当然原因也很多,主要还是测试目的的不明确,测试范围的不明确,测试标准不明确,对模块本向在产品中为何存在,为什么是这样存在没有弄清楚,走了很多湾路。
当然接到一个模块的测试任务时,应该与开发人员,系统测试人员和产品人员,了觖这个模块为什么存在,有什么功能,在产品中是怎么样的一个位置和作用。
要多读开发的代码,测试用例写完后,一定要调试一次测试案例,跟到开发的代码里面去,一行一行的看状态是不是正确,一般都会发现很多bug的。
测试一个模块不光要看是否正确实现了想要的功能,还要看开发这样写是不是合理,有没有改进的地方,并提出改进方案,开心的就是我们周围的一群同事常常在测试一个模块的时候就会写一封改进方案的邮件发给开发人员,并抄送给相关上级,在算法的简单,性能的提高,成本的减少都引到了很好的作用。这也是人们的开发人员很尊重我们测试人员提出的任务意见的原因,原因他们知道这个模块还有一个测试人员和他们自己一样对这个模块里的每一行代码都很熟悉。
测试的标准很难,怎么样才算这个模块测试好了,因为上级不用看你写的测试用例,他只要数据,但是给什么他才有说服力,我们在案例数,bug数等很多数据里选了代码覆盖率做为标准,虽然这个数据也不能充分说明测试的完整性,但相对来说还是比较有信服性。
测试用例在后期维护的时候,我们就头疼过,而且是很头疼,模块内部实现变了,接口变了,在产品里的位置变了,数据结构变了,我们的用例就天天跟着开发也在变。目前没有特别好的办法,只能看具体问题具体处理,在写用例的时候还是有一点点技巧的。
可恨的setup和teardown,之前对它的理解就是测试前的事情和测试后做的事情,所以不太注意这个地方,就引起测试用例一个一个运行是正常的,一批一批运行就失败了;一个人写的案例运行是正常的,两个写的案例在一起运行就失败了。一失败就是几百个,要找出是那个用例引起的测试环境的破坏,很痛苦。真的很痛苦!!!“吃掉自己的狗食”这句话用到这里绝对合适,不要仅把自己的setup做好,自己的teardown比setup更重要。
测试用例常失败的几个原因:
1、行为敏感性 如果系统的行为发生变化,如需求变了,这时测试就会失败。
2、接口敏感性
3、数据敏感性测试
4、上下文敏感。
突然想写,写的比较乱多见凉。
本文讨论的是基于字符终端型的银行核心业务系统。银行核心业务系统由于其复杂的业务流程,以及特殊的终端字符形式,与一般的B/S结构、C/S结构系统有较大的差异,其性能测试方法也存在很多的不同。下面就我对银行核心业务系统的理解,以及利用LoadRunner进行银行核心业务系统性能测试的相关经验,介绍一下有关银行核心业务系统的性能测试内容、测试方法,以及测试过程中的注意事项等。
测试内容
◆ 联机业务
联机业务主要是有关的柜台业务,如卡/折业务、贷款业务等。对联机业务的测试,主要是关注以下内容:
● 不同并发用户数(终端数)下,核心业务系统的处理能力,包括交易数/秒、交易成功率等;
● 不同并发用户数(终端数)下,各服务器端的资源利用情况,如数据库服务器、应用服务器、前置服务器端的CPU利用率、可用内存等;
● 不同并发用户数(终端数)下,各常用交易的响应时间情况;
● 一定并发用户数(终端数)下,系统长期运行的稳定性等。
◆ 批量业务
批量业务主要是结息相关的批处理业务。对批量业务的测试,主要是关注以下内容:
● 结息的账户数,包括活期户数、卡数、一本通数、贷款户数等;
● 结息的时间,包括起始时间、终止时间。
● 特殊交易
特殊交易主要是指日始签到、日终轧账等业务。对特殊交易的测试,主要是关注以下内容:
● 不同并发用户数(终端数)下,核心业务系统对日始签到、日终轧账的处理能力,包括登录柜员数/秒、轧账柜员数、登录成功率、轧账成功率等;
● 不同并发用户数(终端数)下,日始签到、日终轧账时各服务器端的资源利用情况,如数据库服务器、应用服务器、前置服务器端的CPU利用率、可用内存等;
● 不同并发用户数(终端数)下,日始签到、日终轧账的响应时间情况。
测试方法
◆ 联机交易
● 对核心业务系统进行负载测试
按照确定的测试功能及用户分布情况,模拟不同数量的柜员并发执行联机交易,得到各执行交易的响应时间、每秒的交易数、成功的交易数、失败的交易数,以及各服务器的CPU、内存利用情况等。
● 对核心业务系统进行疲劳测试
按照确定的测试功能及用户分布情况,模拟一定数量的柜员并发执行联机交易,连续运行一段时间,得到成功的交易数、失败的交易数,以及各服务器的CPU、内存利用情况等,从而得出系统长时间运行的稳定性。
◆ 批量业务
启动核心业务结息操作,利用软件系统中的日志记录功能,记录验证结息的开始时间和结束时间,得到核心系统对结息的处理效率。
◆ 特殊交易
● 日始签到
模拟不同数量的柜员并发执行签到操作,记录响应时间、每秒的签到柜员数、成功的签到柜员数、失败的签到柜员数,以及各服务器的CPU、内存利用情况等。
● 日终轧账
模拟不同数量的柜员并发执行轧账操作,记录响应时间、每秒的完成轧账的柜员数、成功的轧账柜员数、失败的轧账柜员数,以及各服务器的CPU、内存利用情况。
注意事项
软件性能测试的大部份工作主要集中在测试前的准备工作上,银行核心业务系统也不例外,而且由于其特殊特点,准备工作中还需要注意一些事项,如:
(1)测试前需要准备一定数量的数据,为了保证不影响测试结果,需要在准备数据中注意以下事项:
◆ 每个存折、卡账号对应的钱数足够多,防止该账号余额不足,而不能再连续进行取款等操作,影响测试结果;
◆ 每个柜员对应的尾箱的钱数足够多,防止该柜员因为尾箱现金不够而导致不能进行取款、销户等操作,影响测试结果;
◆ 在非结息日事先选择一些账户进行存、取款业务操作,然后在结息日进行结息操作。脚本准备。
(2)利用LoadRunner准备测试脚本过程中,需要根据核心业务系统,选定终端类型、调整键盘选项等。
(3)另外在测试场景运行前,需要在LoadRunner的Countroler中选中RTE用户类型,否则虚拟用户会一直处于挂起状态,LoadRunner默认是没有选中该用户类型的。
以上对银行核心业务系统的性能测试内容、测试方法、测试中的注意事项等进行了简单的总结,由于经验有限,而且银行核心业务系统包含的内容繁多,以上只是对一些简单业务的性能测试进行了讨论,如有不当之处,请批评指正。
自2010年06月21日中国人民银行公布《非金融机构支付服务管理办法》以来,针对非金融机构“支付业务许可证”的申请及检测认证工作已经逐步展开。下面,我们将结合央行检测认证的相关规定,对非金融机构第三方支付系统性能检测的要点进行解读和分析。
一、第三方支付系统性能检测内容
中国人民银行于2011年1月17日发布了《非金融机构支付服务业务系统检测认证管理规定》(征求意见稿)。其对第三方支付系统性能检测的目的和内容作了如下说明:“验证业务系统是否满足业务需求的多用户并发操作,是否满足业务性能需求,评估压力解除后的自恢复能力,测试系统性能极限”。
通过这段说明我们不难看出,对支付服务业务系统性能的检测主要包括以下三方面内容:一是系统的并发能力验证;二是压力解除后系统自恢复能力;三是系统性能极限验证。
系统的并发能力验证应包含两方面检测内容:一是验证系统是否支持业务的多用户并发操作;二是结合典型交易检验各测试点在给定并发用户数下,系统各项性能指标是否满足用户性能需求。
系统自恢复能力验证的内容主要是在系统并发能力验证和系统性能极限验证的同时,记录各测试点在加压和压力解除前后系统资源的使用情况及资源恢复所用的时间。
系统性能极限验证的内容主要是对典型交易采用极限测试策略,通过逐步增加系统负载的方式,测试系统性能的变化,并最终确定在什么负载条件下系统性能处于失效状态,同时记录此时系统所能承受的最大并发用户数。
二、第三方支付系统性能检测要点分析
与其他应用系统的性能测试一样,规范的第三方支付系统性能测试同样需要经历测试准备、测试实施和测试总结等过程。
1) 性能需求分析
因各家非金融机构支付服务系统的用户规模不同,所以央行并未对第三方支付系统性能检测环境和性能指标进行硬性规定,性能指标的确认依据主要来自于系统需求文档中对性能的约定或用户性能需求的调研。
性能需求的主要调查内容包括:系统实际使用的用户数量、正常情况下系统的平均使用用户数、高峰时段的在线用户量、可预期生命周期内系统的用户增长情况、一年的业务量及日交易量、压力解除后系统自恢复时间要求等。
2) 测试策略分析
根据非金融机构支付服务系统的业务特点,对其性能的测试大致可分为两类:一类是包含数据插入操作和数据查询操作的并发测试性能(如:支付、交易明细查询等);另一类是大数据量处理性能(如:日终批处理等)。
并发测试策略的主要内容应包括:并发用户数、性能指标要求(包括响应时间、系统资源占用)等;对大数据量计算性能测试策略的制定过程中,需要关注的是对批处理交易数据量的要求。
3) 性能测试点选取分析
按照央行的定义,第三方支付服务包含网络支付、预付卡和银行卡收单等,而无论采用哪种支付方式,三种支付平台实质上都是买卖双方交易过程中的“中间件”,它的核心功能就是通过提供的支付网关为交易双方提供支付、充值等交易服务,并记录双方的交易数据。对其测试点的选择可以典型交易、复杂业务流程、频繁的用户操作、大数据量处理等为总体指导原则,围绕支付、交易管理、资金结算、对账处理等核心业务进行选取。
在网络支付系统中,我们将重点选取支付、预存、交易明细查询、日终批处理等操作进行测试;预付卡部分重点选取联机消费、联机余额查询、交易明细查询、批量充值、日终批处理等操作进行测试;银行卡收单部分重点选取消费、预授权、日终批处理等操作进行测试。
三、第三方支付系统测试方法简析
第三方支付系统性能测试可以选择常见的商用性能测试软件进行,但需要注意的是由于交易过程通常需要调用银行接口与协约银行进行数据交换,因此在测试脚本编辑过程中需要用模拟接口来替换真实的银行接口来测试支付平台的真实性能。预付卡和银行卡收单其交易数据的来源均为Pos机,性能测试中只能用开发的工具或编制的脚本来模拟发送报文到Pos前置服务器进行并发测试,具体可通过Socket协议编写报文发送脚本的过程进行实现。
每个公司都有自己的基因。做产品起家的,和网络公司不同。对于性能测试,很多的思维还停留在单机时代。于是很多QA就认为,无非就是测试CPU,memory,disk等参数而已。
但是随着后台的service程序逐渐增多,service的性能测试,和之前测试一个产品,已经有了很多的不同。这里,就谈谈这次性能测试的一些经验。
其实之前QA已经做过了一些性能测试。但是有一天我们计划购入机器为产品上线做准备,manager问我如何购买机器。我一看module不少,首先就考虑如何分配这些moudule在不同的机器上,以获得最好的性能。于是我要搞清楚每个module的性能瓶颈,到底是cpu bound,还是IO bound,还是memory bound。
我就建议QA做了这样的测试。测试结果出来了。从结果看来,扫描病毒的模块CPU还是一个瓶颈,毕竟,扫描病毒是一个很耗时的操作。而web service则需要较多的memory。
我后面关心的就是那么多模块协同工作,谁是最慢的环节。因为我们之前的设计还是考虑的拓展性,所以,对于最慢的环节,通过增加进程数目和增加机器可以改善。
结果出来了,QA很快就根据他们的性能,给出了一组最小的机器配置列表。哪些module可以放在一起,每个module至少要起几个进程才不至于出现特别慢的module block整个service的效率。这下就简单了。我们可以把它作为一个service组,以后增加机器,就按照这样的配置成倍的增加。
其实这样的思路,就是现在所谓的SOA运维。别人问你需要多少机器,如何扩容,你给出的不是几台机器,而是以一个最小的service集群组为单位的系统配置。比如说你的service有3个模块,他们的配置可能是
模块 数目 特征
A 1 CPU bound
B 2 IO bound
C 1 memory bound
意味这增加一个A和C,需要两个B配合。
这样,最小的配置就是 1 cpu,2 disk,1 memory,有可能对于到服务器上面,就是
8core cpu
15000 PRM SAS disk *2
32G memory
有了这样的最小单位,下面才是真正的性能测试环节,我们要知道,这个最小的service单位,能够有多大的吞吐量。
于是,尽可以多地喂数据,看看输出的效率。这时,我们关心的已经不是cpu、memory、io这些参数了,因为你的最小配置必须是这样的。你可能会浪费CPU,浪费内存,但是没有办法,因为瓶颈在那里,要增加机器提高整体吞吐量,IO是瓶颈。 我们关心的是,整个最小的service集合到底能有多大的吞吐量。如果我要更大的吞吐量,需要多少个这样的service单位。
这样的性能测试结果,对产品,对运维才是真正有意义的。这就是从整体的角度去考虑一个service产品。而这也为RD后期的开发起了指导意义,哪个模块是重头戏,对整体而言起决定意义。需要重点调优,哪个模块虽然效率很低,但是调优的优先级可以放低。因为他不是关键。
这里的关键,就在于强调整体测试。而且这个整体是建立在之前模块测试后的模块配比的基础上的。强调最小service集合的测试。
百无聊懒中参加了“进销存系统”的测试工作,发现自己还真是喜欢测试,拿起一个软件就想用用,想测测。
测试该系统仅用了差不多一天的时间,没有写测试用例,因为该系统是公司内部使用,所以应上级要求,作为测试人员我只是测试了下基本的功能,对于一些输入框什么的,没有考虑一些特殊的输入,比如特殊字符等。
感觉收获还是有的,现在总结下心得:
一、首先要进行单元功能测试,保证每个功能使用的正确
有以下几个问题,需要自己以后测试中注意:
1、重复添加问题:某个产品被删除或设为“未激活”状态后,再次添加同样的产品信息,能否添加上?或者添加已经存在的产品信息,系统是否有相应的提示信息?
2、采购单或送货单中,产品的删除问题:是否能够连续删除多个产品?当采购单或送货单重只有一个产品时,能否删除掉?
3、产品重量是否可以改为小数?
4、出库管理模块,汇入/汇出功能,要考虑到导入正确的数据、误写的数据、不存在的数据三种情况,但导入这些数据时,系统是否有错误提示信息?
二、业务流程的测试
1、各个模块间数据传递是否正确,要注意到“重量”和“金额”数值类型的问题,比如,当重量和金额都有两位小数时,传到其他模块是否数值一致?
模块间数据传递比较复杂,一定得着重测试,凡是涉及到模块间数据传递的一定要设计不同的数据进行测试,这往往是开发最容易出错的地方。
2、采购后相应的产品库存是否相应的增加?
3、出库后相应的产品库存是否相应的减少?
三、权限分配
1、以不同权限登录时,信息显示是否正确?
2、以不同权限登录进行操作,能否操作成功?同样要注意数据传递关系。
四、个人对该进销存系统的认识
该系统和我想象中的不太一样,它是以订单为驱动来开发的,包括:
产品管理:主要是录入产品的一些基本信息
订单管理:该模块比较复杂,以订单为驱动,下采购订单,在采购订单的基础上生成送货单,入库,涉及到了批次入库(生成批号并填写有效日期)
出库管理:遵循先进先出的原则,即,批号小的先出。
我发现很多人,包括论坛上的网友,还有很多身边的同事都对UI自动化充满了一些恐惧感,从而不敢触及它。当然也有一定的原因是觉得UI自动化没太深的技术含量,这也是我讨厌UI自动化的唯一原因。但是,一旦让这些人去做UI自动化的话,是很难做好的,因为UI自动化需要一定的经验,而我个人认为一年的经验,一个正规的项目应该都能具备编写良好UI自动化测试的能力。因此,对于后来的人,我想把UI自动化关键的几条再谈一谈,UI自动化确实没什么技术含量,你掌握了以下几点也能成为一个小专家了。
1. 用高级语言编写自动化程序,在UI的部分调用UI自动化工具。我反对纯用UI自动化工具去写自动化,因为那样就太死板了,而且功能不强大,不灵活。我推荐学好一门高级语言,把大多数的自动化都用这门高级语言实现,只在需要UI操作的时候才调用UI工具。
2. 只在你测试的UI模块上进行自动化的测试,其他地方避免用UI去操作,使用高级语言去实现。这样你需要用UI的地方就进行了最小化,从而使得只有在真正需要UI的地方才自动化UI,因此测试程序会相对更稳定。
3. UI自动化最基本的操作就是发现控件和操作控件。尽量避免用text来发现控件,而使用一些固定的控件属性来发现,比如Control ID等等。这样的话,测试程序会更稳定,开发改变文本不会影响到你,而你也不用担心localization的问题。
4. 操作控件分为模拟用户操作和事件驱动。简单的例子就是,模拟用户操作就是鼠标真的去点一下,而事件驱动则是跳过点击直接引发点击的事件。我以前用过具有这种功能的工具,但是最近几年用的工具不具备这个功能。
5. 解决好同步问题。UI自动化最不稳定的地方就是同步问题了,你不能连续点击,而需要等待到一定的情况才能进行下一次点击。各种情况都不太一样,需要一些经验进行良好的程序设计。但是,简单来讲,要做到等待的情况发生能立刻返回到程序,不能空等。
6. 减少其他UI对你自动化程序的影响,比如关闭Windows balloon,等等。一般来说是发现了有其他UI影响你的情况,就想一下workaround, 不会有什么大问题。
从我的经验上来看,一般UI自动化有问题都能归结于以上几点,而一旦你解决了以上几点的话,UI自动化就变成了一个熟练工的工作了,没什么挑战性。我本人的有些模块的UI自动化基本可以达到100%的通过率,而所有模块的自动化也能达到95%以上的通过率。不过我基本已经脱离UI自动化了,因为太没有技术含量了,不过我还是认为如果你刚刚进入测试的工作,或者从来没有接触过UI自动化,或者从来都没有做好过UI自动化的话,在这上边工作个2,3年会有一定的收获的。
控件类型 | 大分类 | 小分类 | 检查内容 | 结果判定 |
TextBox | 数值型 | 边界值 | 输入[最小值-1] | 程序应提示错误 |
输入[最小值] | OK |
输入[最大值] | OK |
输入[最大值+1] | 程序应提示错误 |
位数 | 输入[最小位数-1] | 程序应提示错误 |
输入[最小位数] | OK |
输入[最大位数] | OK |
输入[最大位数+1] | 程序应提示错误 |
允许输入小数位的控件,小数位的长度做以上同样测试 | 同上 |
异常值、特殊值 | 输入[空白(NULL)]、空格或‘“~!@#$%^&*()_+-={}[]|\:;”’<>,./?;”等可能导致系统错误的字符 | 程序应提示错误 |
禁止直接输入特殊字符时,使用“粘贴”、“拷贝”功能尝试输入,并测试能否正常提交保存。 | 只能使用“粘贴”、“拷贝”方法输入的特殊字符应无法保存,并应给出相应提示 |
word 中的特殊功能,通过剪贴板拷贝到输入框:分页符,分节符,类似公式的上下标等 | 程序应提示错误 |
输入[负值] | 根据设计书要求判定 |
输入设计书中明确指出禁止输入的数字 | 根据设计书要求判定 |
输入[英文字母] | 程序应提示错误 |
数值输入的长度:整型----32位 最大值 65535,最小值-65535;16位 最大值 32767,最小值-32767 | 根据设计书要求判定 |
带符号的数值:带正号的正数,带负号的负数 | 根据设计书要求判定 |
小数:小数点后的位数,小数的四舍五入问题,小数点前零舍去的情况,如 .12;多个小数点的情况;0值:0.0,0.,.0 | 根据设计书要求判定 |
分数:如 2/3 | 根据设计书要求判定 |
首位为零的数值:如01=1 | 根据设计书要求判定 |
科学技术法是否支持:如 1.0E2 | 根据设计书要求判定 |
指数是否支持 | 根据设计书要求判定 |
全角数字和半角数字的情况 | 根据设计书要求判定 |
数字与字母的混合:16进制数值,8进制数值 | 根据设计书要求判定 |
货币型输入项:允许小数点后几位 | 根据设计书要求判定 |
字符型 | 字符种类 | 输入[全角字符] | 根据设计书要求判定 |
输入[半角字符] | 根据设计书要求判定 |
数字字符 | 根据设计书要求判定 |
邮政编码输入项的输入限制,如只能输入半角数字字符或某几个指定字符 | 根据设计书要求判定 |
电话号码和传真输入限制,如只能输入半角数字字符和半角括号“()”及半角减号“-”;电话或传真只能输入数字和减号。 | 根据设计书要求判定 |
E-mail地址的格式检查,如输入字符串中必须包含“@”和半角“.”字符。 | 根据设计书要求判定 |
年龄的输入限制检查,一般<=200即可。 | 根据设计书要求判定 |
输入设计书中明确指出禁止输入的字符 | 程序应提示错误 |
输入[空白(NULL)]或“~!@#$%^&*()_+-={}[]|\:;”’<>,./?;”等可能导致系统错误的字符 | 程序应提示错误 |
密码输入项的特殊处理 | 登录验证时大、小写是否区分 | 根据设计书要求判定 |
登录只能输入半角字符 | 根据设计书要求判定 |
是否允许输入特殊字符 | 根据设计书要求判定 |
多行文本框输入 | 允许回车换行 | 根据设计书要求判定 |
保存后再显示能够保持输入时的格式 | 根据设计书要求判定 |
仅输入回车换行,检查能否正确保存;若能,查看保存结果。若不能,查看是否有正确提示 | 根据设计书要求判定 |
仅输入空格,检查能否正确保存;若能,查看保存结果。若不能,查看是否有正确提示 | 根据设计书要求判定 |
长度检查 | 输入[最小字符数-1] | 程序应提示错误 |
输入[最小字符数] | OK |
输入[最大字符数] | OK |
输入[最小字符数+1] | 程序应提示错误 |
文件名输入项的测试 | 输入不存在的文件名 | 程序应提示错误 |
输入文件名称超长(256个字符) | 程序应提示错误 |
输入带路径的文件名和不带路径的文件名 | 根据设计书要求判定 |
手工输入后缀名称 | 根据设计书要求判定 |
对于文件大小的限制,需要采用边界值法测试系统的处理方式是否符合需求;考虑磁盘空间不足/满的情况 | 程序应提示错误 |
文件名的非法字符集:/\:*?"<>| | 程序应提示错误 |
不输入文件名和输入空格 | 程序应提示错误 |
输入中间有空格的路径名和文件名 | 根据设计书要求判定 |
输入合法字符,但影响系统判断文件名有效性的情况,如输入a;b-20003.5.8 | 根据设计书要求判定 |
日期型 | 合法性检查 | 日输入[0日] | 程序应提示错误 |
日输入[1日] | OK |
日输入[32日] | 程序应提示错误 |
月输入[1、3、5、7、8、10、12月]、日输入[31日] | OK |
月输入[4、6、9、11月]、日输入[30日] | OK |
月输入[4、6、9、11月]、日输入[31日] | 程序应提示错误 |
输入非闰年,月输入[2月]、日输入[28日] | OK |
输入非闰年,月输入[2月]、日输入[29日] | 程序应提示错误 |
(闰年)月输入[2月]、日输入[29日] | OK |
(闰年)月输入[2月]、日输入[30日] | 程序应提示错误 |
月输入[0月] | 程序应提示错误 |
月输入[1月] | OK |
月输入[12月] | OK |
月输入[13月] | 程序应提示错误 |
异常值、特殊值 | 输入[空白(NULL)]或“~!@#$%^&*()_+-={}[]|\:;”’<>,./?;”等可能导致系统错误的字符 | |
时间型 | 合法性检查 | 时输入[30时] | 允许输入30时制的项目“OK"; 不允许输入30时制的项目程序应提示错误 |
时输入[31时] | 程序应提示错误 |
时输入[00时] | 程序应提示错误 |
30时制是否允许存在1点~5点 | ?? |
分输入[59分] | OK |
分输入[60分] | 程序应提示错误 |
分输入[00分] | OK |
秒输入[59秒] | OK |
秒输入[60秒] | 程序应提示错误 |
秒输入[00秒] | OK |
异常值、特殊值 | 输入[空白(NULL)]或“~!@#$%^&*()_+-={}[]|\:;”’<>,./?;”等可能导致系统错误的字符 | 程序应提示错误 |
特定值(如:只允许输入:"0","1"等) | 合法性检查 | 分别输入所有允许输入的特定值 | OK |
输入任意不属于特定值范围的字符 | 程序应提示错误 |
异常值、特殊值 | 输入[空白(NULL)]或“~!@#$%^&*()_+-={}[]|\:;”’<>,./?;”等可能导致系统错误的字符 | 程序应提示错误 |
ChcecBox | 复选 | 连续选择 | 连续选择相邻的checkbox | OK |
跳跃选择 | 跳跃选择不连续的checkbox | OK |
ComboBox | 单选 | | 选择某一个列表项 | 被选中项目高亮或底色显示 |
复选 | | 使用ctrl选择多个列表项 | 根据设计书要求判定 允许多选时,所有被选中项目高亮或底色显示; 不允许多选时,只有第一次被选中的项目高亮或底色显示,再点击其他项目应无反应; |
0, 11, 92, 23, 0, 60, 93, 11 Bitmap | 鼠标操作 | 上键头 | 鼠标点击按件的“上箭头” | text框中数量自动+1 |
下键头 | 鼠标点击按件的“下箭头” | text框中数量自动-1 |
键盘操作 | 上键头 | 按下键盘的“上箭头” | text框中数量自动+1 |
下键头 | 按下键盘的“下箭头” | text框中数量自动-1 |
箭头控制输入值 | 边界值 | 输入[最小值-1] | 程序应提示错误 |
输入[最小值] | OK |
输入[最大值] | OK |
输入[最大值+1] | 程序应提示错误 |
text框输入值 | 同TextBox输入测试
|
最近还是发现有一些文章,个人对于自动化测试报有很大的怀疑态度,本人也对相关的文章给与了驳斥。我个人和公司对自动化测试都是报有很积极的态度的。这里我想再次的写一篇文章来阐述到底UI自动化测试可以做什么,作为一个优秀的UI自动化测试工程师应该具备有什么方面的技能,以及本人对UI自动化的一些经验和体会。
首先还是要强调一点,API和command line程序都是非常适合用自动化来进行测试的。我想这个观点,即使那些反对自动化测试的人也不应该否认吧?至少我觉得他们应该有这个意识。因此,对于他们反对自动化就集中在了UI自动化方面,我这里也完全站在UI自动化测试的角度来写这篇文章,后边就不再强调了。
再次套用朋友的一句话,"自动化测试听起来很神秘,学起来很简单,用起来很麻烦"。我想有过自动化测试经验的人,可能大多都有这个体会吧?前边的过程我就不提了,以后主要探讨为什么用起来会麻烦和怎样简单化自动化测试。总而言之,想搞好自动化测试,还是需要测试人员比较高的技术水平,尤其是编程能力和解决问题,分析问题的能力。
首先,我要谈谈自动化测试工具和编程语言的关系。作为一个优秀的自动化测试人员,他的最基本的能力就是编程水平了。所谓编程就是至少要精通一门高级语言,比如Java,C#等等,脚本语言不计算在内。请记住,不是熟悉,是精通。高级编程语言给我们提供很强的能力来实现一些东西。你所精通的语言能力越强,你在自动化测试可以做的事情就越多,越好。简单来讲,论程序的能力来排序是这样的,测试工具-〉脚本语言-〉高级语言(Java,C#)-〉 C/C++-〉C++/CLI。根据这个语言能力的排序,结合你自己的语言能力,你可以想想你到底具备多少自动化测试能力。我所强调的是,如果你想优秀,你至少要精通一门高级语言,这是必不可少的。如果很多人还只是用测试工具,脚本语言工作而抱怨自动化,我要强烈的建议他们好好去学习一下编程能力先了。他们的问题在于,由于编程能力的不足,使得自动化测试的很多问题没有能力和办法去解决。再谈一下自动化测试工具,无论哪种测试工具,无论他们设计的多么强大,从编程语言来讲,他们最多能够达到脚本语言的能力。也就是说,如果你完全用测试工具来进行自动化的开发,很多问题你还是无法解决的。因此,我推荐的自动化开发方法是高级语言结合测试工具。我的自动化测试逻辑是,用测试工具只是完成UI操作,其他部分完全用高级语言来实现。我们不能否认高级语言所具有的能力,他们创造出了世界上这么多丰富多彩,这么多优秀的软件,难道开发测试程序会有问题吗?因此,我们的焦点就落在了测试工具的UI操作部分。
第二,关于测试工具。开发语言重要,选择一个合适的测试工具也同样的重要。一个灵活,强大的测试工具可以使你的自动化开发起到事半功倍的作用。结合不同的项目,不同的语言,你可能会有不同的选择。不过,这里我想解释的是,具有了高级语言的开发能力之后,我们期望测试工具来为我们做什么。我前边也说过了,我们所要求自动化测试工具所做的就是UI的操作。这里边比较重要的是三个方面,一是找到UI对象,二是操作UI对象,三是同步。如果一个工具能够让你找到所有的UI对象,并且能成功操作这些对象,就完全满足我们的自动化开发需要了。如果,工具能够提供同步的功能,就使你能够如虎添翼,不然的话要自己去实现,会麻烦不少。到了这里,你已经具有了所有UI的操作能力(测试工具提供),并且具有了高级语言的实现能力(高级语言提供),你才有了基本的能力去做一个优秀的自动化开发。没有这些能力的人,我严重怀疑能否做出好的自动化测试。
第三,怎样自动化。我的自动化的原则是,尽量少的进行UI的操作,除非是你本身要测试的UI。道理很简单,UI操作由于可能受各种问题的干扰,很容易失败。通过非UI的方法去实现是更加可靠和快速的。这也是我为什么要强调对于高级语言的精通,具有高级语言的开发能力,你就能过把大量的任务从UI操作转向了程序操作,使得你的自动化程序的可靠性大大的增强。这里还需要强调的一点能力就是系统应用的能力,比如Windows使用的能力。Windows的很多的操作是有相关的命令来实现的,不一定非得通过大家熟悉的UI。记住这个原则:除非是你要测试的UI,否则尽可能的通过高级语言来实现。我想大家对于高级语言来实现的工作应该还是有信心吧?因此,下边我要谈的内容就完全的与你要测试的界面相关了。
第四,怎样进行UI测试。首先要尽量的减少UI操作,除非是你必须要测试的操作。比如简洁快速的启动你要测试的界面,用快捷键代替鼠标操作等等。总而言之,理想状态下我们进行的每一次UI操作,都是我们需要测试的,其他操作尽量避免,不能避免用最可靠的方式去实现。那么我们现在的焦点就变成了,怎样来处理我们真正要测试的UI了。UI测试的开发基本上就三个问题:发现对象,操作对象和同步。简单解释一下同步,同步就是有一个机制告诉你何时可以执行一个 UI操作。很多人是用sleep的方式,等待一定的时间去执行下一个操作,这是我非常反对的。我的原则是,尽量少用sleep,就算要用每次最多不要超过一秒。滥用sleep会严重影响测试程序的性能(具体的UI自动化过程,大家可以参考我的其他文章)。
第五,UI测试错误/异常的解决和Debug。通过以上的解释,我们只是在自己需要测试的UI操作才进行UI操作,否则通过高级语言或者系统命令来实现。是不是我们的UI自动化就完美了呢?绝对不是,这只是一个基础,还远远没有达到完美。我们在自动化开发和应用的过程中,大部分的时间其实是花费在了异常/错误处理和Debug上面。这跟真正的程序开发非常的类似,你如果去看代码的话,大量的是在进行返回值得检验和异常的处理。如果我们的程序在运行过程中出了问题怎么办,或者如果没有出现我们期望的结果怎么办?一般来说有三种问题,第一是产品的问题,我们可以报bug了,第二是你测试程序的bug,你需要fix。第三是其他的问题,比如测试工具,甚至高级语言本身的问题,你需要workaround。总而言之,优秀的测试程序最终的目的是,一旦程序的运行发现了问题,就是产品的问题,就是可以报bug的。能够达到这种境界才能算自动化测试的完美,才能算是一个真正优秀的测试人员。(当然了,正如软件产品不可能没有bug,你的测试程序也不可能完全没有bug。但是,由于软件产品是有大量的用户来使用,而你的测试程序只是很小范围内来使用,使得你消除影响测试过程的bug成为完全可能)
综上所述,一个优秀的自动化测试工程师必须要具备高级语言的开发能力,自动化工具的灵活应用能力,系统命令和使用的熟练能力等这些基本功,还更要具备优秀的Debug,Fixbug的能力,和保持程序稳定性能力。换句话讲,一个优秀的自动化测试工程师必定也是一个优秀的软件开发工程师。
最后谈一下我为什么要转向C++/CLI?从上边的排序大家可以看到,C++/CLI是目前Windows平台最强大的编程语言。在我的自动化开发的过程中,我需要高级语言和系统命令都不能完成的功能。如果没有C++/CLI我就必须要通过UI来实现,从而降低我程序的可靠性。而有了C++/CLI的功能,我就可以绕过UI操作了。总之,能够绕过UI操作的能力也体现出一个自动化测试人员的能力。从这个角度讲,测试人员有很多东西要学的。最后说一下,我自动化工作的要求是100%可靠,我还不能完全满足,因为使用我程序的人是那些手工测试的人,他们的使用环境的变化有可能引起一些问题的产生,基本上还不是我程序的问题,而是测试工具,或者其他模块的问题,我需要想办法去workaround。不过,随着一定时间问题的积累和解决,如果环境不变,应该可以达到100%可靠。(可是环境的变化是不会停止的,因此实际上很难达到永久的可靠,不过一段时间的可靠还是应该可以达到的,或者说我们的测试开发必须有这样一个目标,就如同软件开发的目标一样)
什么是 Selenium?
Selenium 是 ThoughtWorks 专门为 Web 应用程序编写的一个验收测试工具。据 Selenium 主页所说,与其他测试工具相比,使用 Selenium 的最大好处是:
“Selenium 测试直接在浏览器中运行,就像真实用户所做的一样。Selenium 测试可以在 Windows、Linux 和 MacintoshAnd 上的 Internet Explorer、Mozilla 和 Firefox 中运行。其他测试工具都不能覆盖如此多的平台。”
使用 Selenium 和在浏览器中运行测试还有很多其他好处。下面是主要的两大好处:
* 通过编写模仿用户操作的 Selenium 测试脚本,可以从终端用户的角度来测试应用程序。
* 通过在不同浏览器中运行测试,更容易发现浏览器的不兼容性。
Selenium 的核心,也称 browser bot,是用 JavaScript 编写的。这使得测试脚本可以在受支持的浏览器中运行。browser bot 负责执行从测试脚本接收到的命令,测试脚本要么是用 HTML 的表布局编写的,要么是使用一种受支持的编程语言编写的。
在下面的情况下,可以选择SeleniumRC进行功能测试。
* condition statements
* iteration
* logging and reporting of test results
* error handling, particularly unexpected errors
* database testing
* test case grouping
* re-execution of failed tests
* test case dependency
* screenshot capture of test failures
首先要下载SeleniumRC,不用安装,解压即可,可以看到这样几个目录,下图示:
selenium-server-1.0.1目录,是服务器端,他可以接受测试程序指令,并将测试结果返回测试程序。
在测试前必须先启动他,启动过程:开始-运行-cmd-cd <服务器端目录>-java -jar selenium-server.jar(服务器端其实就是个Jar文件)
然后就可以进行客户端,本文用C#来进行测试,首先建立一个C#类库工程,添加引用selenium-dotnet-client-driver-1.0.1目录下的所有DLL,具体如下图示。
下面,新建类SeleniumTest,具体代码如下:
1 [TestFixture] 2 public class SeleniumTest 3 { 4 private ISelenium selenium; 5 private StringBuilder verificationErrors; 6 7 [SetUp] 8 public void SetupTest() 9 { 10 selenium = new DefaultSelenium("localhost", 4444, "*iexplore", "http://localhost:2896/WebTestSite/"); 11 selenium.Start(); 12 13 verificationErrors = new StringBuilder(); 14 } 15 16 [TearDown] 17 public void TeardownTest() 18 { 19 try 20 { 21 selenium.Stop(); 22 } 23 catch (Exception) 24 { 25 // Ignore errors if unable to close the browser 26 } 27 Assert.AreEqual("", verificationErrors.ToString()); 28 } 29 30 [Test] 31 public void TheSeleniumTest() 32 { 33 selenium.Open("/WebTestSite/"); 34 selenium.Type("TextBox1", "qeq"); 35 selenium.Type("TextBox2", "qwe"); 36 selenium.Click("Button1"); 37 38 //判断是否出现alert("fail") 39 Assert.AreEqual("fail", selenium.GetAlert()); 40 41 selenium.Type("TextBox1", "123"); 42 selenium.Type("TextBox2", "123"); 43 selenium.Click("Button1"); 44 Assert.AreEqual("fail", selenium.GetAlert()); 45 46 //点击链接 47 selenium.Click("link=2"); 48 //等待 49 selenium.WaitForPageToLoad("30000"); 50 selenium.Click("link=3"); 51 selenium.WaitForPageToLoad("30000"); 52 53 } 54 [Test] 55 public void TestTitle() 56 { 57 selenium.Open("/WebTestSite/**.aspx"); 58 Assert.AreEqual("yourtitle", selenium.GetTitle()); 59 60 } 61 } |
这样,就建好了,可以打开NUit进行测试,也可以直接写个main进行测试。
seleniumhq官方文档:
http://seleniumhq.org/docs/05_selenium_rc.html#introduction