在刚过去的一个月中,我完成了一个小软件框架的设计与实现。期间由于并行开发的需要,在没有对代码完成单元测试的情形下我将之check in到了SVN的主干上,随后的心情很是忐忑。因为我知道我一定会犯错(事实也证明在单元测试完成之前就发现了两个缺陷),害怕给他人带来麻烦并影响自己的形象。
另外,由于对刚加入项目的单元测试环境完全不了解,所以在该框架的前期开发工作中我并没有运用单元测试渐进地保证软件质量。其结果可想而知,我花了不少时间去修复编码过程中遗留下来的低级错误。需要指出的是,所在项目是一个大型嵌入式系统,项目编译一次就得20分钟左右,调试效率可以想象。与之不同的是,单元测试可以在Linux/Cygwin这样的环境中完成,加上可以使用gdb进行调试,开发效率提高一个数量级应不是问题。
由于我深刻地体会到了单元测试对工作与生活质量的重要性,所以持“真正高质高效的软件开发工程师,一定是那些深刻理解并切实实施单元测试的人”这一观点。然而,就我过去几年的工作见闻来看,发现身边绝大多数的工程师并没有真正用心去拥抱单元测试。出现这样的状况,我认为存在一定的原由,因此想借本文谈谈一些认识。
有相当一部分工程师是因为并不了解什么是单元测试而没能尝到单元测试的好处。一部分人认为,平时开发工作中的调试其实就是单元测试(参见《明析单元测试》),有的则因为没有花时间学习单元测试而对之不了解。对于这些新手,我并不打算在本文对之“扫盲”,请下载ClearRTOS源码(点击下载)和本文的附件自行学习。 ClearRTOS 是我为《专业嵌入式软件开发》一书所设计的、可在Cygwin和Linux环境中运行的“实时”操作系统,其中涵盖有单元测试方面的内容,更具体的信息见文后。
另外的一部分人尽管实施过单元测试,但却没能从中受益,甚至得出“单元测试无用”的结论。这部分人的困惑是我最想在这里加以指出的。归结起来,我认为实施过程中的“缝太大”是其中一大主因。
不少团队将项目的产品代码与单元测试代码加以区别对待,这是产生“缝”的第一大根源。表现之一是,编译产品代码与编译单元测试代码采用完全不同的编译环境,程序员在日常工作中需要不停地在两个编译环境中进行切换。这种方式很容易让工程师感到麻烦,甚至因此遭到抵制或弃用。好的方式是,将单元测试代码的编译环境与产品代码进行无缝整合。比如,在嵌入式系统项目中做到运行“make release”或“make debug”实现产品代码编译,运行“make unitest”完成单元测试代码编译(ClearRTOS项目就是这么做的)。做到编译环境的无缝整合需要团队中存在精通编译环境构建语言(比如Makefile)的专家。很不幸的是,这方面的专家少得可怜,团队对这方面知识的精进也因为没有意识到其重要性而缺乏动力。
表现之二是,产品代码与单元测试代码区别维护。采用这种方式的团队,很容易在工作中将单元测试摆到更低的位置,开发过程会先以产品代码为主,然后(有时间时)再补上单元测试代码。这种方式很容易降低实施单元测试的效果,且容易因为产品代码与单元测试代码的不同步而带来更大的维护成本。实际上,实施单元测试的一大好处就是在对产品代码进行变更时,通过及时实施单元测试保证软件质量。及时维护单元测试代码从短期和长期都具有很好的经济性,而非象我们想象的那样成本高昂。
产生“缝”的第二大根源,是因为工程师在单元测试过程中不能方便地获得代码覆盖报告。以我的观点,代码覆盖报告应在开发环境中运行象“make creport”这样的命令而轻松获得(ClearRTOS项目同样实现了这一点)。我看到过一些项目,工程师为了获得覆盖报告,需要登录到一个Web服务器上才能查看,而非在工程师的工作机器上随手获取。
产生“缝”的另一大根源与单元测试的“打桩”方法有关。被广为采用的方法是通过使用象Cmockery这样的单元测试框架以打桩的形式,将被测模块独立出来。这种方式尽管被广泛采用,但我觉得所需付出的成本还是很高的。因为“桩的世界”与“产品代码世界”存在很大的“缝隙”,维护期间需要不停地在两个“世界”进行切换,更好的方式是将桩代码融入到产品代码中(细节我想通过另一篇文章给出)。
尽管我认为单元测试是一种有效的质量保障方法,但其有效性在工程界和学术界都存在一定的争议。
关于ClearRTOS
ClearRTOS现在是一个开源项目,读者可以通过SVN获取将来的新版本。为了能在ClearRTOS项目中获得HTML格式的代码覆盖报告,读者需在Linux/Cygwin中安装LCOV(链接)。
在ClearRTOS项目中获取代码覆盖报告的步骤如下:
1)运行“tar xzvf ClearRTOS.tar.gz”解压。
2)运行“cd ClearRTOS/build”进入编译目录。
3)运行“make unitest”编译单元测试程序。
4)运行“make test”执行单元测试程序。
5)运行“make creport”获取单元测试报告。报告可用IE、Chrome等浏览器打开ClearRTOS/build/coverage/index.html文件进行浏览。通过点击网页中的相关链接可以查看到每个源文件的代码覆盖情况。
QA 这个职位在中国有前途么?
QA还是很有前途的。
目前国内QA的工作面很广。web上点鼠标的是QA,linux上写脚本的是QA,编写单元测试的是QA,负责工具开发的是QA,推广TDD或者敏捷的也是QA。
正是因为QA这一统一的称呼,以至于让人经常混淆QA的概念,并且对QA的工作妄加评论。
根据QA的工作类型区分前途是比较合适的。
1、黑盒测试工程师。
这类比较常见,低端的比如web上测试页面的。高端的,比如linux下启动apache测试服务的。使用黑盒测试的技术去检测质量。质量来源于开发,而不是测试,所以他们只能检测而不能提高质量。
这类工程师的工作,目前正在被不断的挤压,随着自动化的发展,这部分的工作讲越来越少。所以人员也是越来越少。但是始终是不可缺少的。
不幸的是,目前这部分人是国内最多的,所以这部分竞争很激烈。已经是红海了。
目前国内的大多数中小型公司都是采用黑盒测试和人海战术来保证质量的,根源是这些公司的QA规划不合理,技术也相对浅薄,优秀的人员,这样的公司也留不住。所以QA的发展缓慢。
在这些公司工作,最重要的是了解业务。
如果接触不到产品的核心环节,比如代码、数据、业务细节,那么这类工程师最周会面临外包的危险。
大公司也会倾向于使用外包。
2、自动化测试工程师
使用qtp,selenium,watir,或者是其他的技术框架来自动化测试工作的。在*unix上做自动化工作的,比如编写shell脚本,或者其他的脚本,也是属于此类。
因为自动化在回归阶段可以节省人力,可以有效的对产品的质量进行度量,并且可以不断的累积,结合覆盖率统计,或者需求覆盖统计等手段,可以很好的保证产品线的开发质量。所以自动化是很重要的技术。
大公司一般都有这样的工作和人员配备。
不过前端的自动化,和后端的自动化,仍旧有一些弊病。很多公司倾向于使用分层自动化去解决不同层面的质量问题。
这部分相对有点技术含量,大公司招人,也是必考的内容。相对来说,有点前途。
但是一旦自动化方案稳定了,那么这类人也会面临职业发展困境。只不过目前自动化仍然在不断发展,这个问题暴露的不是很隐蔽。
这个领域的工程师将来会两极分化,一部分转向自动化工具的研发,一部分转向自动化case的维护。
3、白盒测试工程师
这部分人主要做代码分析,审核,编写必要的单元测试,并关注代码的各种覆盖率情况。
跟开发走的很近,可以尽早的发现bug,并能较好的适应产品的变化。
在敏捷模式中,是很重要的一个角色。这部分人也做单元测试,或者推TDD模式等。
白盒测试目前的技术还不是很全面和成熟,里面有价值的内容其实也很多。还有待进一步发展。
通过深入到代码层的测试工作,QA和开发可以实现很多的紧密配合,有助于及时保证业务质量,所以这部分工程师是很有价值的。
4、测试架构师
负责规划辅助测试的各种工具和平台。基本上是全能的。并能对自动化,技术改进和测试理论有很好的贡献。属于大牛级别。比如研究封装开源的框架,或者开发新技术,来提高QA的测试效率和保证质量覆盖。 不过这个职位将来会比较尴尬,可能会并到测试工具开发工程师中,或者在对应的工具开发团队担任管理。
这个职位,将来会死掉。企业不需要太多的title。。
5、性能测试工程师
国内的黄金职业,技术相对专业,但是精通了基本可以一劳永逸。性能测试的理论基本跟开发技术关联不大,所以还是很稳当的。
6、安全测试工程师
严格来说不算QA,虽然QA里面有做这个的,但是专业理论要求较高,跟开发技术的关联性也不是太大,具备通用性,所以也是很黄金的。
7、测试管理
去做QA的管理角色,比如带项目,QA数据统计和分析。带团队等。自然也是很黄金的了。
对于大部分公司来说,职位并不是严格的,很多人可能是一职多能。
发展方向主要有以下几种
1、走QA技术路线,测试分析,自动化,白盒,或者专心走性能测试,安全测试,测试规划等。
2、走RD技术路线,转行做研发。这个例子也很多。开发肯定比QA更可靠。 已经有不少先例了。
3、走管理路线。有管理爱好的,可以往这个方向发展。
4、走业务路线。去做产品经理,规划产品设计。也是蛮不错的职位。
5、开发测试工具,测试解决方案,提供测试服务。类似于51Testing和博为峰这样的公司。
本节用对话的形式讨论探索式测试的概念与实践。提问者是本书的一位虚拟读者,回答者是本书的作者们。
问:探索式测试中的“探索”该如何理解?
答:所谓探索是指有目的的漫游,即带着使命在某个空间中漫游,但没有预先确定的路线。探索包括对产品与技术的深入研究和基于研究成果的实践应用。
问:如何实施探索式测试?
答:本书第3部分将专门讨论该问题。这里先介绍一种可行的探索式测试实施方法,其灵感来源是基于测程的测试管理(Session-Based Test Management)。
探索式测试鼓励测试人员依据当前语境选择合适的测试流程与技术。在测试过程中,SMART原则 为测试者提供了很好的指导。
Specific(具体的):测试需要一个具体的目标。
Measurable(可度量的):有明确的指标可以评估目标是否达成。
Achievable(可实现的):目标应该是可实现的。这潜在地要求将一个大目标分解为多个小目标,且每个小目标也是具体的、可度量的、可实现的。而且,追踪小目标的完成情况提供了整体进度的可度量性。
Relevant(相关的):目标要切合当前语境,符合团队利益,且不忘企业愿景。
Time-boxed(有时间限制的):为每个目标设定一个合理的最后期限。这可以帮助测试人员在固定的时间窗口(Time Window)中排除不相关干扰,专注地工作。
依据SMART原则,测试人员可按如下描述展开探索式测试。
(1)测试人员制订测试计划。分析被测试应用,确立若干个具体的测试使命(Mission),每个使命针对一个可能的产品风险。
(2)测试人员将测试使命分解为一系列测试任务(Charter),每个任务都有明确的退出条件和时间限制。
(3)在短暂的测试计划之后,测试人员根据优先级选择一个任务,在一个固定的时间窗口中执行探索式测试(窗口的长度是60~120分钟,以90分钟为宜)。这样一个时间窗口被称为一个测程(Session)。在该测程中,测试人员设计测试、执行测试、评估测试结果。他会根据获得的知识和发现的疑问再设计测试用例,以拓展测试的广度和深度。
(4)在测程结束后,测试人员适当休息,放松思维。
(5)随后,他会反思当前的测试进展,并优化测试计划。也许他会为当前任务追加一个测程;也许他会再增加一个新的任务以弥补先前测试计划的不足;也许他会删除一些任务以反映他对测试对象的最新认知。
(6)这时,他会更有自信地开始新一轮探索式测试。
以上只是一种可能的探索式测试实施方法。负责任的测试人员一定会选择他自己的方式展开测试,因为只有作为领域专家的他,才能做出最符合语境的决策。此外,集合整个团队的力量,进行同行评审、头脑风暴、结对测试等活动,有助于产生更好的测试结果。
问:探索式测试与即兴测试(Ad-hoc Testing)有何区别?
答:探索式测试与即兴测试都强调“即兴发挥”,即利用直觉和经验,快速地测试软件,并不停地调整测试策略。软件专家Andrew Hunt指出,直觉是非显性知识的代名词,是大脑富(Rich)模式的杰出能力。如果人类只使用大脑的线性模式(包括语言可表达的显性知识、抽象能力、逻辑能力等),而漠视富模式的能量,我们将浪费自身的巨大潜力。
然而人是不完美的,某些直觉可能是认知偏见或错误。这就引出了探索式测试与即兴测试的关键不同:探索式测试是带着“反思”的测试。在探索式测试中,测试人员不断地提出假设,用测试去检验假设,并分析测试结果来证实或推翻假设。在此过程中,测试人员持续完善头脑中的产品模型和测试模型,然后利用模型、技能、经验去驱动进一步的测试。通过将测试学习、设计、执行和结果分析作为相互支持的活动并行展开,探索式测试总是在不停地优化测试模型、测试设计和测试价值。因为测试设计和测试执行的切换速度很快,许多人误以为探索式测试没有计划和设计。实际上,这些活动被划分到细微的时间片中,被反复执行。
即兴测试往往利用错误猜测、典型风险和常见攻击来快速地试探软件,可以在短时间内发现许多软件错误。但是即兴测试不强调测试的系统性和完整性,测试遗漏的风险很高,也难以发现一些需要深入研究才能发现缺陷。探索性测试通过测试来透彻地理解被测试产品,从而拓展测试的广度与深度,以持续优化测试的价值。
问:如果探索式测试是硬币的正面,那么硬币的反面是什么?
答:探索式测试的对立面是脚本测试(Scripted Testing)。脚本测试要求预先编写好测试脚本(Script),脚本规定了如何配置被测试软件、如何输入、如何判断软件输出了正确的结果。编写详细的脚本往往需要大量的测试资源。
如果运用得当,脚本测试可能有如下收益:
测试人员可以仔细地思考被测试软件。
测试脚本可以被项目关系人(Stakeholder)评审。
测试脚本可以被复用。
测试团队可以评估测试脚本集的完备性。
测试团队可以度量测试脚本的执行情况,以评估测试进度。
问:本书为什么反对脚本测试?
答:笔者不反对任何测试思想或方法,但是反对不分语境地滥用某一种测试思想或方法。例如,苛刻地要求编写详细的测试脚本可能会导致如下测试风险。
在测试执行前,大量的测试资源被用于测试设计。但是,产品的发展往往难以预料,早期的预先设计不能有效地处理动态变化的情况。有可能花费了大量的时间,却获得了一批充满缺点的测试脚本。
过于详细的测试脚本压抑了测试执行的灵活性,使得测试执行变成单调的过程。这可能导致测试人员对一些明显的错误视而不见,因为它们不在测试脚本的检查范围之内。此外,运行测试是观察软件行为、获得测试灵感、设计新测试用例的极佳时刻,这要求测试人员在测试执行时全神贯注、头脑灵活、反应敏捷。但是,枯燥的测试执行将使这些目标难以实现。
大量的详细测试脚本导致了沉重的维护代价。在进度压力下,测试人员可能没有时间更新测试脚本,这使得测试脚本不能随需求和产品一同演化。随着时间的推移,大量的测试脚本成为过时的、无人问津的“摆设”,原先投入大量资源所获得的测试资产几乎消耗殆尽。
要求测试人员编写求全责备的测试脚本,很可能带来不良的心理暗示:测试人员在不知不觉中将编写脚本看做测试的目的,而不是辅助测试的工具。他们会盲目追求脚本的数量,而很少关注产品和项目的风险,用看似“完备”的脚本集提供虚假的安全感。更糟糕的是,醉心于脚本数量的测试领导很可能会鼓励这种以“文案工作”为核心的测试流程,使测试的价值进一步流失。
相比“硬币”的比喻,笔者更喜欢Cem Kaner的观点:“纯粹的探索式测试和纯粹的脚本测试好似区间的两个端点。在实践中,大多数测试者位于这两者之间。然而,大多数好的测试非常接近探索式测试的一端。”
此外,根据语境驱动测试的基本原则,测试人员需要经常反思:根据当前语境,测试策略是应该偏向探索式测试,还是脚本测试?如何综合它们的优势,以获得更好的测试结果?
问:探索式测试编写测试文档吗?
答:探索式测试者创建任何有助于实现其目标的文档,他会撰写并维护符合语境的测试文档。这里提出一种可能的测试计划编写方法,供读者参考。
在不同的开发组织,测试计划有不同的名称:测试计划、测试规格说明和测试设计文档等。但是,大多数测试计划会涵盖产品概述、测试范围、测试风险、测试策略、部分详细测试用例、资源分配和日程安排。
与脚本测试在测试执行前编写大量文档不同,探索式测试在整个测试过程中持续编写、修改、优化测试计划。测试计划的内容、格式、详略是由项目语境决定的。
在项目计划阶段,测试计划可以包含产品概述、测试范围、测试风险、测试策略、资源分配和进度安排。编写文档的目的是建立对产品的整体认知,根据风险提出相应的测试策略,并安排测试任务和日程。测试计划不必面面俱到,用精简的格式记录必要的内容即可。此时项目的不确定性较大,未知因素较多,很可能不适合做出详细的测试决策。
在项目计划阶段,应该邀请项目关系人对测试计划进行评审。评审的形式可以多种多样,会议评审、桌面走查、邮件评审、在线文档批注、头脑风暴皆可。评审的目的是发现“大图景”上的缺陷,并丰富测试策略。眼界开阔的项目经理、负责任的开发人员、经验丰富的测试同事能够提供很好的建议。
在测试阶段,测试人员需要根据测试进展持续更新测试计划,内容可能包括产品模型、检查列表、测试策略、覆盖大纲和风险列表等。测试计划应该是“活”的文档,能够反映测试人员对产品和项目的最新认识。对测试范围、测试风险、测试策略和进度安排的重大变化应该写入测试计划,重要的测试用例(如发现严重缺陷的测试用例)也应该及时融入测试策略。测试计划应该尽可能地精简,这有助于文档的持续更新,而冗长的文档将不利于阅读、增删和修订。
当测试人员认为测试计划相对完整之后,他还应该邀请项目关系人对测试计划进行评审。评审的目的是发现测试遗漏,补充测试策略,提高测试覆盖率。
以上方法有三个重要的特点。
测试计划的编写与改进贯穿整个测试过程。在测试执行之前,对测试计划不必求全责备。在测试执行中,测试人员根据测试反馈,动态地调整测试范围、项目风险和测试策略,生成新的测试用例,并对测试计划做必要的更新。
测试人员持续地收集对测试计划的意见,并将改进意见纳入测试实践。正式和非正式的测试计划评审可以识别潜在风险,丰富测试策略。在一些测试流程中,测试计划评审只发生在测试开始之前。但是,笔者建议在测试流程的中期再次评审测试计划,目的是进一步丰富测试策略的多样性,并发掘可能的测试遗漏。这时,测试团队对产品与风险拥有更深刻的理解,能够提出更具针对性和多样性的测试策略,也可以帮助测试人员发现测试盲点,从而提高测试覆盖率。
测试计划侧重测试规划(一组指导测试过程的想法)和测试策略(一组指导测试设计的想法)[Kaner01],而不是脚本化(Scripted)的测试用例,即测试计划用精简的格式表达测试人员的测试想法,而不必详细记录测试用例的设置、步骤和预期结果。测试人员可以用文字、列表、表格、思维导图等任何合适的方式表达想法,目的是激发测试灵感,并促进测试思想的交流。此外,他会利用发现缺陷的测试用例来完善测试策略,而不是让过去的测试用例控制未来的测试。
关于其他形式和内容的测试文档,测试专家Michael Bolton的文章What Exploratory Testing Is Not:Undocumented Testing提出了很好的建议。
问:探索式测试的核心优势是什么?
答:探索式测试的核心优势是有助于“学习”。此处的学习是指学(获取知识)与习(应用知识)的持续过程。
对于测试人员,软件测试是一个持续学习并实践的过程。他的学习范围如下。
行业知识:为什么需要这个软件?软件如何帮助使用它的人和团体去获得成功?
用户角色:目标用户是谁?他们有什么特点,有什么期望?软件如何帮助他们去获得个人成就?
软件产品:产品是一种解决方案,它解决了行业和用户所面临的问题吗?
计算平台:只有深刻理解软件所依赖的计算平台(如操作系统、中间件和网络协议等),才能更好地进行测试。
开发技能:理解项目所使用的具体技术,知晓典型的技术缺陷,具备测试开发的能力。
测试技术:针对当前项目,选择合适的测试技术,并能够熟练地应用。
程序缺陷:研究已知的软件缺陷,提炼错误模式,制订缓解或预防方案。
开发团队:语境决定策略和实践。在一起工作的人,是所有项目语境中最重要的组成部分。
测试人员不需要在项目之初就掌握所有知识,他可以通过每天的工作去逐步理解用户、项目、技术和团队。更重要的是,他需要在每天的工作中实践所学的内容:规划测试方案、创建并执行测试用例、分析测试结果和编写测试报告。实践是练习,是“学”的自然延伸。知行合一才构成“学习”的完整内核。
学习的一个重要成果是成为更优秀的测试人员。他们可以根据项目语境,选择合适的流程、技术和工具来高效地测试,以推动软件质量的提高。
近期,为了对外缩短发布周期,对内建立更加快速的反馈机制,我们团队全面尝试了从“先开发后测试”到“边开发边测试”的转变。在此过程中,我感到测试用例设计和评审、测试执行两个活动需要做出一定的调整。
1、测试用例设计和评审:从时间驱动转变为用户故事驱动
当开发和测试顺序执行时,我们的功能测试计划会指定测试用例设计和评审时间。当开发和测试并行时,我们不再以时间的维度来计划测试用例的设计和评审,而是以用户故事的维度。我称这一转变为“从时间驱动的测试用例设计和评审转为用户故事驱动”。“用户故事驱动的测试用例设计和评审”有两个要点:(1)以用户故事的优先级来确定测试用例设计的优先级;(2)配合开发节奏,以一个或多个用户故事为批次,多次分批进行测试用例评审。为了让团队对测试用例设计的进度有一个直观的了解,我们为每个用户故事创建了一个设计测试用例的任务。
2、测试执行
(1)从广度优先转变为深度优先
以前的测试我们会安排多个轮次,每个轮次的测试重点和方法都有所变化。而现在再也没有测试轮次的概念了,因为除了回归测试期间,基本都是每日构建,有什么测什么。那么这样的变化要求测试人员怎样调整测试执行呢?我认为是要把“回锅肉”模式变为“煮饭”模式。“回锅肉”模式是先把五花肉煮个八成熟(类似我们先把某个用户故事测试完成),然后再和大蒜等配料一起回到锅里炒,并最终装盘(类似我们通过回归测试最后确保所有功能在一起工作正常,然后发布)。“煮饭”模式是“一灶火把饭煮熟”,因为夹生饭再煮又浪费时间又不好吃。这类似我们的测试要围绕该用户故事尽量一次测试充分,而不要象以前因为知道后续还有多轮测试而有意识地推迟某些当前就可以做的测试。从某种程度上说,是要把广度优先测试转移到深度优先测试。
当然,深度优先的一个缺陷在于可能会忽视用户故事间的关系。因为用户故事的INVEST(Independent,Negotiable,Verifiable,Estimable,Small,Testable)特性,故事间是比较独立的。但是由用户故事构成的系统却不能忽视故事间的内在关联。所以,我建议在测试单个用户故事之后,在回归测试之前,尽量穿插一定的用户场景测试(scenario testing),把在某些用户场景上自然集成起来的多个故事一起测试,从而尽早暴露故事间的一些缺陷,弥补深度优先测试的不足。当然,最后的回归测试也是对深度优先测试的一个补充。
(2)测试执行需要借力于持续集成
持续集成不是传统测试范畴的内容,但为了及时保卫胜利果实,对已经进行过测试之处的正确性和稳定性提供必要的及时的检查和信心保障,持续集成不可或缺。边开发边测试的过程中,我们通过集成了自动化测试的每日构建,及时发现了由于版本不断被修改而造成的意料之外的问题。开发人员也因为能将引起这些问题的原因及时锁定在近期刚刚修改过的内容上而加快了修复一些棘手问题的速度。当开发和测试人员马不停蹄地向前冲的时候,持续集成为我们巩固了后方。
除了上述两个较大的方面,我也隐约感到其它一些方面,如测试进度报告、测试人员心态的困扰等,也有一些变化,还有更多细节需要调整。所以,我们仍然需要拥抱变化,应需而变,持续调整和改进我们的测试实践。
版权声明:本文出自 zdlzx 的51Testing软件测试博客:http://www.51testing.com/?56882
原创作品,转载时请务必以超链接形式标明本文原始出处、作者信息和本声明,否则将追究法律责任。
如何抓住软件测试的主线及确定主要功能?
最近一直在思考以下两个问题:
1、每个项目每个Build测试执行过程中如何把握测试主线?
2、哪些属于测试模块的主要功能?对于测试人员有哪些途径去积累判断测试模块的主要功能?
对于第一个问题,看起来很简单,但是执行起来不容易,有时即使对于有经验的测试人员也很容易在测试过程忽略掉每一轮测试的重点,陷入一个小模块进行详细测试,而忽略了对这轮测试总体测试侧重点的把握。所谓测试主线就是该轮测试我们的测试重点,如冒烟测试我们主要看测试模块主要功能在正常情况下能不能实现,影不影响项目进入系统测试;本地化测试时,当主要功能较稳定时,我们关注的更多的是字符,乱码,拼写,提示信息,标点等等是否已本地化;测试后期我们更多的是验证bug,而不是进行详细的系统测试。实际测试中,我经常看到同事对每轮的测试主线把握的不是太好,如我们公司对每个项目每轮测试都要进行冒烟测试,在冒烟测试问题反馈中,经常看到上报的有些小的控件问题,如按钮没有完全展示出来,输入框对特殊字符及快捷键没有做处理等,这些确实是问题,但是这不是我们进行冒烟测试的初衷;一个项目的最后一个Build有时会看到同事会提好多建议,提建议固然很好,但是我们应该清楚,最后一个Build测试重点一般主要放在验证上轮Bug上,对于建议应该在前几个Build来提,而不是最后一轮,因为最后一轮意味着项目要投向市场,面临着市场压力,即使提了建议,开发人员为了保持主要功能的稳定性及降低风险,一般也不会修改这些建议的。
那么我们如何知道每轮测试的侧重点?拿我们公司来说,每轮测试都会有更新文档和本轮测试重点描述的相关文档;如果没有这方面的文档,这时候就可以去询问测试项目负责人或测试执行负责人。当我们知道该轮测试重点后,结合测试时间,大致划分下每个时间段测试哪些模块,具体执行中牢记本来测试重点,从负责的整个测试模块来把握测试主线,尽量避免被一个小模块拖累了整个项目的测试进度。
关于第二个问题:哪些属于测试模块的主要功能?我的理解是这里的主要功能是由用户的使用频率和这个功能失效对用户的影响共同决定的,举个简单例子,我们经常提到的UI测试,一般UI的关注点在于美观性,控件类等问题,如进入tab页后信息的展示,这个对功能的使用没有影响(功能已经实现),但是用户进行相关操作时直接就可以看到这些信息如何的展示,用户看到和使用到的频率很高,如果信息的展示不是很好,就会直接影响用户对我们产品的使用信心。决定主要功能的是用户,所以对于测试人员有哪些途径去积累判断测试模块的主要功能也是从如何了解用户习惯展开的,总结了以下途径:
1、直接与用户接触了解用户使用习惯,了解用户主要关注哪些模块,这些模块这些点就是我们所说的主要功能。(对于我们公司很少有这种机会)
2、关于1是个很理想的状态,但是大多数情况下,我们测试人员是很少有机会接触到用户的。这个时候可以间隔性的请技术支持或者销售人员给我们介绍用户主要关注哪些模块及哪些问题的出现会使用户非常恼火以及现场问题收集等等,技术支持和销售人员对于用户的了解一般会比我们测试人员多。(这个途径也像老大建议过,但是一直没有做起来,导致现在对现场问题及用户习惯处在模模糊糊的状态)
3、看需求,这里的需求有产品需求说明书和用户需求说明书,通过需求说明书我们就可以知道用户的需求,优先级等,这些也是我们判断是否是主要功能的依据。
4、经验,这个主要就看我们对测试模块的熟悉程度。大家可能有这个经验,就是一个模块测试久了,对这个模块的总体把握,哪些相对重要在心里会有大致的区分,这个就要求我们在测试执行过程中,其他时间多了解我们的测试项目,哪些是主线,哪些是次线。(这种途径个人感觉存在一定风险,有时只是自我感觉是不是主要功能,实际上是不是-----)
5、向以前测试该模块的测试人员学习,询问;向小组领导请教,一般测试领导对模块主要功能的把握比较准
6、看产品使用说明书,当我们刚接手一个新模块时,还没有对该模块形成所谓的经验,这时候我们可以看看使用说明书,了解该模块,使用说明书一般是技术支持人员来写(我们公司如此,其它公司不太清楚),写的角度也可能是从用户角度出发,对我们对一个模块主要功能的判断有一定的启示意义。
测试过程中,也遇到对主要功能把握不准的情况,请教老员工,老员工说过最多的就是:经验。其实我很反感这句话,对于新人能不能告诉我们掌握主要功能的途径,或者说经验积累的方法,通过哪些途径去积累这些经验,而不是动不动就那经验来显摆,这样才能让新员工快速积累这方面的经验,抓住主要功能,少走些弯路,快速成长起来,这里谴责下这些不负责任的老员工,呵呵。
以上只是自己的一点看法,如测试前辈有更好的途径和建议,请指导,共同进步,谢谢。
版权声明:本文出自 没翅膀的飞鱼 的51Testing软件测试博客:http://www.51testing.com/?363907
原创作品,转载时请务必以超链接形式标明本文原始出处、作者信息和本声明,否则将追究法律责任。
对于试用期新人来说没正事做,并不等于没事做。一些练练手的东西还是源源不断的。这两天在捣鼓日立扶梯项目的一些功能。其实这项目还未正式签约的,但一些功能上的实践还是必须的。这项目是日立公司应用于新品发布会,运用Flash动态画面,实时展示他们扶梯的安全性。日立对扶梯运行状况进行实时监控,将一些扶梯的运行数据从服务端传输到客服端,也就是平板电脑。这是基于C/S模式的,Flash在这方面的应用我也是首次尝试。
Flash对实时数据传输提供了套接字连接,也即是Socket连接。AS3.0提供两种不同类型的套接字连接,XML套接字连接的XMLSocket类和二进制套接字连接的Socket类。对于我来说如果服务器能发送XML数据过来当然是最好的,但可惜服务器是传输十六进制的数据的。相对XMLSocket来说,Socket数据处理更为底层,可操控范围更大,但难度也较大。
对于实时传输数据有几大有解决的问题,如何持续正确连接,如何处理粘包,如何缓存数据等。正确连接,可以采用类似三次握手原则,服务端向服务器发起连接请求,服务器接收后返回一个验证码,客服端接收到后确认可以进行连接,然后后向服务器发回一个指定的数据,服务器确认了数据的正确性再发送指令确认客服端可以连接了。
Flash的Socket连接是基于TCP的,所以不存在掉包的情况,最要是如何解决粘包或断包。从网上看到一些解决法案是服务器发送的数据在包头上加上数据长度信息,当客服端接收到数据包,先读取信息头,读取指示后面的数据有多大。如果已经读了信息头,则看能不能收到满足条件的字节数,若数据流里的数据满足条件,开始读数据,如果数据流里还未满足读取数据条件,则继续读取数据。至于如何缓存数据,最简单的就是将数据保存到数组中,不过如何在适当的时候读取数组中的数据还未想到较好的办法。
package { import flash.display.Sprite; import flash.net.Socket; import flash.events.IOErrorEvent; import flash.events.SecurityErrorEvent; import flash.events.ProgressEvent; import flash.events.Event; import flash.errors.IOError; import flash.events.MouseEvent; import flash.errors.EOFError; import flash.system.Security; import flash.utils.ByteArray; /** * * @author whk */ public class SocketExample extends Sprite { private var targetServer:String = "192.168.0.68"; //连接ip地址 private var port:uint = 9991; //连接端口 private var socket:Socket; private var str:String; private var response:String; private var msgLenMax:int; //收到的消息最大长度 private var msgLen:int; //消息长度 private var headLen:int; //消息头长度 private var isReadHead:Boolean; //是否已经读了消息头 private var bytes:ByteArray; //所读数据的缓冲数据,读出的数据放在这里 public function SocketExample() { isReadHead = true; headLen = 2; //2个字节 msgLenMax = 4028; bytes = new ByteArray(); //Security.loadPolicyFile("socket://"+targetServer+":"+port); socket = new Socket(); btnSend.enabled = false; btnConnect.addEventListener(MouseEvent.CLICK, btnHandler); } /** * 处理按钮事件 */ private function btnHandler(event:MouseEvent):void { switch (event.target.name) { case "btnConnect": btnLabel(); break; case "btnSend": sendRequest(); break; } } private function btnLabel():void { if (btnConnect.label == "连接" || btnConnect.label == "重新连接") { //注册socket事件 configureListeners(); //进行socket连接 connectToSocketServer(); } if (btnConnect.label == "关闭连接") { if (socket.connected) { socket.close(); btnConnect.label = "连接"; pringMessage("客服端已关闭连接"); } } } /** * 连接socket服务器 */ private function connectToSocketServer():void { try { socket.connect(targetServer, port); } catch (error:SecurityError) { pringMessage("SecurityError: " + error); } } private function configureListeners():void { socket.addEventListener(Event.CONNECT, connectHandler); socket.addEventListener(Event.CLOSE, closeHandler); socket.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler); socket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler); socket.addEventListener(ProgressEvent.SOCKET_DATA, socketDataHandler); } /** * 连接成功 */ private function connectHandler(event:Event):void { btnSend.enabled = true; btnConnect.label = "关闭连接"; pringMessage("连接成功" + "\n" + "connectHandler: " + event + "\n"); btnSend.addEventListener(MouseEvent.CLICK, btnHandler); trace("connectHandler: " + event); trace(txtRead.text); } /** * 向服务器发送数据 */ private function sendRequest():void { trace("sendRequest"); response = ""; //发送内容 writeln("GET /"); } private function writeln(str:String):void { str += txtSend.text + "\n"; if (socket.connected) { try { //将UTF-8字符串写入套接字 socket.writeInt(int(str)); txtRead.text += "发送数据的数据:" + str; socket.flush(); } catch (error:IOError) { pringMessage("socket.flush error\n" + error); } } else { //进行socket连接 connectToSocketServer(); } } /** * 接收数据,并读取 */ private function socketDataHandler(event:ProgressEvent):void { trace("socketDataHandler: " + event); readResponse(); } private function readResponse():void { try { //parseNetData(); var str:String = socket.readUTFBytes(socket.bytesAvailable); response += str; trace(response); //遍历数据包 while (socket.bytesAvailable) { var data:int = socket.readByte(); trace(data); } txtRead.text += "接收的数据:" + str + "\n"; } catch (error:IOError) { pringMessage("当socket已关闭而去读取时引发I/O 异常\n" + "socket.read error\n" + error); } catch (error:EOFError) { pringMessage("没有数据可读而读取时引发EOF异常\n" + "socket.read error\n" + error); } } /** * 解析网络数据流 * 根据信息头长度读取数据 */ private function parseNetData():void { //如果需要读信息头 if (isReadHead) { if (socket.bytesAvailable >= headLen) { //读出指示后面的数据有多大 msgLen = socket.readShort(); isReadHead = false; } } //如果已经读了信息头,则看能不能收到满足条件的字节数 if (!isReadHead && msgLen <= msgLenMax) { //如果为0,表示收到异常消息 if (msgLen == 0) { //一般消息长度为0的话,表示与服务器出了错,或者即将被断开等,通知客户端,进行特别处理 return; } //数据流里的数据满足条件,开始读数据 if (socket.bytesAvailable >= msgLen) { //指针回归 bytes.position = 0; //取出指定长度的网络字节 socket.readBytes(bytes, 0, msgLen); isReadHead = true; } } //如果数据流里还满足读取数据条件,继续读取数据 if (socket.bytesAvailable >= headLen) { parseNetData(); } } /** * socket服务器关闭连接 */ private function closeHandler(event:Event):void { btnConnect.label = "重新连接"; pringMessage("socke服务器已关闭\n" + "closeHandler: " + event); trace("closeHandler: " + event); } private function ioErrorHandler(event:IOErrorEvent):void { trace("ioErrorHandler: " + event); pringMessage("输入/输出错误并导致发送或加载操作失败\n" + "ioErrorHandler: " + event); } private function securityErrorHandler(event:SecurityErrorEvent):void { trace("securityErrorHandler: " + event); pringMessage("尝试连接到调用方安全沙箱外部的服务器或端口号低于 1024 的端口\n" + "securityErrorHandler: " + event); } /** * 将消息打印到屏幕 * @param m:打印的消息 */ private function pringMessage(m:String = null):void { txtError.visible = true; if (m == null) { txtError.text = ""; txtError.visible = false; return; } if (txtError.text == null) { txtError.text = ""; txtError.visible = false; } txtError.text = m; } } } |
Socket三次握手例子:
package { import flash.display.Sprite; import flash.events.ProgressEvent; import flash.net.Socket; import flash.utils.ByteArray; /** * * 三次握手连接socket * 来源socket编程 */ public class SocketHandshake extends Sprite { public const DETERMINE_VERSION:int = 0; public const RECEIVE_CHALLENGE:int = 1; public const NORMAL:int = 2; private var stateMap:Object; private var currentState:int; private var socket:Socket; public function SocketHandshake( ) { stateMap = new Object( ); stateMap[DETERMINE_VERSION] = readVersion; stateMap[RECEIVE_CHALLENGE] = readChallenge; stateMap[NORMAL] = readNormalProtocol; currentState = DETERMINE_VERSION; socket = new Socket( ); socket.addEventListener( ProgressEvent.SOCKET_DATA, onSocketData ); socket.connect( "localhost", 9000 ); trace(currentState); } private function onSocketData( event:ProgressEvent ):void { trace(currentState); var processFunc:Function = stateMap[currentState]; processFunc( ); } private function readVersion( ):void { try { var version:int = socket.readInt(); trace(version); } catch (error:Error) { trace("error:"+error); } currentState = RECEIVE_CHALLENGE; socket.writeInt( version ); socket.flush( ); } private function readChallenge( ):void { var bytes:ByteArray = new ByteArray( ); socket.readBytes( bytes, 0, 8 ); currentState = NORMAL; socket.writeBytes( bytes ); socket.flush( ); } private function readNormalProtocol( ):void { } } } |
很早之前就说好好总结一下自己的职业,一直忙于一些乱七八糟的事,现在这个时间难得偷得空闲,趁着有感觉,赶紧进行敲下“这些年,我的软件性能测试”来祭奠我这IT行业的几年......
记得第一次做性能测试项目,心情是忐忑的,觉得,性能测试,做不好就背包滚蛋了都可能,不过当时带我做项目的老大给了我很大的信心和支撑,我在做的过程中,遇到的疑问,他都会耐心的给我以解答或者给我一个方向,让我去前行,解决,随着一个个问题的出现和解决,自己每一天也过的感觉很充实。也是在这个项目里面,这个老大告诉我,作为性能测试,如果仅仅只会用工具,这个只能算初级性能测试工程师,重要的还是设计能力,思想为王,于是,我从他口里听到了一个词:性能建模和容量规划......当时,我真心的不知道这个是什么,有的就是对老大的崇拜和对未来的路如何走的思考......
第一个性能项目,如期完成,对google的GA插入的js代码进行测试,验证该js注入website之后,对性能的影响(因本身js需要做下载和数据上报,中间的过程需要看下情况如何),测试过程中,发现会有大部分的用户响应时间比较长,当时就是按照2-5-10法则来做的响应时间是否合理(想想,这个就是传说中的拍脑袋吧呵呵),想到该如何分析是哪里原因呢?第一次做性能测试,看到结果之后,欣喜的同时更多的是一种忐忑,不知道对着面前的report该如何下手?好在,有老大给予指导,分析了带宽,排除带宽的影响,查看server本身的资源,也无问题,同时细分验证,发现大部分的时间是消耗在server层,于是基于此基础,直接看了下apache(当时我们用的server)的队列等待(当时用的方法很简单,直接ps -ef|grep httpd|wc -l),发现随着虚拟用户的逐步增多,会造成排队的数也越来越多,初步怀疑是配置问题,咨询了运维,apache 的配置没有动,用的默认的配置方式,于是我们提出,需要查看并尝试调整该配置文件。在查看的过程中,参考网上朋友的资源,发现maxclient的确是默认的,修改之后,进行重启,回归,发现问题还是存在,,,难道是别的地方慢?怎么弄呢?这个时候运维告诉我们,apache只修改这个还不够,还需要修改一个隐藏变量,serverlimit,只有修改了它,maxclient修改超过1000的时候才会生效。于是也是瞎子摸象,动手试一试,发现,果真是这里,修改了之后,排队的用户减少了,超过5S以上响应时间的用户百分比也降低,于是开始准备性能测试报告(报告写了我十多次才发出去,那个苦啊~~~),顺便给予运维建议,因当时这一服务对应的apache是单点,建议去除单点,再申请一台机器作为热备,高峰时还可作为balance功能使用。于是,我的第一次性能测试,随着这第一份性能测试报告的发出,结束了.......也从此,我开始踏上了性能测试的路,开始爱上了性能测试
在后面的工作中,大大小小的项目也接触了一些,零零散散的一些性能问题也跟研发,DBA一起定位,调优解决了,但是慢慢的我在思考,性能测试就是这样了吗?在随后而来的一次项目中,让我知道,其实性能测试并不如此,远非我之前看到的,之前看到的还不够深入......
记得那次项目,是对c++开发的一个搜索服务server的测试,在测试的过程中,发现,开始没多久,磁盘就速度变大,内存也消耗的很快,cpu飙的特别快......可并发用户才5啊,,,,这个问题会在哪里呢?cpu飙的特别快,那一般是运算复杂,调度频繁,切换频繁等原因造成的,于是,找到研发,咨询相关的算法,是否过于复杂,可否再优化?研发也好沟通,给耐心的讲解了算法,并回复,当前无法再优化,只能后面逐步来看。那难道就这样放出去?反正公司不差服务器,堆服务器就是了,硬件解决性能问题似乎成了行业的潜规则,而且产品也说了,平常根本也没多少人会用这个,这样的问题,他们也可以接受......就这样发到外网?不,作为一名性能测试工程师,如果就这样洗洗睡了,那我们的价值在哪里?于是,跟研发建议,如果确实是算法的问题,在当前我们无法进行修改,调优的基础上,是否可以进一步请求资深和专家研发给予check和给予建议,对该风险进行分析?于是,研发开始进一步确认到底是算法哪一块,哪一处消耗最多,戏剧的是,分析到最后,发现该问题并不是算法是主要凶手,之前冤枉了算法,而是程序本身的一个bug,研发之前对于c++里面用到的hashtable,错误的认为是有序的,但实际该hashtable在处理的时候是无序的,从而造成每次都会生成新的追加的文件,这样文件越来越大,造成磁盘疯狂的长,同时写磁盘这个过程对cpu的占用也就飙的很高,再一个,读数据的时候,之前都是直接很疼的从磁盘上拿,没有做map处理,map处理之后使得程序都从内存里面读,这样响应时间也得到了有效的提升,并且,研发还对不该加锁的地方进行了顺序读的锁处理进行修复,导致server的吞吐率得到提升......最终,版本打回研发,研发自测后,再进行提测和验证
在上个项目中,让我觉得,作为一名性能测试工程师,不要错误的将自己定义为架构师(很多行业的人都觉得性能测试工程师很牛逼,牛逼的过程中,不自然间就把这个职位等价成了架构师,其实我想说,架构师是高于性能测试工程师的),但是,一名优秀的性能测试工程师应该不断的靠近架构师,只有这样,才能真正的从根本上去发现,解决问题,才能在研发体系中更好的体现自己的价值.也在这个项目之后,我开始思考并有了后面我主导的一个虚拟项目,“BMW软件性能平台”(据说名字霸气点是好事:))......
另:这些年的项目经验,还让我认识到,对于有的性能问题,在调优的时候,并不是说就一定需要用技术解决的,有的问题,技术不能解决的,需要思考和尝试业务需求上的调整,有的性能问题的定位和分析,并不是说就一定要那么费劲周折,高并发,查这里,看那里来定位和解决,有的只用单个用户,发个请求,抓个包,看下timechart,看下代码构成,就可以解决了.性能测试,我一直坚持,它跟监控是离不开的,有了监控,有了数据准备和数据收集,我们才能更快更好的发现问题,分析和解决问题(这个也是我弄“BMW软件性能平台”的原因)
这些年,我的软件性能测试项目经验,让我获得了很多,也失去了很多,对于行业的认识也看的更加深入了些,开始接触并学习之前根本不知道的语言内核知识,TCP/IP原理等网络知识,深深的感觉到学海无涯,而吾身有涯......
这些年,我的软件性能测试,写在我即将逝去的28岁......
最近在研究自动化测试框架,也和网上的很多朋友聊了很多各种自动化框架的实现,我对其总结归纳比较下。当然,一家之言,仅供参考:
1、以QTP为核心的框架
QTP是大家最常用的测试工具。而现在很多公司用的自动化测试框架都是以此为核心的。我在触自动化测试之初最先上手的也是QTP。
以QTP为核心的自动化测试框架优点在于:适用性好,很多人都已经会用或者至少说可以简单应用,脚本也简单易懂,大多数无任何代码基础的测试人员都可以加入脚本录制和调试。
我本人一直对QTP不太感冒的原因也就是它的缺点:对象库。这个词对自动化测试的tester们实在是个巨大的打击。我不去一一细数其罪行,但是,关键字的框架,灵活度实在不敢恭维。再加上QTP在对flex等的支持上实在是也让人欲哭无泪。如果说还有其他的,就是一旦应用于企业自动化测试框架,必然需要购买正版,价格的问题。。。
2、RFT
Rational Functional Tester,IBM的产品。我一直对ibm产品颇具好感,不知道是不是由于第一台笔记本就买了IBM的缘故。跑题了,回来说这个框架。
优点:其一是相比起QTP框架,灵活度要高。因为它最核心的find()。每个脚本里都会大量出现类似“new uiTestObject(find(atDescendant(".xxxx","xxxx",".xxxx","xxxx")))...”的语句,用来动态查找对象以解决对象识别问题。其二是对java的无缝连接,让很多人能更好更快的上手。
缺点:首先还是俗一点,说这个价格。高于QTP的价格让很多公司难以接受。第二,尽管ibm的团队非常强大,但是我们可以看到,由于种种原因,RFT的使用率比较低,这就导致网上关于该框架的疑难问题解决方案较少。第三,根据亲身经历,RFT的国内技术支持太弱,有问题很难请到,并且其技术支持人员测试技术能力都较差。
3、Ant+Selenium+Testng+Jenkins
这是我现在正在研究并使用的框架。(ps:jenkins这...还没用到。原来听说了hudson的强大,这个升级版估计会更有使用价值,未来研究)我这里说的selenium没有区分RC还是webdriver,两者各有千秋又互相补充,兼而用之即可。还是先说优点:第一:它开源不要钱!很多时候这是最关键的一点..当你在研究或推行一套框架的时候,价格是不得不考虑的因素。第二:灵活性,比RFT更加灵活,因为更加入了xpath(当然大型项目的脚本里xpath..慎用,尽量取id或稳定的属性)。加上配合IDE进行定位等,效果比较好。第三:相比rft,资料更全面,用该框架的也越来越多。据我了解,北京一些中型公司也在应用类似以selenium为核心的自动化测试框架。第四:就是开源性可以方便我们进行二次开发,例如提取对json和xml的处理来实现的数据驱动等。
缺点:第一:无论是RC还是Webdriver,对测试人员的编码水平有一定要求。同时ant,testng,hudson使用也都是小众,大多数人执行这个框架前需要有较长时间学习适应。第二:毕竟时间较短,不如QTP如此完善,但是我们可以期待其未来发展。也许3.0会带来一个巨大的变化。
4、Mcafe
我也不知道是不是这样拼这个框架,这是百度内部使用的一套自动化测试框架,或者叫平台。外面当然也买不到,我有幸见识了一次,包含了虚拟机的集成分配直至自动化测试执行,非常之惊艳。优点一大把缺点就是买都买不到。。。也给了我们一个方向,自主开发的自动化测试框架也许才是最适合你的。
欢迎大家发表意见。
由于项目需要,需要获取一组数据的的最新一条数据,表结构如下:
CREATE TABLE [dbo].[WUSU_SUOLITest_Table]( [ID] [bigint] IDENTITY(1,1) NOT NULL, [ReceiveTime] [datetime] NULL, [GroupID] [bigint] NOT NULL, [DataValue] [float] NULL, [SensorCode] [char](10) NOT NULL, ) |
在这个表上只有两种操作,插入和查询,没有删除和更新。而且同一种设备,随着id列的变大,ReceiveTime也随着变大。
每一个不同的SensorCode代表了一个设备,目前有50个设备,每30秒上报一次数据,ReceiveTime代表上报数据的时间,现在需要获取每一个设备最新一次的数据,
开始我使用如下的查询语句:
select * from WUSU_SUOLITest_Table where id in (select max(id) from WUSU_SUOLITest_Table group by SensorCode ) |
在数据量比较小时,是没有问题的,但数据量特别大时,这种方式,目前一天的数据就超过了14万,有很大的延时,即使在id上有聚集索引,SensorCode上使用了分区,依然没有多大作用。时间主要花费到了group by上。
实在想不多到什么好的而解决方法,就只能在此表上创建一个触发器,每次插入数据时就把最新的数据放在了一个临时表,又由于临时表最多只有50条数据,速度当然就很好了。
create TRIGGER [dbo].[UpdateWUSU_LastOriginalDataSUOLI] ON [dbo].[WUSU_SUOLITest_Table] AFTER INSERT AS BEGIN declare @SensorCode char(10), @DataValue float ,@ReceiveTime datetime ,@GroupID bigint select @SensorCode=SensorCode,@DataValue=DataValue,@ReceiveTime=ReceiveTime,@GroupID=GroupID from inserted update WUSU_LastOriginalData set DataValue=@DataValue,ReceiveTime=@ReceiveTime,GroupID=@GroupID where SensorCode=@SensorCode END |
当然这是为了获取各种设备最新的一条数据,如果要获取最新的两条数据,最多也就是100条记录,一次类推,只需要把上边的触发器修改一下就可以。
但还有没有更好的方式,在不修改表结构的情况下?目前还没有想到。
有人提供了使用关联子查询的方式,确实比group by好多了,但当数据量大时,十天的数据,依然会很慢,大约20多秒。
select * from WUSU_SUOLITest_Table as t where id = (select max(id) from WUSU_SUOLITest_Table where SensorCode=t.SensorCode ) |
一、简介:
RSA加密算法是最常用的非对称加密算法,CFCA在证书服务中离不了它。RSA是第一个比较完善的公开密钥算法,它既能用于加密,也能用于数字签名。这个算法经受住了多年深入的密码分析,虽然密码分析者既不能证明也不能否定RSA的安全性,但这恰恰说明该算法有一定的可信性,目前它已经成为最流行的公开密钥算法。
二、RSA的公钥、私钥的组成,以及加密、解密的公式可见于下表
三、使用方式:
① 假设A、B机器进行通信,已A机器为主;
② A首先需要用自己的私钥为发送请求数据签名,并将公钥一同发送给B;
③ B收到数据后,需要用A发送的公钥进行验证,已确保收到的数据是未经篡改的;
④ B验签通过后,处理逻辑,并把处理结果返回,返回数据需要用A发送的公钥进行加密(公钥加密后,只能用配对的私钥解密);
⑤ A收到B返回的数据,使用私钥解密,至此,一次数据交互完成。
四、代码示例:
1、第一步获取私钥,为签名做准备。
/** * 读取私钥 返回PrivateKey * @param path 包含私钥的证书路径 * @param password 私钥证书密码 * @return 返回私钥PrivateKey * @throws KeyStoreException * @throws NoSuchAlgorithmException * @throws CertificateException * @throws IOException * @throws UnrecoverableKeyException */ private static PrivateKey getPrivateKey(String path,String password) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, UnrecoverableKeyException { KeyStore ks = KeyStore.getInstance("PKCS12"); FileInputStream fis = new FileInputStream(path); char[] nPassword = null; if ((password == null) || password.trim().equals("")) { nPassword = null; } else { nPassword = password.toCharArray(); } ks.load(fis, nPassword); fis.close(); Enumeration<String> en = ks.aliases(); String keyAlias = null; if (en.hasMoreElements()) { keyAlias = (String) en.nextElement(); } return (PrivateKey) ks.getKey(keyAlias, nPassword); } |
2、签名示例:通过第一步得到的私钥,进行签名操作,具体请看以下代码:
/** * 私钥签名: 签名方法如下:BASE64(RSA(MD5(src),privatekey)),其中src为需要签名的字符串, privatekey是商户的CFCA证书私钥。 * @param plainText 待签名字符串 * @param path 签名私钥路径 * @param password 签名私钥密码 * @return 返回签名后的字符串 * @throws Exception */ public static String sign(String plainText,String path,String password) throws Exception { /* * MD5加密 */ MessageDigest md5 = MessageDigest.getInstance("MD5"); md5.update(plainText.getBytes("utf-8")); byte[] digestBytes = md5.digest(); /* * 用私钥进行签名 RSA * Cipher负责完成加密或解密工作,基于RSA */ Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); //ENCRYPT_MODE表示为加密模式 cipher.init(Cipher.ENCRYPT_MODE, getPrivateKey(path, password)); //加密 byte[] rsaBytes = cipher.doFinal(digestBytes); //Base64编码 return Base64.byteArrayToBase64(rsaBytes); |
3、B收到数据后,需要使用A提供的公钥信息进行验签,此处使用公钥的N、E进行验签
首先通过公钥N、E得到公钥PublicKey,如下:
/** * 根据公钥n、e生成公钥 * @param modulus 公钥n串 * @param publicExponent 公钥e串 * @return 返回公钥PublicKey * @throws Exception */ public static PublicKey getPublickKey(String modulus, String publicExponent) throws Exception { KeySpec publicKeySpec = new RSAPublicKeySpec( new BigInteger(modulus, 16), new BigInteger(publicExponent, 16)); KeyFactory factory = KeyFactory.getInstance("RSA"); PublicKey publicKey = factory.generatePublic(publicKeySpec); return publicKey; } |
得到公钥PublicKey后,再去验证签名,代码如下:
/** * 用公钥证书进行验签 * @param message 签名之前的原文 * @param cipherText 签名 * @param pubKeyn 公钥n串 * @param pubKeye 公钥e串 * @return boolean 验签成功为true,失败为false * @throws Exception */ public static boolean verify(String message, String cipherText,String pubKeyn, String pubKeye) throws Exception { Cipher c4 = Cipher.getInstance("RSA/ECB/PKCS1Padding"); // 根据密钥,对Cipher对象进行初始化,DECRYPT_MODE表示解密模式 c4.init(Cipher.DECRYPT_MODE, getPublickKey(pubKeyn,pubKeye)); // 解密 byte[] desDecTextBytes = c4.doFinal(Base64.base64ToByteArray(cipherText)); // 得到前置对原文进行的MD5 String md5Digest1 = Base64.byteArrayToBase64(desDecTextBytes); MessageDigest md5 = MessageDigest.getInstance("MD5"); md5.update(message.getBytes("utf-8")); byte[] digestBytes = md5.digest(); // 得到商户对原文进行的MD5 String md5Digest2 = Base64.byteArrayToBase64(digestBytes); // 验证签名 if (md5Digest1.equals(md5Digest2)) { return true; } else { return false; } } |
至此,签名验签已经完毕
4、提供一个从.cer文件读取公钥的方法:
/** * 读取公钥cer * @param path .cer文件的路径 如:c:/abc.cer * @return base64后的公钥串 * @throws IOException * @throws CertificateException */ public static String getPublicKey(String path) throws IOException, CertificateException{ InputStream inStream = new FileInputStream(path); ByteArrayOutputStream out = new ByteArrayOutputStream(); int ch; String res = ""; while ((ch = inStream.read()) != -1) { out.write(ch); } byte[] result = out.toByteArray(); res = Base64.byteArrayToBase64(result); return res; } |