前言
我在CSDN上看到一篇名为《程序员为什么非要参加一场编程竞赛》的文章,这是一篇译文,原著是国外的作者。这让我想起前段时间我参加的一场软件测试比赛,最终的感想可以用8个字概括,人无远虑必有近忧。在这里我想和大家分享我的参赛过程和赛后总结。
一、赛事简介
比赛给人的第一印象就是激情,热血,因为比赛一定能分出高低。编程竞赛在国内出现的时间比较早,现在已经上规模,成体系了,加之媒体的报道,大家也比较熟悉。但是软件测试比赛或许大家还没听说过。实际上成体系的软件测试比赛,例如CST全国大学生软件测试大赛,在2016年就已经首次举办,今年已经是第四届了。赛事项目分有:单元测试、性能测试、安全测试、web自动化测试、APP自动化测试,嵌入式测试等,可谓是相当全面。赛程从初赛、省赛、决赛到国际赛,中间也会穿插一些其他比赛,例如欧洲邀请赛,以及一些工业性质的比赛。
我所参加是南京7月份软博会期间,举办的一场工业APP软件测试比赛。
二、赛程赛制
比赛分为个人赛和团队赛,团队赛每队3人。由于个人赛和团队赛是同时进行的,所以无法同时参加2个项目。比赛分为3个阶段,第一阶段是报名和练习,选手报名后,可以在官方网站阅读比赛规则和注意事项,还会开放一些练习题,帮助赛前热身;第二阶段是网络预选赛,比赛时间3小时。个人赛前60名,团队赛前20名,可以进入决赛;第三阶段是决赛,比赛时间3小时,在南京国际博览中心进行线下比赛。总奖金12万RMB。
三、评分标准
预选赛评分标准:
1. (60%)Selenium脚本的测试需求覆盖率以及成功回放率;
2. (40%)Jmeter性能测试脚本的场景设置和参数设定准确性以及成功回放率;
3. 总分=评分1+评分2,比赛有多道题则累加计算;
4. 总分相同的选手按测试用例集运行时间二次排名,运行时间短优先。
总决赛评分标准:
1. (40%)Selenium脚本的测试需求覆盖率以及成功回放率;
2. (20%)Jmeter性能测试脚本的场景设置和参数设定准确性以及成功回放率;
3. (30%)协作式众包测试,Bug报告编写(0.6)+Bug报告审核(0.4);
4. (10%)工业APP标准符合性评价
5. 总分=评分1+评分2+评分3+评分4,比赛有多道题则累加计算;、
6. 总分相同的选手按测试用例集运行时间二次排名,运行时间短优先。
Selenium脚本和jmeter脚本为自动化评分;其余由专家人工评分。
四、赛前准备和初赛
报名后,我在阅读注意事项时,看到其中一条“所有的IDE都需要从官网下载,并按照官方要求进行配置,否则无法得分”。于是我在赛事官网上下载Eclipse,Jmeter,jdk等。浏览器、第三方jar包、驱动器、笔记本电脑需要选手自备。由于平时工作较忙,练习基本没做过,只是体验了一下比赛流程。初赛时间是下午2点到5点,持续3个小时时间,看似很长,但是分析完需求文档后,我觉得时间还是挺赶的。比赛过程中,是允许多次提交代码的。每次提交完都会立刻显示当前提交代码所获得的评分。最终的得分并不是以多次提交中的最高分为准,而是以最后一次提交代码所获得的分数为准。这就需要我们自己做好版本控制,若后期提交的代码分数不高,可以在比赛结束前进行回滚,以获取最高分。由于参赛人数较多,所以jmeter在调试脚本时,不允许使用大并发数,否则IP会被服务器的安全协议禁用,导致调试失败且无法提交代码。可以用1个线程进行调试,然后提交的时候改成多线程。
五、决赛
决赛是线下比赛,比赛地点在南京国际博览中心。比赛时间早上9点到12点,可以在8点后进入场地,调试比赛机器。
当天处于软博会展会期间,参展人员很多,7月份天气又很热。到达现场已经是汗流浃背,心情比较浮躁。我是个喜欢比赛的人,中学参加过数学竞赛,大学参加过电子竞技,比赛经验也算比较丰富了。所以看到现场这么多人,我并没有紧张,但却也不平静。而是兴奋,因为这是我第一次参加和工作有关的比赛。找到我的位子后,我调整了一下心态,便开始调试机器。
9点准时开始比赛,发放需求文档。selenium自动化测试需要完成13个页面的操作和校验;jmeter需要完成3个接口的性能测试;手工测试则针对整个被测软件,大小页面加起来约上百个;最后还要对被测软件进行工业标准符合性评价。
看完文档我的第一感觉就是时间不够。于是我快速开始编写selenium脚本,大约2分钟时间,登录脚本写完,调试通过,但是代码提交后,显示运行得分为0,我认为是系统显示的问题,可能要等比赛结束才会显示分数。于是我继续编写脚本,我发现页面的iframe太多,来回切换比较费时,且被测系统也是一个陌生的环境,我便想着先进行手工测试,顺便熟悉下操作,然后再进行脚本编写。有了这个念头以后,我又重新看了一遍功能测试的需求文档,再次阅读后,我发现了一条重要的线索“当某位选手发布bug后,其余选手提交bug与该选手类似,则由专家判定,若确实相同,优先发布bug的选手得分,其余选手不得分”,需求中的原文我不记得了,但是理解后就是这个意思。我的天哪,这意味着手工测试的30分是抢分赛,如果我拿了10分,其余59人加起来只能拿到20分。此时已经9点50,我觉得这30分可能已经所剩无几了,我立刻提交当前selenium代码,开始转战手工测试,让我意外的是,仅10分钟的时候,我发布了15个bug,竟然没有一个是重复的,说明大部分选手并没有注意到这条规则的真正含义。我继续进行手工测试,渐渐的,开始出现重复bug,直到11点,几乎提交的bug都是重复bug。我知道大部分的人都在进行手工测试,且未被发现的bug也越来越少,也就是说手工测试这30分中,剩余的分数越来越少,相对于时间的紧迫性,我觉得剩余的bug性价比已经不高了。我便开始了jmeter脚本的编写,大约20分钟,没有任何阻碍,完成了性能脚本编写,提交后,虽然得分不高,但我不打算优化,毕竟jmeter的占比只有20%。
我将剩余的时间放在了selenium脚本的编写。提交后,得分依然是0,此时引起了我的注意,一开始我认为可能比赛后才会显示分数,但jmeter是立刻就给出了分数,我断定是selenium的评分机制出了bug。我开始检查脚本,我发现在设置启动主站点时,裁判给我们的是url中有一个单词是大写。
而在浏览器中,输入url,会被重定向成小写,
然后我修改了selenium脚本中的url,提交后,得分出来了。最后简单的写了一下工业符合性评价。手工测试我抢到了不少分,其他选手如果没有发现url这个问题,可能selenium这一项就是0分了,而我是有得分的。我觉得占据着两大优势,可以拿到一个不错的名次。距离结束还有20分钟的时候,有一位选手提出了疑问,为什么selenium提交是0分。裁判询问后,发现现场很多选手都是0分,于是组委会专家开始寻找问题,在距离比赛结束还有10分钟的时候,问题找到了。大家修改url后开始疯狂的提交代码,以至于服务器承受不住压力,出现了断开连接、长时间未响应等异常情况,裁判最后也是允许未提交成功的选手,可以由组委会使用U盘将代码从选手的机器拷贝出来,复制到服务器上。最终我获得的是二等奖。
六、赛后总结
这次比赛自动化编码和手工测试占据了大部分分值。关于比赛中selenium的编码,我从3个地方做一个总结。(以下是我比赛中编写的一部分代码)
public static void test(WebDriver driver) { try { driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); driver.get("http://app.eedi.org.cn/SEEMMS.Server/public/index.php/index/login/login.html"); driver.manage().window().maximize(); driver.findElement(By.name("ACCOUNT")).sendKeys("test32"); driver.findElement(By.name("PASSWORD")).sendKeys("752167"); driver.findElement(By.name("ylogin")).click(); Thread.sleep(1500); driver.findElement(By.id("a-Conditionmonitor")).click(); driver.findElement(By.xpath("//*[@name='Conditionmonitor']/li[2]/a")).click(); driver.findElement(By.xpath("//*[@id='add-panel']/div/a")).click(); driver.findElement(By.name("SHAFT_SPEED")).sendKeys("10"); driver.findElement(By.name("SHAFT_TQRQUE")).sendKeys("20"); driver.findElement(By.name("SHAFT_POWER")).sendKeys("30"); driver.findElement(By.name("THRUST_TQRQUE")).sendKeys("40"); driver.findElement(By.name("THRUST_SPEED")).sendKeys("50"); driver.findElement(By.name("THRUST_POWER")).sendKeys("60"); driver.findElement(By.name("THRUST_THRUST")).sendKeys("70"); Thread.sleep(1000); driver.findElement(By.xpath("//input[@value='确定']")).click(); driver.findElement(By.id("alert")).click(); Thread.sleep(1000); driver.findElement(By.xpath("/html/body/div[2]/div/div[2]/ul/li[2]/a")).click();
(1)定位问题。
传统的定位方式都是选择id和name,在没有id和name属性时,选择使用其他属性,例如link,css等,最后选用xpath。但随着技术发展,这些传统的规则也有些不适用了。因为id,name可能会重名,link可能被封装等等元素。
这段代码中有一部分我使用了xpath的方式去定位,但实际上这个元素是有id属性的。为什么我不用id去定位?因为现在很多前端页面,都是通过模板生成的或是程序员只写一种通用方法,每次重复生成页面元素。这就会产生下面这个现象:①id前缀相同,后缀是每次点击展开按钮时动态生成。这样的id每次都不一样。
②多个元素使用相同id
此时使用id就无法就精准定位,所以传统的定位规则已经不适合现在的前端了。不论什么规则,我们只要理解其思想就行了。我认为满足3个条件就是好的定位方法。一是当前环境可以精准定位,运行成功率100%;二是具有健壮性,未来前端发生改变,可能会增加页面元素等,原方法依然可以定位到;三是易阅读,方便其他人员维护代码。
例如上面的2个例子中,对于第一个树形结构,根据边界值测试的设计思想,我们需要定位首个或最后一个树节点,不能使用id去定位,那么可以写成这样:
List<WebElement> tree = driver.findElement( By.xpath("//a[@title='安徽省']/following-sibling::ul")) .findElements(By.tagName("li"));
先通过title找到安徽省,然后通过轴关键字找到其后的兄弟节点ul,最后将这个节点下所有的li元素全部加入到list中,那么以后不同权限的账号登录系统,无论其管理城市数量的多少,我们都可以通过
tree.size(); //获取当前账户权限管理的城市数量 tree.get(0); //定位首个城市 tree.get(tree.size()-1); //定位最后一个城市
这2条语句定位到树结构的边界,对边界节点进行其他操作,完成边界测试。当然除了这3点,还有其他衡量的方法,例如增加冗余代码
这是百度首页,点击“百度一下”按钮的操作代码,可以看到selenium IDE提供了5种定位方式,日后若这个按钮发生了属性变化,则运行代码时,他会先从第一种定位方式进行页面元素寻找,若5种方式都找不到,才会报NoSuchElementException
(2)等待时间。
比赛中有提到程序运行同分的,以运行时间二次排名。那么我的代码中加入了以下语句
Thread.sleep(1500);
这对于比赛显然是不利的,因为增加了运行时间。那为什么还要加等待时间呢?因为java执行速度过快,前端在执行页面跳转的过程中,java代码并不会停止,可能会产生页面还没有完全加载,java代码就已经执行下一句的,会导致无法找到页面元素报错。有时候我们看到NoSuchElementException报错时,并不是定位方法出了问题,而是页面加载速度跟不上java执行的速度。例如:
driver.findElement(By.name("ylogin")).click(); //点击登录按钮 Thread.sleep(1500); driver.findElement(By.id("a-Conditionmonitor")).click(); //点击监视器按钮
系统登录后,会跳转到首页,首页右上角有一个监视器按钮。如果不加入第二行代码,就会报错。因为点击登录,页面跳转大约需要0.3秒的时间,但是此时java已经开始执行“点击监视器按钮”这个操作了,此时页面是空白的,所以无法找到元素,报错。加入等待1.5秒,页面完全加载成功,此时在执行“点击监视器按钮”操作,就可以正常运行了。有人会问既然要加等待时间,为什么选择这种固定的方式?动态等待,可以提高程序的执行速度呀?其实我是出于2点原因这么写的。第一是因为动态等待需要设置参照物,对于第一次测试这么庞大的系统,比赛时间又这么短,我不想把比赛时间浪费在脚本的性能调优上,能跑起来就行。第二是因为平时我做自动化测试时,都是以场景为设计对象,真实用户在操作时,每一步之间都会有停顿,我是尽可能的让机器去模仿用户的操作习惯,才这么写。用过LoadRunner的人都明白什么是“思考时间”,其实我这样写,就相当于是“思考时间”。当然,如果是追求全功能的回归测试,那么提高效率,还是应该使用动态等待。
(3)测试类,功能类,配置类应该分开去写。
而比赛中,官方提供的模板是
Example.java内容如下:
import java.util.concurrent.TimeUnit; public class Example { // Mooctest Selenium Example // <!> Check if selenium-standalone.jar is added to build path. public static void test() { } public static void main(String[] args) { // Run main function to test your script. } }
主类用于运行,测试类用于功能编写。也许是方便比赛评分吧。但实际中我认为测试类,功能类,配置类需要分开。这样易读且方便维护。
①这是配置类。定义了打开和关闭浏览器。打开浏览器需要的驱动、url、超时时间等。因为每次测试,都需要这些固定的步骤,所以单独写成一个配置类,如果测试地址改了,只需要在这里修改一次url就行了。如果功能、配置、运行写在一个文件中,那么一旦发生修改,你就要去所有的文件中修改。
public class OpenAndCloseBrowse { String baseUrl = "http://app.eedi.org.cn/SEEMMS.Server/public/index.php/index/login/login.html"; public WebDriver openMethod(WebDriver driver) { System.setProperty("webdriver.chrome.driver", "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chromedriver.exe"); driver = new ChromeDriver(); driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); driver.get(baseUrl); driver.manage().window().maximize(); return driver; } public void closeMethod(WebDriver driver) { driver.quit(); } }
②这是运行类(测试类)。定义了要进行哪些测试,与测试用例对接。第11行代码中的username和password,可以写成从测试用例中动态获取。
public class Login_TestCase { public WebDriver driver; @Test(description = "正常登录") public void normalLogin() { OpenAndCloseBrowse b = new OpenAndCloseBrowse(); driver = b.openMethod(driver); try { Login l = new Login(); Thread.sleep(2000); l.login(driver, "username", "password"); } catch (Exception e) { e.printStackTrace(); } finally { b.closeMethod(driver); } } @Test(description = "账号密码正确,无权限") @Test(description = "输入错误密码登录") @Test(description = "输入不存在的用户名登录") ...... }
③这是功能类。写了一个简短的登录功能。
public class Login { public void login(WebDriver driver, String username, String password) { SQLMethod sql = new SQLMethod(); String mobile = sql.getString("select mobile from user where mobile = '" + username + "'"); int role = sql.getInt("select role from user where and mobile = '" + username + "'"); String passwd = sql.getString("select password from user where mobile = '" + username + "'"); String sign = ""; // 没有输入约定好传no try { if (!username.equals("no")) { sign = "没有找到用户名输入框"; driver.findElement(By.id("phone")).sendKeys(username); } if (!password.equals("no")) { sign = "没有找到密码输入框"; driver.findElement(By.id("password")).sendKeys(password); } sign = "没有找到登录按钮"; driver.findElement(By.xpath("//*[contains(text(),'登录')]/parent::button")).click(); Thread.sleep(500); if (username.equals(mobile)) { if (password.equals(passwd)) { if (role == 1) { // 正常登录 Thread.sleep(2000); assertTrue(driver.getCurrentUrl().contains("HomePage"), "登录成功, 跳转页面错误"); } else { // 帐号密码正确,权限不足 sign = "用户无权限,没有给出提示"; assertEquals(driver.findElement(By.xpath("//*[@title='msg']")).getText() .replaceAll(" ", ""), "用户无权限", "用户无权限,提示信息不正确"); } } else if (password.equals("no")) { // 正确帐号,不输入密码 sign = "不输入密码,没有给出提示"; assertEquals(driver.findElement(By.xpath("//*[@title='msg']")).getText() .replaceAll(" ", ""), "请输入正确的手机号/密码!", "不输入密码,提示信息不正确"); } else { // 正确帐号,错误密码 sign = "输入错误的密码,没有给出提示"; assertEquals(driver.findElement(By.xpath("//*[@title='msg']")).getText() .replaceAll(" ", ""), "密码错误", "输入错误的密码,提示信息不正确"); } } else { if (username.equals("no")) { if (password.equals("no")) { // 不输入帐号和密码 sign = "不输入帐号和密码,没有给出提示"; assertEquals(driver.findElement(By.xpath("//*[@title='msg']")).getText() .replaceAll(" ", ""), "请输入正确的手机号/密码!"); } else { // 不输入帐号,正确密码 sign = "不输入帐号,没有给出提示"; assertEquals(driver.findElement(By.xpath("//*[@title='msg']")).getText() .replaceAll(" ", ""), "请输入正确的手机号/密码!"); } } else if (this.isInteger(username)) { // 不存在的帐号,正确密码 sign = "输入不存在的帐号,没有给出提示"; assertEquals(driver.findElement(By.xpath("//*[@title='msg']")).getText() .replaceAll(" ", ""), "账户不存在", "输入不存在的帐号,提示信息不正确"); } else { // 帐号格式错误 sign = "输入错误格式的帐号,没有给出提示"; assertEquals(driver.findElement(By.xpath("//*[@title='msg']")).getText() .replaceAll(" ", ""), "请输入正确的手机号/密码!"); } } } catch (NoSuchElementException e) { assertEquals(false, sign); } catch (Exception e) { e.printStackTrace(); } } public boolean isInteger(String str) { Pattern pattern = Pattern.compile("^[-\\+] [\\d]*$"); return pattern.matcher(str).matches(); } }
对于设计的自动化测试用例,每一条用例的操作步骤,都可以写成功能类;每一个条用例所需要的测试数据,可以写入到运行类(测试类);每一条用例的前置条件,可以写入到配置类;每一条用例的预期结果,写入到功能类,用断言的方式实现,并将实际结果返回给运行类(测试类),可以拓展一下,将实际结果回填到测试用例中。再集成到jenkins,实现定时自动化测试。我们只要定期维护测试用例和脚本即可。
赛后我还在难过,如果没有选手质疑,可能大部分人第一项都是0分,或许我可以因此捡漏,拿到特等奖。但细想现实生活中,捡漏犹如彩票中奖,几率小且可遇不可求,想要拿到更高的奖项还是要提高自己。现阶段,软件测试比赛还是以大学生为主,纵观历届比赛,各分项赛对技能的标准和要求也是非常高的,能来参加比赛的学生都不是善茬,将来毕业后,就业,一定会对测试行业产生冲击。例如这次特等奖的选手,就是南京大学毕业的学生,已入职微软公司。若不想别淘汰,就要持续学习,努力奋进。
生活在和平年代的我们,衣食无忧,在欢声笑语中长大。但这样的环境来之不易。尤其是观看完国庆阅兵后,这种紧迫感更加强烈。当下“持续学习”,“努力奋进”是2个比较流行的词语,也完美的诠释了如何做,才能居安思危。作为一个IT人,持续学习和努力奋进,说的小一点,可以提升自己的竞争力,改善自己的生活;说的大一点是在为祖国的伟大复兴做贡献。举个例子,2001年中美黑客大战,大战结果也是众说纷纭,我不做过多评论。但从测试的角度去分析这件事情,当时我国IT从业人员少,且能力弱。各门户网站无论是性能、还是安全都相对较弱。而今天我国IT行业的发展,无论是AI、5G又或者是物联网,都是世界领先,且质量是非常有保障的,若IT人员可以持续学习和努力奋进,将学习所得运用到实际生产。再次爆发黑客大战时,无论是网站还是产品都可以保持高质量(包括功能,性能,安全等等),可能就像康辉说的那样“不愿打,但也不怕打,必要时不得不打”。无论是为了国家,还是为了自己,都要想的长远一些,正所谓人无远虑,必有近忧。“持续学习,努力奋进”不是一个口号,而是要付出实际行动的,或许这是我们这一代人最好的出路。
版权声明:本文出自51Testing会员投稿,51Testing软件测试网及相关内容提供者拥有内容的全部版权,未经明确的书面许可,任何人或单位不得对本网站内容复制、转载或进行镜像,否则将追究法律责任。