关于去哪儿网的UI自动化测试脚本及报错,求高手不吝赐教造句

关于去哪儿网的UI自动化测试脚本及报错,求高手不吝赐教_百度知道
关于去哪儿网的UI自动化测试脚本及报错,求高手不吝赐教
我有更好的答案
啥错?脚本呢?
其他类似问题
为您推荐:
不吝赐教的相关知识
等待您来回答
下载知道APP
随时随地咨询
出门在外也不愁IBM Bluemix
点击按钮,开始云上的开发!
developerWorks 社区
自动化测试技术是保证产品质量的重要手段。随着敏捷开发方法的盛行和产品发布周期的缩短,产品对测试的要求也相应提高,因此自动化测试也变得比以前更加重要。冰冻三尺非一日之寒,和产品开发一样,产品的自动化测试系统也需要通过迭代开发不断强化,逐步解决效率瓶颈,更广泛、更灵活的支持产品开发的需求。那么,如何从无到有建立图形用户界面(GUI)产品的自动化测试系统?GUI 产品自动化测试每一阶段的目标是什么?在逐步演化的过程中,我们可能会面对哪些问题?这些又可以如何解决呢?本系列文章希望能够结合我们团队的经验,一一解答。在本文中,您将了解我们如何看待自动化测试,又是如何通过 RFT 来实现简单的测试目标。
, 高级软件工程师, IBM
孙沛,IBM 中国软件开发中心,Lotus Notes 产品开发团队项目组长,高级软件工程师。2005 年起开始在开发中心工作,先后参与了 Lotus Notes,Lotus Symphony 产品的开发工作。目前负责 Lotus Notes 产品的核心组件的跨平台开发,自动化测试,以及部分客户的技术支持工作。您可以通过 developerWorks 社区与进行交流。
, 高级软件工程师, IBM
陈晓鹏,高级软件工程师,目前在 IBM 中国软件开发中心从事 Lotus Notes 的产品开发工作。您可以通过 developerWorks 社区与进行交流。
, 高级软件工程师, IBM
陈海泉,高级软件工程师,在 Lotus Notes 项目组,从事基于 Rational Functional Tester 的自动测试工作与产品研发工作,对自动测试有着丰富的经验。您可以通过 developerWorks 社区与进行交流。
免费下载:
下载更多的 ,并加入 ,参与在线交流。踏上自动化测试之路在中国,大多数开发者并不会太关心测试,保证产品质量似乎更多是测试者应该关注的问题。所以,当只写过单元测试代码的我告诉面试经理我认为“自动化测试如何重要”的时候,我可以明显感觉到他很开心。当然,我最终还是去做了开发而非测试。我们的团队把 Lotus Notes 这一至今有 21 年历史的 Lotus 的拳头产品从 Windows 平台成功的移植到了 Mac 平台。如果您熟悉 Lotus Notes,您会明白我们的不易。这个的产品的软件架构非常庞大,8.0 版以上的 Notes 基于 Eclipse 平台,而其部分核心逻辑和界面组件仍是由 C 完成的。(如果您希望进一步了解这个产品,欢迎您到 Lotus 专区转转)随着项目的顺利进行,测试团队找到了我们:我们是否有方法在 Mac 上进行自动化的测试呢?如果能够在每天的最新版上自动跑一轮冒烟测试,那么我们将不再需要浪费一个测试者每天半小时的时间来痛苦的重复。这是我们和自动化测试的第一次亲密接触。其后,随着敏捷方法的在项目中的推行和产品发布周期的缩短,我们对自动化测试的要求也越来越高,我们参与了更多系统改进的工作,也接触了更多领域的专家。从一群菜鸟开始,步步走来,我们理解了建立自动化测试系统的不易:和产品开发一样,产品的自动化测试系统也需要通过迭代开发不断强化,逐步解决效率瓶颈,更广泛、更灵活的支持产品开发的需求。往事历历在目,在从本文开始的五篇系列文章中,我们希望和您一起来分享我们的心得和感悟:在本文中,您将了解我们如何看待自动化测试,又是如何通过 RFT 来实现简单的测试目标。在第二篇文章中,您将看到我们如何像做产品一样去经营测试,构建易于维护的基础系统。在第三篇文章中,我们将给您展示我们是如何通过完善的日志功能和使用规则提高分析测试结果的效率的。在第四篇文章中,您将看到一些高阶使用技巧,包括如何支持自定义控件、如何实现测试脚本的跨平台以及如何通过开发新的工具来自动的创建自动化测试脚本。在第五篇文章中,您将看到我们如何通过 LA 灵活的使用我们的测试资产,实现自动部署自动化测试环境以及分布式测试的目的。好了,现在,让我们正式启动我们的探索之旅。如果您也对 GUI 产品的自动化测试感兴趣的话,请加入我们!了解自动化测试和产品开发一样,构建自动化测试系统同样需要人力物力的持续投入。所以,在我们开始考虑实施之前,需要对自动化系统有所理解,以帮助我们调整预期并设定合适的目标。那么,自动化测试到底是什么?能够为我们带来哪些好处?实施的切入点在哪里?又应该如何持续改进呢?百度百科的“”词条对这些方面做了概要性的介绍,推荐您阅读。在此基础之上,我们还希望您能够理解以下一些内容:自动化测试如何改善产品质量首先,通常说来,受限于人力资源、产品规模和发布周期,任何大型产品都很难在发布之前对产品的各个方面进行全面的测试。我们只能根据新版本中的代码变更预测影响的功能和行为,并围绕这些影响区域进行重点测试,而对于其他区域测试完整性将大打折扣。这就为产品缺陷的滋生留下了隐患。另一方面,很多的测试都需要机械性的重复执行,比如冒烟测试、回归测试、性能测试、和长期运转测试(Long Run),这些测试的手工执行将占用大量的人力。由于缺乏充足测试的支持,开发者对产品结构性问题修正的风险将大大增加,从而使得开发者更倾向于针对问题的表象“打补丁”而不敢触及根本。通过自动化测试,测试团队可以从这些繁冗的机械重复中解放出来,集中精力加强重点区域的测试力度,也有余力通过制定更完善的测试计划,设计合适的测试用例,改进自动化测试脚本来提高测试的整体覆盖率;从开发者的角度看,随着回归测试成本的降低,开发者也更有信心对产品进行结构性的调整。自动化测试无法代替测试者虽然 IBM 的“沃森”能够赢得智力问答比赛,但这并不代表当前的自动化测试具备足够的“智能”来替代测试人员。自动化测试只能够按照指定的步骤来执行测试,它实际上是在帮我们完成需要不断重复或者通过人工执行容易出错的工作,充当我们的“廉价劳动力”。而测试人员则能够解脱出来,进行更多创造性的工作。要知道测试人员在项目中永远是短缺的。自动化测试的覆盖范围并非越大越好100% 的自动化测试只是一个美好的愿景。事实上,有很多测试工作目前是计算机难以完成的。比如说,我们需要检查一篇文档打印稿的格式正确性,自动化测试脚本要如何实现呢?如果能够通过计算得出正确的结果,我们完全可以直接把它作为产品里的排版代码了。事实上,人所擅长的模糊判断恰恰是程序的短板。另外,自动化脚本的编写和维护同样需要成本,如果脚本的复用和重复执行所带来的收益小于我们的付出,这种自动化将变得毫无价值,因此,自动化测试也不适用于变化较频繁的功能区域——我们需要在覆盖率和成本之间寻求一个平衡。自动化测试无法发现新的问题呃,这是很奇怪的一个观点,对吧?在我们编写和调试自动化脚本的时候,或许能够发现一些产品的新问题,而一旦脚本投入使用之后,将不会帮助我们发现“新”的产品问题。因为,这些脚本只能不断的重复,来确保我们设计的测试用例在今后的日子里不会发现问题。自动测试脚本充当的更像是一个“守护者”而非“探索者”的角色,它将帮助我们更加确信产品没有问题。它们发现的问题通常都是回归性的,而不是新问题。自动化测试需要成本这又是一个显而易见的命题,不过自动化测试的成本经常会被忽略。和我们的产品一样,自动化测试脚本也是一堆代码。我们通过自动化测试来保证产品的正确性,而保证这些脚本的正确性却需要人工,这意味着我们需要开发、调试;产品是会不断演化的,昨天正确的脚本今天可能就是错误的,这意味着我们需要不断的维护。为了复用现有的脚本获得更大的价值,我们需要持续的改进自动化系统,甚至移植到新平台上。开发,维护,演进,请记住自动化测试需要长期的持续投资。看过了上面这些内容,您是否会觉得自动化测试不再像想象中的那么美妙了?呵呵,请相信自动化测试并非万能,而现在的大型产品没有自动化测试的支持是万万不能的。作为我们的工具箱中的一个强力工具,我们需要用好它,发挥出它的威力。如何进行 GUI 产品的自动化测试四年前,在我们决定开始支持自动化测试时,所面对的最直接的问题就是,我们应该如何进行 GUI 产品的自动化测试呢?对于普通的单元测试,我们只需要给目标函数一个输入并检测输出就可以了;对于 Server 端逻辑的测试,我们只需要模拟客户端发起一个 HTTP 的请求就可以了;对于浏览器里的 Web 应用,我们理论上也可以通过 DOM 树进行直接操作。那么,对于在本地运行的 GUI 产品呢?我们又应该怎样去支持界面交互操作的自动化?在实际工作中我们发现这个问题其实可以分解成三个基础问题:如何捕获需要操作的对象?如何对捕获到的对象进行操作?如何验证操作的结果?好吧,让我们举个简单的例子来详细说明这三个问题。下图 1 展示了一个基于 Java SWT 框架的 GUI 应用程序,它可以帮助您从 CD 库中挑选 CD 并下订单。您可以简单的通过列表选择您喜欢的 CD,然后通过点击“Place Order”按钮来下订单。图 1. CD 订购程序在您点击之后,您将会首先看到如图 2 所示的登录界面。图 2. 登录对话框程序有了,我们应该如何测试下面这个简单的用例呢?在 CD 列表中选中 CD“Die schone Mullerin, Op. 25”点击“Place Order”按钮验证登录对话框能够正确弹出回顾我们的问题:如何捕获需要操作的对象?首先,我们要发现并定位 treeview 控件(包括特定的条目“Die schone Mullerin, Op. 25”)以及名为“Place Order”的按钮。在 Windows 下,我们可以感知每个原生应用程序的控件组织关系,比如一个对话框上有哪些按钮(您可以尝试使用微软的 SPY++ 去观察每个原生程序的窗体结构)。而在 Mac 上,我们只能借助系统的 Accessibility 特性支持发现程序的部分架构,这将受限于程序对 Accessibility 的支持。对于我们来说,最稳妥的解决方法,还是在被测程序中加入一个插件,来直接向我们“汇报”程序的窗体结构和控件信息。如何对捕获到的对象进行操作?在捕获到 treview 和按钮之后,我们需要模拟用户展开 treeview,点击选取“Die schone Mullerin, Op. 25”条目,再单击“Place Order”按钮。相对而言这一点比较容易实现:我们只需要在系统级别控制住鼠标和键盘输入,并模拟输入对应的事件就可以了。当然,要实现操作的跨平台,我们还需要针对不同的操作系统进行封装。如何验证操作的结果?在单击“Place Order”按钮之后,我们还需要找到名为“Member Logon”的对话框,并且验证对话框中的子控件的属性(如文字、位置等)是否与我们预期的一致。和第一个问题的答案一样,通过系统框架能够访问的对象信息是非常有限的。当然,对于验证而言,我们还可以通过局部截屏来比较操作结果(实际上,在最开始的时候,我们就是这么做的),不过这么做的局限性非常大。首先,用于截屏的基准测试机器和普通的测试机器需要在许多方面保持一致性,比如分辨率、颜色质量等。另外,图像匹配的效率也不高。最重要的是一旦我们调整了产品里控件对象的形状、位置以及文字等属性,以前所截取的包含这个控件对象的样图将全部作废,必须重新截取,这将使得维护成本大增。所以,最稳妥的方法,依然是通过插件从产品中直接获取信息。好吧,转了一圈我们又回来了。在我们试用了很多方法之后,发现最彻底的解决方式还是通过插件来直接获得对应用程序的控制权。而且,我们对自动化开发工具的需求还不止于此:我们希望能够用熟悉的语言开发脚本,毕竟没有多少人愿意为了写个脚本专门去学 Perl;方便的开发环境和脚本的版本管理自然也是需要考虑的因素;一个灵活而有效的日志框架是分析测试结果和调试测试用例必不可少的工具… …这么这么多的需求,分明就已经是一个产品了,开发出来都可以卖钱了。幸好 Rational Functional Tester 为我们提供了这一基础,并且比我们所需要的提供的更多。假如我们不得不从轮子造起的话,恐怕管理团队就需要重新考虑自动化测试的性价比了。初识 Rational Functional TesterRational Functional Tester(简称 RFT)是 IBM 提供的强大的自动化功能和回归测试工具,至于其具体如何强大本文就不赘述了,您可以参考文末参考资料里给出的 RFT 的产品专题。现身说法,Lotus Notes 的大量的自动化测试脚本都是建立在 RFT 之上,包括功能测试、性能测试和本地化测试。对于我们来说,RFT 是一个值得信赖的工具。RFT 的特性很多,以下仅列出我们所关注的一部分关键特性:支持多种应用程序,包括 Win32 Native、Eclipse、.Net, 浏览器和其他一些应用支持使用 Java 和 VB.NET 进行脚本开发支持操作的录制和回放和 Eclipse 紧密集成,支持方便的开发和调试通过插件可以和 ClearCase 紧密集成易于扩展,可以灵活的适应被测程序的需求下图展示了 RFT 的使用界面,如果您曾经使用过 Eclipse,您将会对此感到非常亲切。图 3. RFT 使用界面在 RFT 的众多特性中,录制和回放是非常值得一提的功能,测试人员可以直接通过录制屏幕操作来生成测试脚本,然后通过回放来执行测试。有经验的测试人员也可以通过它来快速生成有用的代码片段。下面让我们来看一看如何使用 RFT 录制功能来生成上一章中的测试用例的脚本:运行我们的 CD 订购程序:C:\Program Files\IBM\SDP\FunctionalTester\FTSamples\ ClassicsJavaA.jar。实际上,这是 RFT 自带的一个示例程序。(RFT 在安装后会对系统 JVM 进行配置,所以我们不再需要对普通的 SWT 程序安装额外插件,直接运行就可以了)在一个空白的 RFT 测试脚本中,点击 "Script-&Insert Recording" 菜单,RFT 将弹出“Recording”对话框,启动脚本录制。手工操作我们的 CD 订购程序,选择条目,点击“Place order”按钮,然后点击 Cancel 关闭弹出的“Member Logon”对话框。执行完毕后,在 Recording 对话框中点击停止按钮,录制就会终止。RFT 会生成测试用例步骤所对应的代码。如图 4 所示。图 4. RFT 录制生成的测试脚本您可以通过点击“Script-&Run”菜单来运行新生成的脚本,一切都很简单。如果您足够细心,就能够在图 4 中发现更多的信息。RFT 在生成脚本的同时,会把从应用程序捕获的 UI 信息保存在映射文件(object map)里面,比如 tree2 和 placeOrder。而在生成的脚本中将直接使用对象上的方法,这些方法在底层将会最终映射到具体的鼠标和键盘事件。图 5 展示了 RFT 本身的录制流程如,对于每个脚本,都有独立的映射文件。RFT 会从映射文件里面查找,如果找不到当前测试对象,会把测试对象的信息加入到映射文件里面。图 5. 录制流程如果您更细心的话,您会发现 RFT 的录制完成了捕获和操作的功能,而没有生成验证相关的代码,因为这些需要您随后自己补齐。如果您对如何更高效的做验证感兴趣的话,欢迎关注我们的后续文章。小结本系列的第一篇文章到此告一段落了。相信现在您已经了解了在启动项目的自动化测试之前应该注意些什么,以及怎样对一个 GUI 产品进行自动化测试。如果您还没有使用过 RFT 的话,建议您不妨下载一份,亲自动手试试它的录制和回放功能,相信它会带给您惊喜的。
参考资料 访问
和 developerWorks 上的 。访问
和 developerWorks 上的 ,了解我们测试对象的复杂性。阅读百度百科的“”词条,概要了解自动化测试。阅读“”一文,了解如何在 RFT 中创建脚本。访问 ,获得关于 IBM Rational 软件交付平台(Rational Software Delivery Platform)产品的技术资源和最佳实践。订阅 ,一份关于 developerWorks 指南、文章、下载、社区活动、网络广播和技术讲座的电子周刊。学习 ,这是学习 IBM 软件工具的快速通道。在每一篇教程中,都会有快速入门产品演示动画。您可以通过其中的动画演示快速浏览如何使用 IBM 软件完成开发任务。下载免费的 。获取免费的 。获取免费的 ,了解最新的 IBM Rational 软件开发工具技术文档和资源。下载更多免费的 ,了解 IBM Rational 软件的最新特性。获取更多 ,并熟练掌握来自 DB2®、Lotus®、Tivoli®,以及 WebSphere® 的开发工具和中间件产品,用这些试用版软件开发您的下一个项目。这些试用版软件可以免费直接从 developerWorks 下载。加入 ,developerWorks 社区是一个面向全球 IT 专业人员,可以提供博客、书签、wiki、群组、联系、共享和协作等社区功能的专业社交网络社区。加入 ,参与在线交流。
developerWorks: 登录
标有星(*)号的字段是必填字段。
保持登录。
单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件。
在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。
所有提交的信息确保安全。
选择您的昵称
当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。昵称长度在 3 至 31 个字符之间。
您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。
标有星(*)号的字段是必填字段。
(昵称长度在 3 至 31 个字符之间)
单击提交则表示您同意developerWorks 的条款和条件。 .
所有提交的信息确保安全。
IBM PureSystems(TM) 系列解决方案是一个专家集成系统
通过学习路线图系统掌握软件开发技能
软件下载、试用版及云计算
static.content.url=/developerworks/js/artrating/SITE_ID=10Zone=RationalArticleID=659508ArticleTitle=实战 GUI 产品的自动化测试,第 1 部分: 简介publish-date=关于去哪儿网的UI自动化测试脚本 - 张飞_ - 博客园
随笔 - 167, 文章 - 0, 评论 - 144, 引用 - 0
UI自动化测试Qunar机票搜索场景访问Qunar机票首页,选择&单程&,输入出发、到达城市,选择today+7日后的日期,点&搜索&,跳转到机票单程搜索列表页。在列表页停留1分钟,至到页面上出现&搜索结束&。如果出现航班列表,对于出现&每段航班均需缴纳税费&的行随机点选&订票&按钮,在展开的列表中会出现&第一程&、 &第二程&;对于没有出现&每段航班均需缴纳税费&的行随机点选&订票&按钮,在展开的列表底部中会出现&报价范围&如果不出现航班列表,则页面会出现&该航线当前无可售航班&请使用maven创建java工程,引入Selenium框架,编写WebUI代码,实现上述人工操作和验证。要求能随机验证100个城市对的3个月内的任意搜索条件。
package com.
import java.text.SimpleDateF
import java.util.ArrayL
import java.util.C
import java.util.D
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptE
import org.openqa.selenium.WebD
import org.openqa.selenium.WebE
import org.openqa.selenium.firefox.FirefoxD
import org.openqa.selenium.support.ui.ExpectedC
import org.openqa.selenium.support.ui.WebDriverW
import org.testng.annotations.AfterC
import org.testng.annotations.BeforeC
import org.testng.annotations.T
public class Demo3 {
public WebD
private int waitTime = 20;
private Date fromD
private SimpleDateF
@BeforeClass
public void setUp() {
driver = new FirefoxDriver();
driver.manage().window().maximize();
@AfterClass
public void tearDown() {
driver.close();
driver.quit();
private WebElement getElement(final By by) {
boolean flag = new WebDriverWait(driver, waitTime)
.until(new ExpectedCondition&Boolean&() {
public Boolean apply(WebDriver d) {
return d.findElement(by).isDisplayed();
WebElement element =
if (flag) {
element = driver.findElement(by);
private WebElement getElementNotWait(final By by) {
WebElement element =
element = driver.findElement(by);
} catch (Exception e) {
private String getFromDate() {
fromDate = new Date();
sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.format(fromDate);
private String getToDate() {
Calendar c = Calendar.getInstance();
c.setTime(fromDate);
c.add(Calendar.DAY_OF_MONTH, +7);
return sdf.format(c.getTime());
private ArrayList&WebElement& getAllResultElement() {
int labelIndex = 1;
int rowIndex = 1;
ArrayList&WebElement& list = new ArrayList&WebElement&();
while (true) {
if (this.getElementNotWait(By.xpath("//div[@class='outContainer']/div[" + labelIndex + "]")) != null) {
if (this.getElementNotWait(By.xpath("//div[@class='outContainer']/div[" + labelIndex + "]/div[starts-with(@id,'itemRowXI')][" + rowIndex + "]")) != null) {
list.add(this.getElementNotWait(By.xpath("//div[@class='outContainer']/div["+ labelIndex + "]/div[starts-with(@id,'itemRowXI')][" + rowIndex + "]")));
rowIndex++;
labelIndex++;
rowIndex = 1;
private int getRandom(int count) {
return (int) Math.round(Math.random() * (count - 1));
private void sleep(int s){
Thread.sleep(s*1000);
} catch (InterruptedException e) {
e.printStackTrace();
public void process() {
driver.navigate().to("/");
this.getElement(By.id("searchTypeSng")).click();
this.getElement(By.xpath("//div[@id='js_flighttype_tab_domestic']//input[@name='fromCity']")).clear();
this.getElement(By.xpath("//div[@id='js_flighttype_tab_domestic']//input[@name='fromCity']")).sendKeys("武汉");
this.getElement(By.xpath("//div[@id='js_flighttype_tab_domestic']//input[@name='toCity']")).clear();
this.getElement(By.xpath("//div[@id='js_flighttype_tab_domestic']//input[@name='toCity']")).sendKeys("北京");
this.getElement(By.xpath("//div[@id='js_flighttype_tab_domestic']//input[@name='fromDate']")).clear();
this.getElement(By.xpath("//div[@id='js_flighttype_tab_domestic']//input[@name='fromDate']")).sendKeys(this.getFromDate());
this.getElementNotWait(By.xpath("//div[@id='dom_arrivalDateDiv_disable']//div[@class='sicon']")).click();
JavascriptExecutor j =(JavascriptExecutor)
j.executeScript("$('input[name=toDate]').val('"+this.getToDate()+"')");
this.getElement(By.xpath("//div[@id='js_flighttype_tab_domestic']//button[text()='搜 索']")).click();
this.sleep(10);
this.getElement(By.xpath("//div[@class='outContainer']"));
ArrayList&WebElement& all = this.getAllResultElement();
int random = this.getRandom(all.size());
WebElement element = all.get(random);
String id = element.getAttribute("id");
String lindId = id.replace("itemRow", "btnBook");
this.getElement(By.xpath("//a[@id='"+lindId+"']")).click();
this.sleep(10);谈 Dojo 应用的 UI 自动化测试 - 博客 - 伯乐在线
& 谈 Dojo 应用的 UI 自动化测试
& 825 阅读
随着富 Internet 应用(RIA)的不断兴起,各种 JavaScript 开发工具包的功能也在不断增强,Dojo 正是其中的佼佼者。Dojo 提供了一套完整的开发解决方案,包括核心的 JavaScript 库、简单易用的小部件(Widget)等。当越来越多的企业 Web 应用选择 Dojo 作为前端技术框架的时候,作为测试人员,我们便需要考虑如何有效地进行自动化测试开发与实施。Dojo 是一个 Web 前端技术框架,所以这里所说的自动化测试,具体是指 Web UI 自动化测试,更进一步说是 Web 2.0 UI 自动化测试。
考虑到 Web UI 的特性以及 Web UI 自动化测试的投入产出比(ROI),关于是否应该进行 Web UI 自动化测试,测试领域存在不同的观点。我个人认为还是有必要的。不过这不是本文要讨论的重点。决定 Web UI 自动化测试成败(或者说收效)的因素有很多,技术框架的选取和设计是其中之一,个人认为也是比较重要的一个因素。本文将从 Dojo 应用 UI 自动化测试面临的诸多挑战谈起,进而讨论针对 Dojo 应用 UI 自动化测试的框架适用的设计原则。
虽然本文探讨的主题是 Dojo 应用的 UI 自动化测试,但不少内容也适合其他的 Web 2.0 应用。希望读者朋友或多或少都能从中获益。
Dojo 是什么?
Dojo 是一个 JavaScript 实现的开源 DHTML 工具包。它是在几个项目捐助基础上建立起来的(nWidgets,f(m),Burstlib) 。 Dojo 的最初目标是解决开发 DHTML 应用程序遇到的一些长期存在的历史问题,现在,Dojo 已经成为了开发 RIA 应用程序的利器:
Dojo 让您更容易地为 Web 页面添加动态能力,您也可以在其它支持 JavaScript 的环境中使用 Dojo ;
利用 Dojo 提供的组件,您可以提升 Web 应用程序的可用性和交互能力;
Dojo 很大程度上屏蔽了浏览器之间的差异性,因此,您可以不用担心 Web 页面是否在某些浏览器中可用;
通过 Dojo 提供的工具,您还可以为代码编写命令行式的单元测试代码。
Dojo 的打包工具可以帮助您优化 JavaScript 代码,并且只生成部署应用程序所需的最小 Dojo 包集合。
Dojo 应用 UI 自动化测试面临的挑战
Dojo 应用 UI 自动化测试面临的挑战可以归为两类:开发阶段的挑战和维护阶段的挑战。
开发阶段的挑战:
异步请求的处理
Dojo 复杂性
产品复杂性
维护阶段的挑战:
频繁的 UI 更改
下面,我们具体讲解每一种挑战。为了后文讲解设计原则时能方便的对应到这些挑战,我们用英文字母标记每个挑战。
A. 异步请求的处理
在传统的 Web 应用中,用户每点击页面上的超链,浏览器就会向服务器发出一个请求,等待服务器做出响应,然后返回一个完整新网页,但在大多数情况下用户不得不忍受页面闪烁和长时间的等待。为了解决传统的 Web 应用中出现的问题,出现了 Ajax。通过使用 Ajax 页面和应用向服务器请求它们真正需要的东西,也就是页面中需要修改的部分,服务器就提供哪部分,这使得通信量更少,更新更少,用户等待页面刷新更短,用户更加满意。
但是,Ajax 的到来对 Web UI 自动化测试却未必是好消息。Web UI 自动化测试框架的执行方式类似于一个“队列”:用户定义了一系列的页面操作和验证,之后 Web UI 自动测试框架把它们按顺序一一执行。在传统 Web 应用中,由于每次向服务器端发起请求都会导致页面跳转,Web UI 自动化测试框架能很容易地判断出合适该执行下一个动作。 但是,Ajax 的行为是异步的,也就是说,用户的操作跟服务器端的处理现可以是并行的,不存在严格的顺序。起初,这给 Web UI 自动化测试框架的开发者带来了困扰,而且很长一段时间里这个问题都没有被很好的解决。
起初,Web UI 自动化测试开发人员主要有两种解决方案:硬编码等待时间和基于条件的等待。
硬编码等待时间: 这种方式是指在代码中指定一个静态等待时间,比如指定等待 30 秒后执行下一个操作。通常,这种数值的设置来源于经验。可以想象,如果这个等待时间比实际等待时间小,自动化脚本执行就会失败。而且,不幸的是,这样情况的确经常发生。随着测试环境的变化,实际需要的等待时间事实上是一个变量。以下几个因素都有可能导致它发生变化:
测试服务器在远程还是本地:毫无以为,由于网络的影响,服务器如果在远程,等待时间就会增加。
使用的浏览器: 浏览器 JavaScript 引擎的执行速度对等待时间也有影响。根据经验, Firefox 和 Chrome 都比 IE 要快。因此,如果用的是 IE,等待时间也会增加。
测试运行时的网络环境:如果测试时的网速较慢,等待时间会增加。
测试服务器的负载:测试服务器可能是独占也可能是共享的。如果自动化测试执行时的服务器负载较大,也会导致等待时间增加。
对于上述硬编码等待时间的问题,Web UI 自动化测试开发人员一般有如下对策。
尽量将等待时间设置的大一些。这样做存在一些问题。一来未必能保证在任何情况下该值都足够大;二来它意味着无论服务器在本地还是远程浏览器是快是慢,执行时间都一样,这对于服务器在本地以及浏览器比较快的测试环境是很大的时间成本浪费。
设置放大因子。这种方法是在编码的时候给等待时间乘上一个因子。这个因子或者是硬编码的,比如定义在一个参数文件中。当测试环境比较好的时候,设置小一点;测试环境比较差的时候,设置大一点。 高级一些的,可以动态地设置这个因子。比如,可以把某一个操作的执行时间作为计算该因子数值的参照。这个方法使情况得到了部分改善,但是仍然不能保持其执行的稳定性。
基于条件的等待: 由于硬编码等待时间方式的不稳定性,Web UI 自动化测试框架开始支持“基于条件的等待”。这种方式是采用“轮询”的机制判断是否可以开始执行一个操作。假设,当前要执行的操作是一个按钮点击动作。这个按钮默认是被禁用的,当页面从服务器端加载了数据之后这个按钮才被启用。因此,如果要保证这个操作成功,就需要“轮询”这个按钮的状态 – 直到发现它被启用了,才执行点击操作。 如果由于存在 bug 按钮一直不启用怎么办?所以,基于条件的等待的方法需要设置“超时时间”。如果超过这个时间按钮还没有启用,就认为这个测试用例失败了。所以,大家发现了,这个超时时间的设置其实还是个“硬编码等待时间”,所以某种程度上,这种方式仍存在硬编码等待时间方式的弊端。这种方式的另外一弊端是它带来了额外的编码成本,并且自动化测试代码中可能充斥了许多条件判断代码,而它们本身不完成我们的“业务”目标,仅是辅助代码。
B. 元素定位
一般的 Web UI 自动化测试编码无非包含这样几个步骤:定位及获取元素,执行操作以及验证结果。而且,通常 Web UI 自动化测试开发人员在元素定位和获取这个步骤上花费相当多的时间。大多数 Web UI 自动化测试开发人员希望所需获得的元素能有一个静态 id 属性。根据规范,id 属性的值在 DOM 树中必须是唯一的,因此通过 id 属性就可以很简单并且准确地定位元素。但是,这样的需求常常不是产品的需求,而只是为了满足 Web UI 自动化测试代码开发的简便。所以,对于产品开发人员来说,给每个需要在 Web UI 自动化测试中使用到的元素增加静态 id 属性不是高优先级的工作。而且,从技术角度上讲,有些 Dojo 应用中的元素也无法设置静态 id 属性。
对于 Dojo 应用,情况又复杂了一些。图 1 展示的是一个 Dojo 按钮小部件在 Firebug 中呈现的 DOM 结构。最外层的 SPAN 元素上有一个 widgetid 属性,内部的 SPAN 元素上定义了 id 属性,共同点是它们都是由一个前缀和一个动态的数字构成的。这个数字是由运行时该 Dojo 小部件在整个 DOM 树上的位置决定的,索引从 0 开始。因此可知,图 1 中的按钮小部件是整个 DOM 树上的第二个按钮。Widgetid 是每个 Dojo 小部件都有的属性,如没有特别指定的话,它就是如图中所示的动态结构。
所以,如何通过这种动态格式的 id 和 widgetid 定位获取页面中的元素成为另一个 Dojo 应用 UI 自动化测试开发的挑战。
图 1. Dojo 按钮小部件
C. Dojo 复杂性
读者不要误会,这里说的“Dojo 复杂性”不是说 Dojo 使用起来很复杂,而是说 Dojo 应用最后生成的页面 DOM 结构比较复杂。如图 1 所示,原本用一个 INPUT 元素实现的按钮,现在嵌套了四层 SPAN 元素。 所以,Dojo 小部件已不再是一个简单的 HTML 元素,而是由诸多 HTML 元素组合而成的。Dojo 按钮小部件只是一个简单的例子,对于像表格、日历之类的 Dojo 小部件,它们的结构更加复杂。针对某个 Dojo 小部件的操作也不再像操作普通的 HMTL 元素那样简单(调用某个 JavaScript 方法就可以实现),而是要具体定位响应事件(比如点击事件)的 Dojo 小部件的内部对应元素,然后触发具体的事件。
多数的 Web UI 自动化测试框架只能处理基本的 HTML 元素。这是合理的,因为框架开发者不知道最终的自动化测试开发者使用什么样的前段框架,因而只能提供最基本的“小砖块”。如果具体应用的自动化测试开发者有任何需求,可以在其之上进行二次开发。
对于 Dojo 应用的 UI 自动化测试开发者来说,可以选择直接定位和操作 Dojo 小部件内部元素的方法。但是,这样做的弊端很明显 – 缺乏重用性。 因此,应该将 Dojo 小部件作为最小操作单元。如何将 Dojo 小部件模块化成为了又一个挑战。
D. 产品复杂性
产品复杂性包含两层意思。
一是指产品本身页面层次多,界面元素多以及功能逻辑复杂。Web UI 自动化测试开发过程中,编写定位元素的代码通常占据很大一部分时间,编写这部分代码的工作量直接受页面元素多少的影响。功能复杂度一般不太会直线性增加编码工作量,但是有可能会增加调试的工作量。
二是指产品页面 DOM 结构复杂。Web 2.0 应用的开发模式与传统 Web 应用的开发模式有很大的不同。传统 Web 应用开发基本上是一个页面对应一个物理的程序文件(比如,.jsp 文件)。而 Web 2.0 应用的开发通常是整个应用只有少数几个物理的程序文件,页面的跳转和呈现通过 JavaScript 更新 DOM 树结构完成。所以,在浏览器中的跳转页面实际上往往只是 DOM 树结构被更新了。这样,就有可能带来这样一个问题:DOM 树上同时存在多个页面的 DOM 子节点。如果其他页面有与当前要定位的元素类似的元素,就很容易出现“错误定位”。
如何能在产品变得越来越复杂的同时,尽可能的减少开发工作量,也是一个 Dojo 应用 UI 自动化测试开发者要解决的问题。
E. 频繁的 UI 更改
频繁的 UI 更改在“敏捷开发”大行其道之后成为了又一个让 Web UI 自动化测试开发者头痛的问题。迭代中的产品 UI 经常随着客户的反馈不断地修改,于是自动化测试代码需要跟着更新。由此带来的 Web UI 自动化测试脚本巨大的维护工作量也成为了很多人反对进行 Web UI 自动化测试的重要原因之一。
难道,因此就放弃 Web UI 自动化测试吗?我认为,作为 Web UI 自动化测试开发者需要考虑的应该是:在这样的现实之下,如何能够最小化频繁的 UI 更改带来的代码维护工作量。从经验来看,并非 Web UI 上的任何一点点更改都会导致自动化测试代码的修改。这个问题的解决极大程度上依赖于测试框架适应 DOM 结构变化的能力以及元素定位时所使用的方法。
F. Dojo 升级
Dojo 升级有可能会引发 Dojo 小部件 DOM 结构的变化,继而带来自动化测试代码的维护工作量增加。尽管根据经验,这一点的影响不是很显著,因为目前一些常见的 Dojo 小部件(比如按钮输入框之类)都已经十分成熟,但是还是需要考虑。如果一个小部件的 DOM 结构发生了变化,如何能够最小化对自动化测试代码的影响?这是 Dojo 应用 UI 自动化测试开发者要考虑的问题。
Dojo 应用 UI 自动化测试框架挑选(设计)原则
讲解完了所有的挑战,让我们来看看当我们要设计或者选取一个 Dojo 应用 UI 自动化测试框架时需要考虑的因素和原则。
概括起来,主要有以下八大原则。括号内是该原则所对应解决的挑战。
隐式等待(A)
位置关系操作(B,E)
封装 Dojo 小部件 (C,D,F)
IBM 框架 (E)
代码生成(D)
测试数据的管理(D)
录制与编码 (D)
接下来,我们一一来讲解每一条原则。为了帮助读者理解,讲解过程中会引用内部自行开发的 Dojo 应用 UI 自动化测试框架 Water Bear 的部分内容。
挑战 A 提到了异步请求处理的问题也描述了几种解决方案,但都不甚理想。最理想的情况是自动化测试框架能够自己处理异步请求的问题,并且不需要开发者编写任何代码。这就是所谓的“隐式等待”机制。
目前,不少 Web UI 自动化测试框架都具备“隐式等待”功能,比如 Sahi 和 Selenium。Water Bear 使用的是 Sahi。当然,Sahi 同时也提供了对“硬编码等待时间”和“基于条件的等待”的 API 支持(因为,有些情况下还是不得不使用“硬编码等待时间”或“基于条件的等待”)。根据经验,超过 90%的情形是 Sahi 的“隐式等待”功能可以覆盖的,但是像“进度条”这样的功能就不得不用“基于条件的等待”。在进度条达到 100%之前,前端代码不断地向后台发起 Ajax 请求轮询进度状态。这种情形,Sahi 无法智能地得知哪一个 Ajax 请求的返回是“最后一个”,因此,就无法决定何时可以执行下一个动作。
清单 1. “进度条”代码示例
BrowserCondition cond = new BrowserCondition(getBrowser()) {
public boolean test() throws ExecutionException {
return getBrowser().isVisible(getBrowser().div(&100%&).in(es));
getBrowser().waitFor(cond, waitTime);
清单 1 展示了 Water Bear 中部分实现进度条功能的代码,也就是 Sahi“基于条件的等待”的代码。在 test 方法返回“真”之前(也就是代码发现文本“100%”出现了),waitFor 方法一直处于轮询状态。waitTime 是一个超时时间。如果这个时间达到了,即便 test 方法仍然返回“假”,waitFor 还是返回。通常,这时已经可以认定 Web UI 的功能执行出现了问题。
只所以把“隐式等待”放在第一个来讲,是因为 Web UI 自动化测试框架是否具备该功能对于 Dojo 应用 UI 自动化测试实施成败是至关重要的。
位置关系操作
Dojo 应用 UI 自动化测试框架应当具备根据给定对象定位其他对象的功能。这种相对的位置关系通常包括:在里面、在附近、在左面(右面)以及在上面(下面)等等。目前,Sahi 支持所有这些位置关系查找,另外还支持“在上面或者下面”以及“在左面或者右面”。这当中,“在里面”使用的最为频繁。
为什么要使用位置关系操作?有以下几个原因:
准确定位页面对象。在挑战 D 中,我们提到了在一个 DOM 树中有可能存在与要查找的对象属性与 DOM 结构完全相同的对象。尤其对于“确定”或者“取消”按钮,这种情况很常见。这时,必须要通过限定父对象来缩小查找范围。一般来讲,只要限制到当前页面的边界元素能够阻止跨页面 DOM 节点查找就够了(因为,当前页面有哪些对象一目了然,但是其他页面的 DOM 节点是由运行时的操作控制的,无法预料)。
封装 Dojo 小部件。这一点会在下面具体讲解。
提供更丰富的查找方式,如根据“标签”查找 Dojo 小部件。通常,文本框的左侧会有一个文本,比如“名称:”。如果,该文本框没有任何其他合适的静态属性用来定位,就可以通过“在附近”查找的方法,先定位“名称:”所在的元素,再找到这个文本框。
当 DOM 树结构未发生“功能性”变化时,避免自动化测试代码改动。所谓“功能性”变化是指页面上增加、替换或者移除了控件,比如按钮或者输入框等。当发生这些变化时,自动化测试代码不得不修改。但,如果发生的仅仅是 DOM 树结构上的变化,比如增加了不可见的 DIV 或者 SPAN 元素,只要位置关系操作使用得当,自动化测试代码可以做到不需要修改。
我们来看一个例子。图 2 左边是所测应用现在的状态:一个 Dojo 按钮小部件被一个 DIV 元素包围。在代码中,我们用按钮的文本“OK”并限制在图示的 DIV 中来定位它。之后,DOM 树结构发生了变化,变成了图 2 右边的样子,在原有的 DIV 元素和按钮之间增加了两层 DIV 元素。此时,代码不需要更改,因为这个“OK”按钮仍然在之前的 DIV 元素里。但是,如果用 XPath 定位元素,这种情况就很有可能要修改了。
图 2. 位置关系操作示例
封装 Dojo 小部件
在挑战 C 中,提到了 Dojo 小部件 DOM 结构的复杂性。最好的方式是封装 Dojo 小部件,将每个 Dojo 小部件作为一个整体操作。
Water Bear 使用的编程语言是 Java。它将页面中的 Dojo 小部件映射成一个 Java 类,以此封装所有该 Dojo 小部件的行为。由于 Sahi 支持“在里面”的位置关系操作,使得封装更加容易 – 只需要定位到 Dojo 小部件的最外层 HTMl 元素(一般是 DIV),其他的元素定位都限制在该元素之内。从而,即便页面上同时有两个同类型的 Dojo 小部件,也不会出现跨部件的错误操作。
图 3 展示了 Water Bear 中定义的与 Dojo 小部件对应的一些类。最上层父类 WebElement.java 代表一般的 HTML 元素,它封装了基本 HTML 元素的方法。DojoWidget.java 是所有 Dojo 小部件类的抽象。它定义了所有 Dojo 小部件类的通用方法,比如 getWidgetId()等。所有 Dojo 小部件类均继承自 DojoWidget.java 并定义自己的方法,比如 ComboBox.java 需要定义 selectByText 方法。
图 3. Water Bear 中与 Dojo 小部件对应的类
那么,页面上 Dojo 小部件如何和这些类对应关联起来呢?Water Bear 中通过.wdef 文件把两者关联起来。
清单 2. .wdef 文件示例
org.waterbear.core.widgets.dijit.Textbox=div|textbox|labelnear|dijit_form_ValidationTextBox,\
evo_form_ObjectNameTextBox,dijit_form_NumberTextBox,dijit_form_TextBox,aspen_config_directory_DNTextBox
org.waterbear.core.widgets.dijit.Checkbox=div|checkbox|labelnear|dijit_form_CheckBox
org.waterbear.core.widgets.dijit.Select=table|table|labelnear|dijit_form_Select
org.waterbear.core.widgets.dijit.Button=span|span|labelinside|dijit_form_Button
org.waterbear.core.boBox=div|textbox|labelnear|dijit_form_ComboBox
清单 2 展示了.wdef 文件的部分内容。它是一纯文本文件,每一行定义了一组映射关系。等号左边是 Dojo 小部件映射的 Java 类名,等号右边是页面上 Dojo 小部件的属性描述。其中,起关键作用的是是最后一个属性,它是 widgetid 的前缀部分。一个 Dojo 小部件类可以关联到多种页面 Dojo 小部件,因为有些 Dojo 小部件虽然被扩展了,但是对 Web UI 自动化测试来说扩展的功能不会被涉及到,于是可以用同一个类表示。
IBM 框架以前被称作为 ITCL 框架,由质量软件工程(Quality Software Engineering) 和 IBM 中有经验的自动化团队合作开发而成的。这个框架由三层架构组成,架构的实现贯穿了应用对象、任务和测试用例包(IBM 包)。
潜在于应用对象、任务和测试用例包之下的基本原理是:
层次化的体系架构
将“做什么”与“如何做”分离开来
一致和清晰的组织结构
快速增强的能力
迅速的调试
有效地组织文件
下面是对应用对象、任务和测试用例的解释说明:
应用对象:储存有关你的应用程序中的 GUI 元素信息。同时在这里也可以编写你的 Getter 方法,这些 Getter 方法可以返回对象,使调用者能够对这些 GUI 元素进行查询和操作。一般情况下,这些方法在 Task 层中进行调用。
任务:在这里你将编写可重用的方法,这些方法在你的应用程序中执行通用功能。同时在这里,你将编写可以处理和查询复杂的特定应用程序控件的方法。在任务中的方法可以被测试用例调用。
测试用例:导航一个应用程序,验证其状态,并记录其结果的方法。
这个理念应用到 Java 编程就是用不同“包”组织应用对象、任务和测试用例的类。无论具体的底层 Web UI 自动化测试框架是什么,用 IBM 框架的理念来架构上层测试用例的类都是有帮助的。 采用 IBM 框架能够有效地应对挑战 E,因为高重用性能够将代码改动控制在局部范围,并且一处改动即可纠正所有引用的代码。除了采用 IBM 框架,实际上如何合理地规划任务方法的粒度也很关键。但,这与具体应用就十分相关了。
一般来说,编写用来获取界面元素的代码(也就是应用对象层的代码)在整个开发工作量中占据很大的比例。通常,它需要开发者先借助一些能够显示 DOM 树结构的工具(比如 Firebug)观察元素的属性,然后决定用什么属性来定位元素比较合适。 另外,一些代码又是比较“机械”或者模式化的,例如给界面元素赋值的操作。这样的代码应尽可能的通过一些自动化的方式生成,就可大大节省开发的时间。
在 Water Bear 中,针对以上的两个问题,分别开发了两种代码生成工具。
一个工具叫 AppObjs CodeGen,它可以自动遍历当前的 DOM 树,从而生成应用对象层的代码。它用 Sahi 脚本实现,将结果输出在它的日志窗口中(如图 4)。 开发者将代码复制到自己的类文件中即可。尽管这些代码仍可能需要一些手工修改,比如重命名以增强方法的可读性,但较之完全用手工编码已节省很多时间。
图 4. AppObjs CodeGen 生成结果
第二个工具叫 Skeleton CodeGen。 它的输入是应用对象层的类,输出是任务和测试用例层的部分代码。对于符合特定模式的以及界面有很多 UI 控件的 Dojo 应用,这个工具比较有效。生成代码仍需要二次修改,因为有些逻辑无法自动生成。
测试数据的管理
最好能够将测试数据和测试用例代码分离,这样同样一份测试用例代码可以搭配 N 种不同的测试数据运行。另外,这种分离也可以轻松地做到为不同的测试环境定义不同的测试数据 – 有时候,一些测试数据是有环境依赖的。
Water Bear 利用开源软件 XStream 来实现测试数据与测试用例代码的分离。首先,测试数据被定义在一个 HashMap 中。测试用例在第一次运行时会将 HashMap 通过 XStream 自动序列化成一个.xml 文件。当自动化脚本再次运行时,如果.xml 文件已经存在,它就直接读取已有文件并借助 XStream 把文件内容反序列化成 HashMap。在之后的运行中,只要 HashMap 的数据结构没有变化,就无需重新生成.xml 文件。数据文件的存放目录可以通过配置文件指定,因此对于不同的环境只要修改配置文件指向不同的路径即可。
录制与编码
提高开发效率的另一个途径是提供录制功能。但是录制功能通常有如下一些问题:
录制生成的代码通常是“过程化”的。“过程化”的代码很难重用。即便是针对同一个页面功能模块录制的不同的测试用例代码也是孤立的。如果页面功能发生变化,所有测试用例需要重新录制。因此,表面上看录制功能似乎大大减少了开发时间,但是当页面发生变化的时候,它或许未必如我们想象的那么好。
录制的代码可读性通常比较差。一般来说,录制之后仍需要重命名变量以提高代码可读性。
所以,理想中的录制功能生成的代码应该是模块化的,也就是按照应用对象、任务和测试用例这三个层次生成。这样的代码即便于二次修改,也便于与已有代码集成。从使用的角度看,所有的测试用例代码完全用录制功能生成不太现实,较好的方式是只用录制功能辅助开发应用对象层和任务层代码。 当 UI 发生非功能性变化时,只需要重新录制受影响的任务方法并更新应用对象层。理论上,测试用例层不需要修改(除非测试用例的逻辑不得不修改)。
Water Bear 目前仅支持“过程化”录制功能。图 5 展示的就是 Water Bear Recorder 生成的代码。开发者把生成的代码复制黏贴到自己的测试类中即可在 Water Bear 中直接运行。
图 5. Water Bear Recorder
本文首先列举了 Dojo 应用 UI 自动化测试面临的诸多挑战,之后从这些挑战入手,结合个人的开发经验以及一个内部使用的 Dojo 应用 UI 自动化测试框架,提出了八个在选取和设计 Dojo 应用 UI 自动化测试框架时需要考虑的原则。当然,每一点的重要性不尽相同。比如“隐式等待”是必须的,而录制功能却没有那么重要。但是,若是这些原则都能符合,将大大提高 Dojo 应用 UI 自动化测试实施成功的可能性。
可能感兴趣的话题
为作者带来更多读者;为读者筛选优质内容;专注IT互联网。
最新评论(期待您也参与评论)
汇集优质的Python技术文章和资源。人生苦短,我用Python!
JavaScript, CSS, HTML5 这里有前端的技术干货!
关注安卓移动开发业界动态,分享技术文章和优秀工具资源。
关注iOS移动开发业界动态,分享技术文章和优秀工具资源。
关于伯乐在线博客
在这个信息爆炸的时代,人们已然被大量、快速并且简短的信息所包围。然而,我们相信:过多“快餐”式的阅读只会令人“虚胖”,缺乏实质的内涵。伯乐在线博客团队正试图以我们微薄的力量,把优秀的原创/译文分享给读者,做一个小而精的精选博客,为“快餐”添加一些“营养”元素。
欢迎关注更多频道
– 分享和发现有价值的内容与观点
– 为IT单身男女服务的征婚传播平台
– 优秀的工具资源导航
– 翻译传播优秀的外文文章
– 国内外的精选博客文章
– JavaScript, HTML5, CSS
– 专注Android技术分享
– 专注iOS技术分享
– 专注Java技术分享
– 专注Python技术分享
(加好友请注明来意)
网站使用问题
请在询问或者反馈
& 2015 伯乐在线
赞助云主机}

我要回帖

更多关于 不吝赐教的意思 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信