再谈自动化测试——我们在编写测试时,应该注意什么

本文首发于泊浮目的专栏:https://segmentfault.com/blog…背景最近项目在测试阶段陆陆续续的测出了一些bug.这个情况刚出现的时候,让笔者很困惑——平时我们的每个feature代码都是跟随着大量看起来考虑很周全的case进入代码仓库的,然而事实还是打了我们的脸.故在本文,笔者将会从最近的所学所想来谈谈编写测试的时候我们应该注意什么.AIR原则与BCDE原则前阵子看了一本书,里面提到了单元测试的一些原则:宏观上,单元测试要符合AIR原则微观上,单元测试的代码层面要符合BCDE原则AIR原则AIR即空气,单元测试亦是如此。当业务代码在线上运行时,可能感觉不到测试用例的存在和价值,但在代码质量的保障上,却是非常关键的。新增代码应该同步增加测试用例,修改代码逻辑时也应该同步保证测试用例成功执行。AIR原则具体包括:A: Automatic (自动化)I: Independent (独立性)R: Repeatable (可重复)简单的解释一下三个原则:单元测试应该是全自动执行的。测试用例通常会被频繁地触发执行,执行过程必须完全自动化才有意义。如果单元测试的输出结果需要人工介入检查,那么它一定是不合格的。单元测试中不允许使用System.out等方法来进行人工验证,而必须使用断言来验证。为了保证单元测试稳定可靠且便于维护,需要保证其独立性。用例之间不允许互相调用,也不允许出现执行次序的先后依赖。BCDE原则编写单元测试用例时,为了保证被测模块的交付质量,需要符合BCDE原则。B: Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。C: Correct,正确的输入,并得到预期的结果。D: Design,与设计文档相结合,来编写单元测试。E: Error,单元测试的目标是证明程序有错,而不是程序无错。为了发现代码中潜在的错误,我们需要在编写测试用例时有一些强制的错误输入(如非法数据、异常流程、非业务允许输入等)来得到预期的错误结果。在ZStack白盒集成测试中实践原则之前提到的原则是基于单元测试的,但在ZStack的白盒测试中也可以作为有价值的参考.戳此了解ZStack的白盒集成测试:https://segmentfault.com/a/11…由于ZStack的整套测试框架也是基于Junit扩展而来,因此也是一定程度上遵循了上面提到的AIR原则.除了A原则,I和R原则在一定程度上打了折扣:I: 如果上一个测试没有清理干净状态,则会影响下一个测试R: 基于上面提到的I,很有可能导致可重复性大打折扣当然,出现这些问题时则表示当前的代码中有bug.但单元测试则不会受到这样的影响——它能测出bug,AIR原则也得以保证.在本次示例中,我们将以VmInstance的创建API即——APICreateVmInstacneMsg作为测试对象.如果读者不是很了解上下文,也可以简单的看一下这个Case:OneVmBasicLifeCycleCaseBorder Test && Error Test边界测试是用来探测和验证代码在处理极端的情况下会发生什么.而错误测试为了保证ZStack在一些错误的状态下做出我们所期待的行为.那么我们该如何编写这样的测试呢?我们先来简单的理一下创建Vm的流程:VmImageSelectBackupStorageFlowVmAllocateHostFlowVmAllocatePrimaryStorageFlowVmAllocateVolumeFlowVmAllocateNicFlowVmInstantiateResourcePreFlowVmCreateOnHypervisorFlowVmInstantiateResourcePostFlow而其中每一个步骤可以分成好几个小步骤,以VmAllocateHostFlow为例:我们可以看到,根据不同的策略,allocateHost里还会有好几个flow.而由于松耦合架构,我们可以在测试中轻易的模拟极端问题的出现,如:找不到合适的BackupStorageHostCapacity的不够Agent返回的回复在某一个时刻与管理节点的状态不同…….以此类推,以上创建vm的8个flow都可以轻易模拟各种边界条件及错误情况.Correct Test && Design Test正确性测试听起来应该会很简单,(比如调用一个API,然后看结果返回是否正确)但如果放到集成测试中,我们还是可以拓展出一些额外的关注点的.还是以上面提到的createVm为例子,我们看到了8个flow,然后里面可能还嵌套着好几个子flow.如图所示:在编写正确性测试时,我们可以考虑额外关注以下几点:APIParam在各个Flow间中转时是否如预期关注管理节点内的服务:Flow之间调用的时序是否符合预期Flow之间流转时,业务目标状态是否符合预期关注管理节点外的服务:对于agent的请求是否符合预期在API调用完后,相关资源的目标状态是否符合预期而与文档结合的测试用例,则应当由团队的测试人员来定义.可以确定的是,这类的测试更加关注于API(即输入输出),而不是内部的状态.

March 12, 2019 · 1 min · jiezi

谈谈代码——如何避免写出糟糕if...else语句

本文首发于数据浮云:https://mp.weixin.qq.com/s?__…在写代码的日常中,if…else语句是极为常见的.正因其常见性,很多同学在写代码的时候并不会去思考其在目前代码中的用法是否妥当.而随着项目的日渐发展,糟糕的if…else语句将会充斥在各处,让项目的可维护性急剧下降.故在这篇文章中,笔者想和大家谈谈如何避免写出糟糕if…else语句.由于脱密等原因.文章中的示例代码将会用一些开源软件的代码或者抽象过的生产代码作为示范.问题代码当我们看到一组if…else时,一般是不会有什么阅读负担的.但当我们看到这样的代码时: private void validate(APICreateSchedulerMessage msg) { if (msg.getType().equals(“simple”)) { if (msg.getInterval() == null) { if (msg.getRepeatCount() != null) { if (msg.getRepeatCount() != 1) { throw new ApiMessageInterceptionException(argerr(“interval must be set when use simple scheduler when repeat more than once”)); } } else { throw new ApiMessageInterceptionException(argerr(“interval must be set when use simple scheduler when repeat forever”)); } } else if (msg.getInterval() != null) { if (msg.getRepeatCount() != null) { if (msg.getInterval() <= 0) { throw new ApiMessageInterceptionException(argerr(“interval must be positive integer”)); } else if ((long) msg.getInterval() * (long) msg.getRepeatCount() * 1000L + msg.getStartTime() < 0 ) { throw new ApiMessageInterceptionException(argerr(“duration time out of range”)); } else if ((long) msg.getInterval() * (long) msg.getRepeatCount() * 1000L + msg.getStartTime() > 2147454847000L) { throw new ApiMessageInterceptionException(argerr(“stopTime out of mysql timestamp range”)); } } } if (msg.getStartTime() == null) { throw new ApiMessageInterceptionException(argerr(“startTime must be set when use simple scheduler”)); } else if (msg.getStartTime() != null && msg.getStartTime() < 0) { throw new ApiMessageInterceptionException(argerr(“startTime must be positive integer or 0”)); } else if (msg.getStartTime() != null && msg.getStartTime() > 2147454847 ){ // mysql timestamp range is ‘1970-01-01 00:00:01’ UTC to ‘2038-01-19 03:14:07’ UTC. // we accept 0 as startDate means start from current time throw new ApiMessageInterceptionException(argerr(“startTime out of range”)); } if (msg.getRepeatCount() != null && msg.getRepeatCount() <= 0) { throw new ApiMessageInterceptionException(argerr(“repeatCount must be positive integer”)); } } if (msg.getType().equals(“cron”)) { if (msg.getCron() == null || ( msg.getCron() != null && msg.getCron().isEmpty())) { throw new ApiMessageInterceptionException(argerr(“cron must be set when use cron scheduler”)); } if ( (! msg.getCron().contains("?")) || msg.getCron().split(" “).length != 6) { throw new ApiMessageInterceptionException(argerr(“cron task must follow format like this : "0 0/3 17-23 * * ?" “)); } if (msg.getInterval() != null || msg.getRepeatCount() != null || msg.getStartTime() != null) { throw new ApiMessageInterceptionException(argerr(“cron scheduler only need to specify cron task”)); } } }亦或是这样的代码:try { for (int j = myConfig.getContentStartNum(); j <= rowNum; j++) { row = sheet.getRow(j); T obj = target.newInstance(); for (int i = 0; i < colNum; i++) { Field colField = ExcelUtil.getOneByTitle(metaList, titleList[i]); colField.setAccessible(true); String fieldType = colField.getType().getSimpleName(); HSSFCell cell = row.getCell(i); int cellType = cell.getCellType(); System.out.println(colField.getName()+"|"+fieldType+” | “+cellType); if(HSSFCell.CELL_TYPE_STRING == cellType){ if(“Date”.equals(fieldType)){ colField.set(obj, DateUtil.parse(cell.getStringCellValue())); }else { colField.set(obj, cell.getStringCellValue()); } }else if(HSSFCell.CELL_TYPE_BLANK == cellType){ System.out.println(“fieldName”+colField.getName()); if(“Boolean”.equals(fieldType)){ colField.set(obj, cell.getBooleanCellValue()); }else{ colField.set(obj, “”); } }else if(HSSFCell.CELL_TYPE_NUMERIC == cellType){ if(“Integer”.equals(fieldType) || “int”.equals(fieldType)){ colField.set(obj, (int)cell.getNumericCellValue()); }else { colField.set(obj, cell.getNumericCellValue()); } }else if(HSSFCell.CELL_TYPE_BOOLEAN == cellType){ colField.set(obj, cell.getBooleanCellValue()); } } result.add(obj); }} catch (InstantiationException | IllegalAccessException | ParseException e) { e.printStackTrace();}看完这两段代码,相信大家和我的心情是一样的:阅读它们的负担实在是太大了——我们要记住好几个逻辑判断分支,才能知道到底什么情况下才能得到那个结果.更别说维护的成本有多高了,每次维护时都要读一遍,然后再基于此来改.长此以往,我们的代码就变成"箭头式代码"了. //…………… //…………… //…………… //…………… //…………… //…………… //…………… //…………… //…………… //……………目标和关键指标前面说过,我们的目标是减少糟糕的if…else代码.那么什么是糟糕的if…else代码呢?我们可以简单的总结一下:两重以上的嵌套一个逻辑分支的判断条件有多个,如:A && B || C这种.其实这也可以看作变种的嵌套这样就可以看出来,我们的关键指标就是减少嵌套.常见Tips1. 三元表达式三元表达式在代码中也是较为常见的,它可以简化一些if…else,如: public Object getFromOpaque(String key) { return opaque == null ? null : opaque.get(key); }为什么说是一些呢?因此三元表达式必须要有一个返回值.这种情况下就没法使用三元表达式 public void putToOpaque(String key, Object value) { if (opaque == null) { opaque = new LinkedHashMap(); } opaque.put(key, value); }2. switch case在Java中,switch可以关注一个变量( byte short int 或者 char,从Java7开始支持String),然后在每个case中比对是否匹配,是的话则进入这个分支.在通常情况下,switch case的可读性比起if…else会好一点.因为if中可以放复杂的表达式,而switch则不行.话虽如此,嵌套起来还是会很恶心.因此,如果仅仅是对 byte,short,int和char以String简单的值判断,可以考虑优先使用switch.3. 及时回头 /* 查找年龄大于18岁且为男性的学生列表 / public ArrayList<Student> getStudents(int uid){ ArrayList<Student> result = new ArrayList<Student>(); Student stu = getStudentByUid(uid); if (stu != null) { Teacher teacher = stu.getTeacher(); if(teacher != null){ ArrayList<Student> students = teacher.getStudents(); if(students != null){ for(Student student : students){ if(student.getAge() > = 18 && student.getGender() == MALE){ result.add(student); } } }else { throw new MyException(“获取学生列表失败”); } }else { throw new MyException(“获取老师信息失败”); } } else { throw new MyException(“获取学生信息失败”); } return result; }针对这种情况,我们应该及时抛出异常(或者说return),保证正常流程在外层,如: / 查找年龄大于18岁且为男性的学生列表 / public ArrayList<Student> getStudents(int uid){ ArrayList<Student> result = new ArrayList<Student>(); Student stu = getStudentByUid(uid); if (stu == null) { throw new MyException(“获取学生信息失败”); } Teacher teacher = stu.getTeacher(); if(teacher == null){ throw new MyException(“获取老师信息失败”); } ArrayList<Student> students = teacher.getStudents(); if(students == null){ throw new MyException(“获取学生列表失败”); } for(Student student : students){ if(student.getAge() > 18 && student.getGender() == MALE){ result.add(student); } } return result; }使用设计模式除了上面的几个tips,我们还可以通过设计模式来避免写出糟糕的if…else语句.在这一节,我们将会提到下面几个设计模式:State模式Mediator模式Observer模式Strategy模式1. State模式在代码中,我们经常会判断一些业务对象的状态来决定在当前的调用下它该怎么做.我们举个例子,现在我们有一个银行的接口:public interface Bank { /* * 银行上锁 * / void lock(); /* * 银行解锁 * / void unlock(); /* * 报警 * / void doAlarm();}让我们来看一下它的实现类public class BankImpl implements Bank { @Override public void lock() { //保存这条记录 } @Override public void unlock() { if ((BankState.Day == getCurrentState())) { //白天解锁正常 //仅仅保存这条记录 } else if (BankState.Night == getCurrentState()) { //晚上解锁,可能有问题 //保存这条记录,并报警 doAlarm(); } } @Override public void doAlarm() { if ((BankState.Day == getCurrentState())) { //白天报警,联系当地警方,并保留这条记录 } else if (BankState.Night == getCurrentState()) { //晚上报警,可能有事故,不仅联系当地警方,还需要协调附近的安保人员,并保留这条记录 } } private BankState getCurrentState() { return BankState.Day; }}显然,我们涉及到了一个状态:public enum BankState { Day, Night}在不同的状态下,同一件事银行可能会作出不同的反应.这样显然很挫,因为在真实业务场景下,业务的状态可能不仅仅只有两种.每多一种,就要多写一个if…else.所以,如果按照状态模式,可以这样来重构:public class BankDayImpl implements Bank { @Override public void lock() { //保存这条记录 } @Override public void unlock() { //白天解锁正常 //仅仅保存这条记录 } @Override public void doAlarm() { //白天报警,联系当地警方,并保留这条记录 }}public class BankNightImpl implements Bank { @Override public void lock() { //保存这条记录 } @Override public void unlock() { //晚上解锁,可能有问题 //保存这条记录,并报警 doAlarm(); } @Override public void doAlarm() { //晚上报警,可能有事故,不仅联系当地警方,还需要协调附近的安保人员,并保留这条记录 }}2. Mediator模式在本文的第一段的代码中,其实是ZStack 2.0.5版本中某处的代码,它用来防止用户使用Cli时传入不当的参数,导致后面的逻辑运行不正常.为了方便理解,我们可以对其规则做一个简化,并画成图的样子来供大家理解.假设这是一个提交定时重启VM计划任务的“上古级”界面(因为好的交互设计师一定不会把界面设计成这样吧…).规则大概如下:2.1 Simple类型的SchedulerSimple类型的Scheduler,可以根据Interval,RepeatCount,StartTime来定制一个任务.2.1.1 当选择Simple类型的任务时,Interval,StartTime这两个参数必填2.1.2 当填好Interval,和StartTime,这个时候已经可以提交定时任务了2.1.3 RepeatCount是个可选参数2.2 Cron类型的SchedulerCron类型的Scheduler,可以根据cron表达式来提交任务.2.2.1 当填入cron表达式后,这个时候已经可以提交定时任务了在这里请大家思考一个问题,如果要写这样的一个界面,该怎么写?——在一个windows类里,先判断上面的可选栏是哪种类型,然后根据文本框里的值是否被填好决定提交按钮属否亮起…这算是基本逻辑.上面还没有提到边界值的校验——这些边界值的校验往往会散落在各个组件的实例里,并通过互相通信的方式来判断自己应该做出什么样的变化,相信大家已经意识到了直接无脑堆if…else代码的恐怖之处了吧.2.3 使用仲裁者改善它接下来,我们将会贴上来一些伪代码,方便读者更好的理解这个设计模式/* * 仲裁者的成员接口 * /public interface Colleague { /* * 设置成员的仲裁者 * / void setMediator(Mediator mediator); /* * 设置成员是否被启用 * / void setColleagueEnabled(boolean enabled);}/* * 仲裁者接口 * /public interface Mediator { /* * 当一个组员发生状态变化时,调用此方法 * / void colllectValueChanged(String value);}/* * 含有textField的组件应当实现接口 /public interface TextField { String getText();}/* * 当一个组件的值发生变化时,ValueListener会收到相应通知 * /public interface ValueListener { /* * 当组员的值变化时,这个接口会被调用 * / void valueChanged(String str);}定义了几个接口之后,我们开始编写具体的类:用于表示Simple和Cron的checkBoxpublic class CheckBox { private boolean state; public boolean isState() { return state; } public void setState(boolean state) { this.state = state; }}Buttonpublic class ColleagueButtonField implements Colleague, ValueListener { private Mediator mediator; @Override public void setMediator(Mediator mediator) { this.mediator = mediator; } @Override public void setColleagueEnabled(boolean enabled) { setEnable(enabled); } private void setEnable(boolean enable) { //当true时去掉下划线,并允许被按下 } @Override public void valueChanged(String str) { mediator.colllectValueChanged(str); }}以及几个Textpublic class ColleagueTextField implements Colleague, ValueListener, TextField { private Mediator mediator; private String text; @Override public void setMediator(Mediator mediator) { this.mediator = mediator; } @Override public void setColleagueEnabled(boolean enabled) { setEnable(enabled); } private void setEnable(boolean enable) { //当true时去掉下划线,并允许值输入 } @Override public void valueChanged(String str) { mediator.colllectValueChanged(str); } @Override public String getText() { return text; }}SchedulerValidator的具体实现SchedulerValidatorImpl就不贴上来了,里面仅仅是一些校验逻辑.接着是我们的主类,也就是知道全局状态的窗口类public class MainWindows implements Mediator { private SchedulerValidator validator = new SchedulerValidatorImpl(); ColleagueButtonField submitButton, cancelButton; ColleagueTextField intervalText, repeatCountText, startTimeText, cronText; CheckBox simpleCheckBox, cronCheckBox; public void main() { createColleagues(); } /* * 当一个组员发生状态变化时,调用此方法 * 组件初始化时都为true */ @Override public void colllectValueChanged(String str) { if (simpleCheckBox.isState()) { cronText.setColleagueEnabled(false); simpleChanged(); } else if (cronCheckBox.isState()) { intervalText.setColleagueEnabled(false); repeatCountText.setColleagueEnabled(false); startTimeText.setColleagueEnabled(false); cronChanged(); } else { submitButton.setColleagueEnabled(false); intervalText.setColleagueEnabled(false); repeatCountText.setColleagueEnabled(false); startTimeText.setColleagueEnabled(false); cronText.setColleagueEnabled(false); } } private void cronChanged() { if (!validator.validateCronExpress(cronText.getText())) { submitButton.setColleagueEnabled(false); } } private void simpleChanged() { if (!validator.validateIntervalBoundary(intervalText.getText()) || !validator.validateRepeatCountBoundary(repeatCountText.getText()) || !validator.validateStartTime(startTimeText.getText())) { submitButton.setColleagueEnabled(false); } } private void createColleagues() { submitButton = new ColleagueButtonField(); submitButton.setMediator(this); cancelButton = new ColleagueButtonField(); cancelButton.setMediator(this); intervalText = new ColleagueTextField(); intervalText.setMediator(this); repeatCountText = new ColleagueTextField(); repeatCountText.setMediator(this); startTimeText = new ColleagueTextField(); startTimeText.setMediator(this); cronText = new ColleagueTextField(); cronText.setMediator(this); simpleCheckBox = new CheckBox(); cronCheckBox = new CheckBox(); }}在这个设计模式中,所有实例状态的判断全部都交给了仲裁者这个实例来判断,而不是互相去通信.在目前的场景来看,其实涉及的实例还不是特别多,但在一个复杂的系统中,涉及的实例将会变得非常多.假设现在有A,B两个实例,那么会有两条通信线路:而有A,B,C时,则有6条线路当有4个实例时,将会有12个通信线路当有5个实例时,会有20个通信线路以此类推…这个时候,仲裁者模式的优点就发挥出来了——这些逻辑如果分散在各个角色中,代码将会变得难以维护.3. Observer模式ZStack源码剖析之设计模式鉴赏——三驾马车结合本文的主题,其实观察者模式做的更多的是将if…else拆分到属于其自己的模块中.以ZStack的为例,当主存储重连时,主存储模块可能要让模块A和模块B去做一些事,如果不使用观察者模式,那么代码就会都耦合在主存储模块下,拆开if…else也就不太可能了.改进之前的仲裁者例子观察者模式一般是通过事件驱动的方式来通信的,因此Observer和Subject一般都是松耦合的——Subject发出通知时并不会指定消费者.而在之前仲裁者模式的例子中,仲裁者和成员之间紧耦合的(即他们必须互相感知),因此可以考虑通过观察者模式来改进它.4. Strategy模式通常在编程时,算法(策略)会被写在具体方法中,这样会导致具体方法中充斥着条件判断语句。但是Strategy却特意将算法与其他部分剥离开来,仅仅定义了接口,然后再以委托的方式来使用算法。然而这种做法正是让程序更加的松耦合(因为使用委托可以方便的整体替换算法),使得整个项目更加茁壮。ZStack源码剖析之设计模式鉴赏——策略模式小结在这篇文章中,笔者和大家分享几个减少if…else的小tips,由于这些tips都会有一定的限制,因此还向大家介绍了几个能够避免写出糟糕的if…else的设计模式,并使用观察者模式简单的改进了仲裁者模式的例子. ...

January 18, 2019 · 6 min · jiezi