测试开发之路 ---- 可读性,可维护性,可扩展性

news2024/11/28 10:40:58

目录

前言

测试框架与测试脚本的目标(部分)

分层

使用类似 xml 这种可扩展性强的语义存储数据

代码复用:抽象一切可抽象的,减少一切可能的代码相似与重复

活用 java 注解和反射(python 中应该也有相关的机制)

一个模块知道的越少越好

case 的隔离


前言

测试开发是软件开发中的一个重要领域,它涉及编写和维护自动化测试脚本、执行测试、生成报告等任务。在测试开发的过程中,可读性、可维护性和可扩展性是非常重要的考虑因素。

可读性是指测试代码的清晰度和易读性。一个具有良好可读性的测试代码可以让其他开发人员轻松理解和维护,减少错误和改动的风险。

可维护性是指测试代码的易于维护和修改。一个具有良好可维护性的测试代码可以快速适应需求变化,并且容易修复和调试。

可扩展性是指测试代码的易于扩展和重用。一个具有良好可扩展性的测试代码可以方便地添加新的测试用

测试框架与测试脚本的目标(部分)

  1. Tests as Documentation(你能很容易通过测试脚本理解被测软件的功能) ----可读性
  2. Defect Localization(通过测试脚本能够快速定位 bug 的位置) ----可读性与隔离性
  3. Tests should be easy to write and maintain(测试脚本应该是容易编写和维护的) ----可维护性
  4. Tests should be easy to improve when product changes(当产品变化时,测试应该是很容易扩展自身以适应变化的)----可扩展性

分层

为了提高我们测试脚本的质量, 分层显然是最常用的方法. 想象一下如果我们把根测试所有相关的东西都放在脚本里那是怎样的一种灾难,每次你去看脚本的时候都会一个头两个大。其一你不知道脚本在干嘛,其二你根本不敢随便动这个脚本。深怕动了哪里就破坏了这条脚本。所以当我们作了分层后,将责任划分出去,分而治之,每一层负责特定的功能,其他层不用担心这些特定的功能。
原则:
测试脚本只关注被测的功能逻辑,其他一切责任分层出去,或交给框架作,或交给其他模块作。
常用的分层方式:

  1. 数据驱动,把测试参数的构建分离出去,减少脚本复杂度
  2. 注册式数据管理,我们把测试数据的构建与销毁分离出去,减少脚本复杂度
  3. page object,UI 自动化常用的模式. 我看到的大家常用的方式就是把页面元素的定义分到单独的类中.下面来看看我曾经怎么做这个分层的.

脚本是这么写的:

driver.page("登陆页面").sendKeys("用户名输入框", "Admin").sendKeys("密码输入框", "1234567").click("登录按钮");

可以看到我定义了一个 page 的概念.一个页面所有的元素都在这个 page 里. 只要脚本中选定了某个 page,那么他就能随意控制页面操作. 那么 page object 在哪呢?脚本中我们看不到调用 page object 的操作,我们看不到你到底用 xpath 查找的元素还是用 id 还是用 name。请看下一段 xml 定义

<page name="登陆页面">
    <Element eleName="用户名输入框" xpath="//input[@type='text']" id="userNameInput"  ifBaseElement="true"></Element>
    <Element eleName="密码输入框" xpath="//input[@type='password']" ifBaseElement="true"></Element>
    <Element eleName="登录按钮" xpath="//button" ifBaseElement="true"></Element>
</page>

page object 在这里,这里面用中文定义了元素名称,以及控件元素到底是用什么方式去查找等信息。当脚本引用任何页面的时候,框架都会去缓存中读取此页面信息,并执行页面元素的控制操作。可以看到我们不仅把页面元素的定义分层出去,还把页面元素的查找过程也都分层出去了。 而且我们可以用自然语言定义控件的名字(英语还是汉语都可以),所以就像上面的代码一样,脚本在做什么一目了然。这就是可读性,我们做的事情跟之前没什么分别,但是我们把责任划分的更详细,脚本中只剩下业务逻辑。我们有一个原则就是脚本中只有业务逻辑。其他一切不相关的要不交给框架,要不交给其他层的模块。

使用类似 xml 这种可扩展性强的语义存储数据

我们看到上面的 xml 里还有一个 ifBaseElement 属性。 这个是什么呢? 它就是给这些页面元素打个标签,这些控件是属于页面基本元素,这样我们可以通过下面一段代码把所有带有这个标签的页面元素全找出来。

List<String> eleNames = driver.getBaseElementsNameOfPage("登陆页面");

for(String eleName:eleNames){
    WebElement element = driver.findElement("登陆页面."+eleName+"");
    Assert.assertNotNull(element);
}

看到效果了么? 这样我可以验证所有这些页面基本元素在页面中是存在的,这就是我们 UI 自动化策略中的静态元素验证。我们不用再一行一行去写代码验证了。而是通过 xml 这种方便扩展的定义遍历出所有的静态元素。这是一种方式,你也可以通过定义 xml 文件的属性扩展出很多功能。这是可扩展性。记得我的那篇数据驱动及其变种么?之后的关键字驱动框架就使用 xml 在数据驱动的基础上扩展而来的。同时 xml 是一种很清晰很结构化的定义方式。实际上 xml 本身的可读性就不低。可扩展性和可读性上去了,可维护性也就差不到哪去

代码复用:抽象一切可抽象的,减少一切可能的代码相似与重复

记住一点:代码越少越简单,维护起来就越方便。简单即是美
还是用 UI 自动化这个例子吧,我们看到上面讲 xml 可扩展性的时候。我们可以通过定义一个标签 ifBaseElement 来帮助做静态元素验证。但是 java 里普遍也就是用 dom4j 等工具遍历 xml 文件,你为 ifBaseElement 需要写一套遍历,你加另一个属性可能还要一套遍历。或者 xml 树结构改了,我们在已有的标签下又加了一套新的标签等情况。都需要重写遍历。而且一层又一层的 for 循环也挺让人崩溃的。外人不知道你这段代码在干吗。可维护性,可读性,可扩展性都差的要死。那我们一般怎么做呢。看下面一个例子。
:有个方案是写迭代器(请 Google 迭代器模式),for 循环过多,而且复杂的时候一般使用此模式增加可维护性和可读性。不过在 xml 遍历的场景中,应变能力不强。xml 变化,迭代器也必须变化。所以我一般使用解释器模式遍历 xml 和 json

XMLParser.parser(pageObject, "page/Element$(ifBaseElement=true).eleName");

OK,大家看到了吧,一个解释器接收一个 string 和 xml 对象为参数。String 就是我们自定义的语法,上面的意思就是取出 page 节点下的 Element 节点中所有 ifBaseElement 属性值为 true 的 eleName 属性的值。这样就满足上一个例子的遍历出所有的页面基本元素的需求了。通过定义一个简单易懂的语法(一开始我想做成根 sql 语句一样的语法的,后来觉得太麻烦了)满足了我们各方面的需求:使用者很容易使用,也很容易看懂这段代码再作什么。很容易接受变化,xml 改变了我们改变一下字符串就行了。扩展性也很好。语法很容易进化。基本上可读性,可扩展性,可维护性都做到了。
举些 json 的例子:

dataList[id=89898,54546,90723,1,90724,90725,54545]/*     取json中dataList数组中 id为这些的所有的值,*代表查询所有
dataList[id=89898,54546,90723,1,90724,90725,54545]/id   取json中dataList数组中 id为这些的所有的值,id代表只查询id
dataList[0~5]/*                                       取json中dataList数组中前6个元素
dataList[*]/*                                         取json中dataList数组中所有元素

想知道实现方式的自行 google 解释器模式吧,这个模式比较大,我说不清楚

再举个例子,我们写脚本的时候一定会验证返回值,有时候这个值可能是简单的数字或者字符串。有些时候可能就是复杂的对象了。这个时候对复杂对象作验证就比较痛苦,每个属性都写断言的方式简直要人命。为了解决这个问题,我们的方式是 java 反射机制加上责任链模式

VerifyHandler handler = VerifyHandlerFatory.createVerifyHandler();
String[] notverify = {"Task:id"};
handler.PassRequest(copyTask, sourceTask, notverify);

大家看上面的代码,就是比较两个 task 对象是否相等,第一行代码是创建一个责任链对象,第二行代码规定了什么东西不需要验证,因为 task 的 ID 是随机生成的不可能相等。最后我们把两个对象仍进去就行了。你不用管它怎么验证的,责任链在运行到 javaBean 类型的时候,就会用 java 反射解析两个对象的每一个属性并调用链表中其他的节点做相应的断言。是不是很好用?不仅仅是 javaBean 类型,JSON,数组,List,Map,File 你全都能不管三七二十一的仍进责任链里。这下子写脚本的人可爽了,以前我们最怕的就是一个 ORM 映射出来的字段百八十个的,光是写断言就写到手软。现在完全木有这个问题了。如果有新的验证类型出现,你只需要在责任链表里增加一个节点对应这个类型作验证就好了。不需要一大堆的 if else 递归调用的,可维护性,可扩展性很不错。现在可读性也好了,就一行验证代码,你肯定知道脚本在干吗。

说一下大概实现思路吧。责任链可以是单向链表,也可以是循环链表,甚至你可以发展成树形结构 (暂时我在测试中没碰见这种复杂结构,开发那常碰见),每个节点对应一种类型,如果判断当前类型是该节点应该处理得,就处理。如果不是就传递给下一个节点处理,依次类推,直到遇到跳出链表的点(例如验证结束)或者是到达链表的尾部。中间如果遇到容器类型例如一个 javaBean 或者一个 List 等等,就循环遍历每一个值依次传递下去。你可以理解为你为链表作线性遍历,但是链表给传递进来的对象做的事树的先序遍历 (深度优先)。


上图所有的类,所有的节点继承 VerifyHandler 抽象类,VerifyAlgoChain 是链表的容器,VerifyHandlerFatory 是组装链表的工厂类。下面贴一个 List 类型的代码

/**
 * 验证list类型
 * 
 * @author Gaofei Sun
 *
 */
public class ListType extends VerifyHandler {

    @Override
    public Boolean PassRequest(Object actualValue, Object expectedValue, String fieldName_no,Params info,String[] notVerifyFlag) {
        // 如果对象属于List类型就验证
        if (expectedValue.getClass().isAssignableFrom(List.class)
                || expectedValue.getClass().isAssignableFrom(ArrayList.class)
                || expectedValue.getClass().isAssignableFrom(LinkedList.class)) {

            List<?> expectedValueList = (List<?>) expectedValue;
            List<?> actualValueList = null;
            try {
                actualValueList = (List<?>) actualValue;
            } catch (ClassCastException e) {
                e.printStackTrace();
                Assert.assertTrue(fieldName_no + "返回值并不是List类型,而是:" + actualValue.getClass() + " 类型", false);
            }

            if (actualValue == null) {
                if (expectedValueList.size() == 0) {
                    return true;
                } else {
                    Assert.assertTrue(fieldName_no + " 返回值中的List为空,但是预期值不是", false);
                }

            }

            Assert.assertEquals("输入的List:" + fieldName_no + " 的大小与返回的不等", expectedValueList.size(),
                    actualValueList.size());

            VerifyHandler handler = VerifyHandlerFatory.createVerifyHandler();
            for (int i = 0; i < expectedValueList.size(); i++) {
                                // 取出所有对象继续在责任链表中传递。
                handler.PassRequest(actualValueList.get(i), expectedValueList.get(i), fieldName_no,info,notVerifyFlag);
            }
            return true;
        }
        // 不属于List类型,传递给下一个节点
        return nextHandler.PassRequest(actualValue, expectedValue, fieldName_no,info,notVerifyFlag);
    }
}

好了具体的实现原理请大家自行 Google 责任链模式
当然了大家也许会说我不用这个屁的责任链模式也可以阿,我写 N 个 if else 加递归调用加 java 反射也可以实现。那么我们通篇都在说什么呢?可读性,可扩展性,可维护性。如果你这么写你指望谁愿意接手你的代码。反正我写这种代码出来我老大肯定抽我。多少个公司的代码规范里都是严禁出现这种情况的

活用 java 注解和反射(python 中应该也有相关的机制)

这个例子是在 测试数据管理策略一章中讲到的注册式数据管理。看一下下面的例子

@DataBaseFile(filePath="defaultProject.xls",scope=Scope.CLASS)
public class UnitTestNew extends UnitCaseBase{

上面的代码在类的基础上加一个 DataBaseFile 的注解,然后再基类中我们有如下定义:

    /**
 * 根据数据文件内容解析出的数据库执行语句的集合,用来初始化和销毁数据库。 初始化方法读取数据文件执行数据库insert语句并给此变量赋值,销毁方法在测试结束后读取此变量执行销毁操作
 */
private List<DataEntity> dataEntityList; 
    /**
 * 表明子类的DataBaseFile注解
 */
private DataBaseFile data;
/**
 * 表明子类的DataBaseFile注解中数据文件的路径信息
 */
private String[] filesPath;
/**
 * 表明子类的DataBaseFile注解中执行初始化和销毁的策略信息
 */
private Scope scope;

    /**
 * 构造方法,获取子类的@DataBaseFile信息
 */
public UnitCaseBase(){
    register = false;
    data = this.getClass().getAnnotation(DataBaseFile.class);
    dataEntityList = new ArrayList<DataEntity>();
    if(data!=null){
        this.filesPath = data.filePath().split(",");
        this.scope = data.scope();
    }
}

我们可以看到,在子类中,我们使用注解的方式制定数据文件的路径和作用域,基类默认构造方法会使用反射的方式去读取注解的信息,然后再基类中定义好了方法去做测试测试数据的初始化和销毁。如下:

    // 供子类重写,用于setup测试用例
protected void methodSetUp(){}

// 供子类重写,用户销毁测试用例
protected void methodTearDown(ITestResult result){}

// 供子类重写,用于在测试类开始前执行初始化工作
protected void classSetUp(){}

// 供子类重写,用于在测试类结束后执行销毁工作
protected void classTearDown(){}

/**
 * 测试用例的初始化
 */
@BeforeMethod
protected void methodDataBaseSetUp(){
    this.setUpDataBase(Scope.METHOD);
    this.methodSetUp();
}
/**
 * 测试用例的销毁
 */
@AfterMethod
protected void methodDataBaseTearDown(ITestResult result){
    this.methodTearDown(result);
    // 判断子类是否注册了测试数据
    if(dataEntityList!=null&&register.equals(true)){
        this.destoryData();
    }
    this.tearDownDataBase(Scope.METHOD);
}
/**
 * 测试类的初始化
 */
@BeforeClass
protected void classDataBaseSetUp(){
    this.setUpDataBase(Scope.CLASS);
    this.classSetUp();
}
/**
 * 测试类的销毁
 */
@AfterClass
protected void classDataBaseTearDowm(){
    this.classTearDown();
    this.tearDownDataBase(Scope.CLASS);
}

我们可以看到基类定义的 before 系列的方法中有着针对数据作用域进行初始化和销毁的操作。并且留给子类接口扩展销毁和初始化操作。一般情况子类只需要使用注解规定数据文件的路径和作用域就可以了。这种基类定义行为,子类定义实现的方式是 模板模式 的变种. 这下我们可以看到我们的脚本类只需要继承这个基类,使用一个简单的注解就可以不用管数据的销毁与创建了. 我十分推荐这种方式制作测试框架.
活用注解和反射很重要, 很多工具和框架都离不开这两种机制

一个模块知道的越少越好

其实这个原则跟分层有点像,把责任划分出去了,知道的就少了. 现在让我们来看看下面一个例子,这个例子我之前关于数据驱动及其变种的帖子里的一部分. 用来解析作为数据驱动的 xml 文件中的数据类型,我们知道 java 是一门强类型语言,从 xml 读取出来的都是 String 类型,我们需要对其作类型转换. 如果我在脚本里做类型转换的话无疑太痛苦了.所以我们交给框架来做,我们希望脚本是这样的.

    @Test(dataProvider="unitDataProvider",dataProviderClass=UnitDataProvider.class)
@DataFile(filePath="test.xml")
public void test(List<String> in,File file, String mock,String out) throws SQLException{

如上面代码,测试脚本中用一个 DataFile 注解定义数据文件位置,框架用 java 反射读取内容,这一点和上面的例子很像. 数据类型的转换也交给框架来做.这样测试脚本就 知道的很少 然后我们再看框架. 框架专门有一个地方负责读取这些文件.然后解析并作类型转换操作. 可是我们发现这个类型转换操作其实是很复杂的. 你不确定你穿进来的对象里是不是还包含着另一个对象或者容器.java 里的对象就是个不确定深度的树结构. 我们一般的思路就是写 N 个 if else 判断到底是什么类型,然后做类型转换. 碰到容器类型就递归调用. 这样的话我们发现这个模块就非常复杂了.N 多的 if else 和递归调用无疑是个灾难. 之后不管谁来接手这段代码心里都会骂娘的.

所以我们把这个职责也分层出去成为一个模块. 我们不用那么多的 if else 了, 每个类型都单独一个类型转换算法. 提供一种机制让每一个算法之间都能互相调用 (模拟递归).所有的算法都实现一个接口.如下

import org.springframework.stereotype.Component;

import InterfaceTool.paramLoader.params.Param;

@Component
public interface TypeConvert {
    public Parameter convertType(Param value);
}

这个接口只有一个方法,就是类型转换算法. 所有的类型转换算法都要实现这个接口. 有一个工厂类专门负责创建算法对象.

public class TypeConvertFactroy {
    private static ConcurrentHashMap<String, TypeConvert> map = new ConcurrentHashMap<String, TypeConvert>();

    public static TypeConvert createTypeConvert(String type) {
        if (type == null || type.equals("")) {
            type = "String";
        }
        if (map.containsKey(type)) {
            return map.get(type);
        } else {
            TypeConvert obj = null;
            try {
                type = Tools.convertStringToUp(type);
                obj = (TypeConvert) Tools.reflectObject(
                        "InterfaceTool.paramLoader.typeConvert." + type + "Convert");
                map.putIfAbsent(type, obj);
            } catch (Exception e) {
                e.printStackTrace();
                Assert.assertTrue("没有找到 " + type + " 的参数类型,请核对是否输入错误的参数类型或请在系统中增加对应的参数类型", false);
            }
            return obj;
        }
    }
}

上面我们看到这个工厂类负责维护一个 map,map 里装的就是所有的算法 (缓存). 有一个细节就是

obj = (TypeConvert) Tools.reflectObject("InterfaceTool.paramLoader.typeConvert." + type + "Convert");

这是利用 java 反射去生成算法对象, 好处是以后加入新的算法类型的时候,只要在特定路径下定义一个特定名字的算法类就行了. 这样这个工厂类就能自动创建这个对象而不用任何的代码变动 (可扩展性)。

OK,那我们看看其他模块怎么调用算法的。

// 将Param转型成真正的参数
TypeConvert convert = TypeConvertFactroy.createTypeConvert(param.getType());
Parameter obj = convert.convertType(param);

上面我们看到调用方只要通过工厂类创建 TypeConvert 类型的对象就可以了。直接使用 convert 算法得到转型后的结果。调用方不需要知道他到底创建了哪个类型的算法 (因为工厂类返回的类型是所有算法的接口类型),不知道里面到底做了什么。只需要知道这么做是在做类型转换就可以了。这是最简单的 策略模式。对调用方来说,它知道的非常少,大家应该可以感觉出来这几个模块的可读性,可扩展性和可维护性了么?顺便说一下工厂模式的这种设计方法也是在模拟递归调用,也就是说算法内部调用其它算法的时候也是通过这个工厂类来调用的
上面的例子涉及到了表驱动,工厂模式,策略模式,注解,反射等知识,不清楚的请自行 Google

我发现篇幅已经好长了~,说最后一个例子吧。。还是用 UI 自动化那个例子。下面是代码

driver.page("登陆页面").sendKeys("用户名输入框", "Admin").sendKeys("密码输入框", "1234567").click("登录按钮");

大家可以猜到上面用的肯定不是 webDriver 原生的接口。我一定是封装过一层的,可我没有都重新定义 dirver 的接口(太多了,而且也没必要)。如果我们想调用原本的接口呢? 也简单,直接调用就行了(我没有重写 webDriver 的东西)。这时候大家可能猜到了,直接用继承来扩展 webDriver 不就得了。像下面这个样子:

public class MyChromeDriver extends ChromeDriver {

可是这样就出现问题了。我们知道 webDriver 里有好几种 driver。难道我们为了实现 page 这个功能要为每个 driver 都扩展一次么。肯定不行啊。多少的代码重复呢。 有的同学说你可以写个适配器阿,把 driver 传到适配器里不就行了。适配器里传得是所有 dirver 的基类类型就行了,在适配器里重新定义你需要的接口。像下面

public MyDriver(RemoteWebDriver driver, String pageName) {
        this.driver = driver;
    this.currectPage = pageName;
    pageObjectMap = PageObjectFactory.getPageObjects();
}

可是这样我们就无法通过这个适配器调用 driver 原本的接口了,而且我要让写脚本的人知道两套东西,一个 driver 和一个适配器,还需要用户去组装这个适配器。 也许有些同学说干嘛那么纠结,这样干活就足够了。但是我们是完美主义追求者,根据知道的最少原则我希望测试人员能通过简单易懂的方式完成工作。在这里我们让用户少知道点东西,在另一个地方我们让他们少知道点,聚沙成塔。最后我们的质量就上去了。OK,我们来看看到底怎么做。

首先我们知道,我们有了一个适配器,我们有了一个通过继承得来的 driver。下面我们要考虑怎么把他们两个合成一个东西。 我们知道 java 里是没有多重继承的。如果有的话我们就不用烦了。所以我们弄了个山寨版的多重继承 ---- 通过内部类。 看下面例子

public class MyChromeDriver extends ChromeDriver {
    private Map<String, Page> pageMap = new HashMap<String, Page>();

    @Override
    public void get(String url) {

        super.get(url);
    }

    public MyChromeDriver(ChromeOptions options){
        super(options);
    }

    public Page page(String pageName){
        // 如果缓存中已经有该page的对象就使用缓存中的,如果没有创建一个新的
        if(pageMap.containsKey(pageName)){
            return pageMap.get(pageName);
        }else{
            Page page = new Page(pageName);
            pageMap.put(pageName, page);
            return page;
        }
    }

    public Page page(){
        return new Page(null);
    }

    public class Page extends MyDriver{
        public Page(String pageName) {
            super(MyChromeDriver.this,pageName);
        }

    }

上面我们通过类内部再定义一个内部类,这个内部类继承了我们之前说的那个适配器类。这样我们就让一个类中拥有两个类的行为了。是不是很简单,我们把这个内部类命名为 page,适配器类中定义跟原生的 webdriver 一样的接口名称,例如,sendkey,click 等等。这样调用方很自然以为这其实就是一个机制。就像下面的代码

driver.page("登陆页面").sendKeys("用户名输入框", "Admin").sendKeys("密码输入框", "1234567").click("登录按钮");

符合人类思维的结构和接口命名,是不是看起来好多了。不想通过 page 的概念控制也可以 直接一个 driver.findElement 的方式去做。额,我嘴比较笨,大家自行体会吧。

case 的隔离

这回真是最后一个了。。。时间有限,我就简单点说了。 其实 case 的隔离主要分为两方面:第一,不依赖产品接口创建和销毁测试数据。第二:每个用例都有自己的测试数据,不跟其他 case 共享,最简单最有效的做法就是做 隔离数据,而且保险起见,case 运行结束后数据都删除掉。

具体实现还是看我那篇测试数据管理策略的帖子把。我说说为什么这么做,我发现其实大多数人都不是这么做的。就说第一点,很多人都是依赖产品接口做的测试数据准备。 测试这个接口前会调用很多别的接口先创建数据。其实这样你的测试粒度已经很粗了,你没有把被测功能隔离开,这是一个标准的粗粒度的集成测试。这么做不是说不行。而是一旦创建数据的接口 bug 了,你觉得你得有多少 case 会直接 fail。你得脚本里之前调用了那么多的接口,一旦 bug 了你能确定到底是哪个接口的 bug 么?举个例子,get 一个 user 的时候出错了,你能确定就是 get 接口出错了么? 很可能是 adduser 的接口就出错了。所以记得我们一开始就说的 测试脚本的目标 么,脚本能根据脚本准确定位 bug 的位置。

我们接下来再说第二点,很多人反对做隔离数据,觉得太麻烦没有必要。 可是我想说数据库是对所有 case 可见的。不做隔离数据的话,case 之前肯定互相影响。例如我这个脚本测试 listuser,把所有 user 都展示出来。 结果后来又测试一个 case 叫 adduser。adduser 就会导致 listuser 的失败。也许有些同学会说我们可以定义 case 执行顺序,我们可以定义不同类型的数据等等,但是这些都是不靠铺的,你的 case 可能依然互相影响。而且这些的前提是只有你一个人在做自动化,而且自动化的规模比较小。 如果是好几个人在做自动化,你能指望记住每个人的 case 执行顺序和数据类型么。 如果来个新人不知道这些规则呢? 如果 case 达到几千的量,你还记得住你的顺序和规则了么?

所以我一直在说数据的管理很重要,我在这一系列的帖子里第一篇就是测试数据管理是由原因的。

OK 就说到这把,篇幅好长了,其他的以后又机会说吧。分享的不好,大家见谅。最后做个总结,其实保证可读性可维护性 和 可扩展性 的方法很多,我列出来的只是冰山一角。不仅仅是脚本的,还有框架的。都要保证这三个纬度。也许大家觉得这没什么卵用,不保证这些我照样干活,用不着学这些有的没的,什么设计模式数据结构的,都是用来装逼得。
我猜一定有很多人是这么想的。那么大家其实细想想,如果按我上面的方法做了,是不是长远上讲其实是增加了效率的。 一旦产品改动,架构改动或者产品出 bug 的时候。会不会很爽? 如果大家仔细想想得话,答案是肯定的。 退一万步讲,如果你的代码里真的是各种 if else for 循环的嵌套好几层,还外带递归调用的,没什么分层没什么模块的。这样的代码你指望谁来接手? 时间长了你不忘么?就算你不忘你能保证你呆在公司一辈子么?你走了留个烂摊子给后辈么? 我们不要嘴里骂着开发的代码质量垃圾的同时,自己还写着垃圾代码吧

So,大家不要排斥学习开发的知识,一旦经历过数千级别的 case,长达数年的自动化项目,遇到过这样那样的坑的时候。你就会知道我说的这些有多重要。

  作为一位过来人也是希望大家少走一些弯路

在这里我给大家分享一些自动化测试前进之路的必须品,希望能对你带来帮助。

(WEB自动化测试、app自动化测试、接口自动化测试、持续集成、自动化测试开发、大厂面试真题、简历模板等等)

相信能使你更好的进步!

点击下方小卡片

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/759441.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何从视频中提取音频?分享三个免费的方法给大家!

在数字时代&#xff0c;视频和音频的使用越来越广泛。有时&#xff0c;您可能希望从视频中提取音频&#xff0c;以便单独使用或与他人分享。无需购买昂贵的软件或具备专业技能&#xff0c;下面将介绍三种免费的方法&#xff0c;帮助您从视频中提取音频。这些方法简单易行&#…

Unity学习笔记--siki学院保卫萝卜

生命周期&#xff1a; 在同一个脚本中的执行先后顺序&#xff1a;先左后右 Inspector 赋值 > 外部调用 > Awake > OnEnable > Start 脚本对象的失活与激活不作用于Awake方法&#xff0c;当方法中只有Awake方法时&#xff0c;控制脚本激活失活的对勾会消失掉 当…

vue3 中ref的函数用法

简介 这里说的ref不是响应式ref,是用在组件身上的ref标识&#xff0c;一般都是ref“某一个字符串”&#xff0c;本文介绍第二种用法&#xff0c;ref“()>{}”,对没错&#xff0c;ref可以等于一个回调函数 ref可以是一个回调 <el-input:ref"(vc: any) > (inputAr…

lwip-2.1.3自带的httpd网页服务器使用教程(三)使用CGI获取URL参数(GET类型表单)

上一篇&#xff1a;lwip-2.1.3自带的httpd网页服务器使用教程&#xff08;二&#xff09;使用SSI动态生成网页部分内容 认识URL参数 在上网的时候&#xff0c;我们经常会见到在网址后面带有?AB&CD这样的语法格式。例如&#xff1a;https://blog.csdn.net/ZLK1214/articl…

OpenCV的HSV颜色空间在无人车中颜色识别的应用

RGB属于三基色空间&#xff0c;是大家最为熟悉的&#xff0c;看到的任何一种颜色都可以由三基色进行混合而成。然而一般对颜色空间的图像进行有效处理都是在HSV空间进行的&#xff0c;HSV(色调Hue,饱和度Saturation,亮度Value)是根据颜色的直观特性创建的一种颜色空间, 也称六角…

如何撤销git上一次的commit(或已push)

如何撤销git上一次的commit&#xff08;或已push&#xff09; 当多人开发时&#xff0c;我们本地commit后&#xff0c;刚要push&#xff0c;发现忘记pull最新代码&#xff0c;此时会有冲突push失败&#xff0c; 我们想要撤销最近的一次commit 我们先简单介绍一下git git有三大…

GreenPlum数据库日常维护

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&#x1f61…

使用 YOLOv8 和 Streamlit 构建实时对象检测和跟踪应用程序:第3部分:添加跟踪算法

介绍 对象跟踪是随着时间的推移识别一系列帧中的特定对象或多个对象的过程。它涉及定位对象在每个帧中的位置并跟踪其跨帧的移动。对象跟踪在各个领域都有广泛的应用,包括监控、机器人、自动驾驶、运动分析等。 跟踪算法使用各种技术(例如颜色直方图、运动分析、深度学习等)…

【多线程】(二)线程安全问题与线程同步

文章目录 一、多线程带来的风险1.1 观察线程不安全1.2 线程安全概念1.3 线程不安全的原因1.4 线程安全的解决方法 二、synchronized关键字2.1 synchronized 的特性2.2 synchronized 使用示例2.3 Java 标准库中的线程安全类 三、volatile关键字3.1 保证内存可见性3.2 禁止指令重…

Java反射的应用:动态代理

代理设计模式的原理 使用一个代理将对象包装起来, 然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。 对于静态代理&#xff0c;特征是代理类和目标对象的类都是在编译期间确定下来&#xff0c;不利于程…

基于FPGA的按键消抖

文章目录 基于FPGA的按键消抖一、按键消抖原理二、按键消抖代码三、仿真代码编写四&#xff1a;总结 基于FPGA的按键消抖 一、按键消抖原理 按键抖动&#xff1a;按键抖动通常的按键所用开关为机械弹性开关&#xff0c;当机械触点断开、闭合时&#xff0c;由于机械触点的弹性…

怎么使用Netty解码自定义通信协议

网络协议的基本要素 一个完备的网络协议需要具备哪些基本要素 魔数&#xff1a;魔数是通信双方协商的一个暗号&#xff0c;通常采用固定的几个字节表示。魔数的作用是防止任何人随便向服务器的端口上发送数据。协议版本号&#xff1a;随着业务需求的变化&#xff0c;协议可能…

SAP顾问生涯闲记:在SAP工作是什么体验

又有一段时间没更新自己的公众号了&#xff0c;为什么突然决定新开一篇SAP顾问生涯闲记系列的文章呢&#xff0c;是因为最近很荣幸地当选了SAP雇主品牌推广大使&#xff0c;作为SAP官方的推广大使在收获这份荣誉的同时&#xff0c;也承担了一些工作以及责任。 集结完毕︱SAP雇…

Flask_实现token鉴权

目录 1、安装依赖 2、实现代码 3、测试 源码等资料获取方法 1、安装依赖 pip install flask pip install pycryptodome 2、实现代码 import random import string import time import base64from functools import wrapsfrom flask import Flask, jsonify, session, req…

苍穹外卖day02——员工管理功能代码开发+分类管理代码导入

目录 新增员工——需求分析与设计 产品原型 接口设计: 数据库设计: 新增员工——代码开发 在Controller层中 在Service层中 在Mapper层中 功能测试 接口文档测试: 前后端联调测试: 新增员工——代码完善 ​编辑 第一个问题 第二个问题 员工分页查询 需求分析与设计 …

PostgreSQL考试难不难 ?

当涉及到PostgreSQL考试的详细难度&#xff0c;以下是一些可能涉及的主题和考点&#xff0c;这些主题在不同的考试中可能有所不同&#xff1a; 1.数据库基础知识&#xff1a;数据库的基本概念、关系型数据库模型、表、字段、主键、外键等。 2.SQL语言&#xff1a;对SQL语言的掌…

数据集——个人收集标注与使用过的数据集

前言 这是一个我个人在工作和学习中使用过以数据集的一部分&#xff0c;有语义分割&#xff0c;目标识别&#xff0c;人像抠图等几个大类&#xff0c;这只是我用过数据集中的一部分&#xff0c;这些数据集有小一部分是来源自网络&#xff0c;很大一部分都是我自己收集。 一、…

【动手学深度学习】--05.权重衰退

文章目录 权重衰退1.原理1.1使用均方范数作为硬性限制1.2使用均方范数作为柔性限制1.3对最优解的影响1.4参数更新法则 2.从零开始实现权重衰退2.1初始化模型参数2.2定义L2范数惩罚2.3训练2.4忽略正则化直接训练2.5使用权重衰退 3.简洁实现 权重衰退 学习视频&#xff1a;权重衰…

在网格化数据集上轻松执行 2D 高通、低通、带通或带阻滤波器研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…