2.3 SET的招聘
优秀的SET在各个方面都很出色:是一个编码能力很强的程序员,可以写功能代码;也是一个能力很强的测试者,可以测试任何产品,有能力管理他们自己的工作和工具。优秀的SET不仅可以看到树木而且可以看到整个森林,在看到小段函数原型或者API的时候,就能想到各种使用这段代码的方法以及怎样破坏这段代码。
在Google,所有的代码都存放在同一个代码库中,这意味着任何人可以在任何时间使用里面的任何代码,所以代码本身一定要可靠且稳定。SET不仅仅要发现功能开发人员遗漏的代码缺陷,而且还要去关心其他的工程师是如何使用这些代码模块,并确保这种使用方式是没有问题的,甚至还会去关心这些代码未来适用的功能。由于Google前进变化的速度非常快,所以代码一定要保持干净、连贯一致。在最初的代码作者都不再关心这些代码的时候,仍要保证这些代码可以正常工作。
在面试的过程中我们如何考察这些技能和心态呢?这可不是一件容易的事但幸运的是,我们已经找到了上百个满足条件的工程师。我们期望有这样的混合型人才:对测试有强烈兴趣和天资的开发人员。一个通用且有效的招募优秀SET的方法是,给候选人和其他开发角色一样的编程问题,并考察他们在处理质量与测试方面的方法。在面试过程中,SET有两次回答错误的机会。
常常通过一些简单的问题就可以识别出哪些是优秀的SET。在一些棘手的编码问题或功能的正确性上浪费时间,不如考核他们是如何看待编码和质量的。在SET的一轮面试中会有一个SWE或SET来考察算法方面的问题。对于候选者,最好去考察如何思索问题的解决方案,而不是解决方案本身的实现上体现得多么高雅。
注意
SET的面试重点在考察候选人如何思索问题的解决方案,而不是解决方案本身的实现上有多么高雅。
这里有一个例子。假如这是你第一天上班,你被要求去实现一个函数acount(void* s),返回一个字符串中大写字母A出现的次数。
如果候选人上来就直接开始写代码,这无非在传递一个强烈的信息:只有一件事情需要去做而我正在做这个事情,这个事情就是写代码。SET不会遵循这样的世界观。我们希望先把问题搞清楚。
这个函数是用来做什么的?我们为什么要构建它?这个函数的原型看起来正确吗?我们期望候选人可以关心函数的正确性以及如何验证期望的行为。一个问题值得更多的关注!候选人如果没头没脑地就跳进来编码,试图解决问题,在对得测试问题上他同样会没头没脑。如果我们提出一个问题是给模块增加测试场景,我们不希望候选人上来就直接开始罗列所有可能的测试用例,直到我们强迫他停下来。其实我们只是希望他先执行最佳的测试用例。
SET的时间是有限的。我们希望候选人能够回过头来寻找最有效的解决问题的方法,为先前的函数定义可以做一些改进。优秀的SET在面对拙劣的API定义的情况下,在测试的过程中也可以把这个API定义变得更漂亮一些。
普通的候选人会花几分钟通过提问题和陈述的方式来理解需求文档,例如以下几点。
传入的字符串编码是什么:ASCII、UTF-8或其他的编码方式?
函数名字比较槽糕,应该是驼峰式(CamelCased)的?需要更多说明描述,还是这里应该遵循其他的什么命名规范?
返回值类型是什么(或许面试官忘记了,所以我会增加一个int类型的返回值在函数原型之前)?
void是危险的。我们应该考虑更合适的类型,如char。在一些编译时刻类型检查中可以为我们提供一些帮助。
如果只有一个A的情况,计数结果是多少?它对小写字母a也计数吗?
在标准库中不是已经有这样的函数了吗(为了面试的目的,假装你是第一个实现这个函数功能的人)?
更好的候选人则会考虑的更多一些。
考虑一下扩展性:或许返回值的类型应该是一个64位的整形,因为Google经常涉及海量数据。
考虑一下复用性:为什么这个函数是针对大写字母A进行计数的?一个好的办法是参数化,使得任意的字符都可以被计数,而不是使用不同的函数来实现。
考虑一下安全性:这些指针都是来自于可信任的地址吗?
最佳的候选人会这样考虑。
考虑扩展性。
这个函数会在Shared data(译注:数据分区,是数据库存储分割(partition)的一种方式。水平分割是一个数据库的设计准则,数据以记录行的方式存储在不同的物理位置,而不是通过不同列的方式存储。(database_architecture))上被作为MapReduce(注:MapReduce是分布式计算编程模型)的一部分运行吗?或许这才是调用这个函数最有用的形式。在这个场景需要考虑一些什么问题吗?针对整个互联网的所有文档运行这个函数,该如何考虑性能和正确性?
如果这个子程序被每一个Google查询所调用,而且由于外部的封装层面已经对参数做了验证,传递的指针是安全的,或许减少一个空指针的检查会每天节省上亿次的CPU调用周期,并缩短用户的响应时间。最少要理解全部参数验证带来的潜在影响。
考虑基于常量的优化。
我们可以假设输入的数据是已经排好顺序的吗?如果是那样,我们或许可以在找到第一个大写字母B之后就快速退出。输入的数据是什么结构?多数情况下都是A吗?多数是字符的混合,还是只包含字母A和空格?如果那样,在我们比较指令的地方或许可以做些优化。当在处理大数据,甚至小数据的时候,在代码执行的时候对于真实的计算延迟也会有比较显著的亚线性变化。
考虑安全性。
在许多系统上,如果这是一段对于安全敏感的代码,可以考虑更多的非空的指针做测试。在某些系统上,1是一个非法的指针。
增加一个字符长度的参数,用以保证代码不会运行到指定字符串之外的部分。检查字符串长度,这个参数的值是否正常。那些不是以null结尾的字符串是黑客们的最爱。
如果指针指向的数据能被其他的线程修改,这里就有潜在的线程安全问题。
我们是否应该使用try/catch来捕获异常的发生?或者如果未能如预期那样正常的调用代码,我们或许应该返回错误代码给调用者。如果有错误代码的话,这些代码经过良好的定义并有文档吗?这意味着候选人在思考大型代码库和运行时刻的上下文环境方面的问题,这样的思索可以避免错误代码的重复和遗漏。
基本上,最佳候选人会有针对性地提出一些新观点。如果这些观点比较明智的话,它们都是值得考虑的。
注意
一个优秀SET候选人不应该被告之要去测试代码,这应该是SET自然要考虑的地方。
所有这些面试问题,无论是针对问题本身还是针对输入参数都有一个关键之处,那就是任何通过入门级别编程课程的工程师都可以针对这个问题写出简单的功能代码。优秀的候选人和普通的候选人在提问和思路上的表现会迥然不同。我们要确保候选人能够感觉足够舒适地去提出问题,如果没有问题,我们就引导他们去提问,确保他们不会因为当前是在面试就直接去写代码。Google的人应该质疑几乎所有事情,但仍然会把问题解决掉。
在这里,如果把这个面试问题的所有正确实现与常见错误都罗列一遍,肯定会招人讨厌,毕竟这不是一本关于编程或面试的书。但为了讨论的需要,让我们使用一个简单且常见的代码实现方式来做讨论(译注:在下面代码中,第6行代码中的‘a’应该是大写字母‘A’,原书有误)。注意,候选人一般都会选择使用自己喜欢的编程语言,如Java、Python等,但这经常会引起一些问题,例如垃圾收集、类型安全、编译和运行时刻的不同关注点等。我们同时要确保候选人可以正确理解这些问题。
int64 Acount(const char* s) {
if (!s)
return 0;
int64 count = 0;
while (*s++) {
if (*s == ‘a’)
count++;
}
return count;
}
候选人应该可以走查他们的代码,指出程序中出现的指针或计数器的值在测试数据输入之后在代码运行时刻是如何变化的。
一般来说,普通的SET候选人会做到以下这些。
在通过编写代码解决问题的过程中很少遇到问题。在编码时,函数重写没有麻烦,很少出现基本语法错误,也不会混淆不同语言的语法和关键词。
在理解指针方面没有明显错误,或者没有分配不必要的内存。
在代码开始的地方做一些输入验证,避免由于取值到空指针等引起比较麻烦的程序崩溃。若在被问到为何不做参数验证的时候,则可以很好地解释为什么要这样做。
理解运行时刻效率或程序代码的大O(译注:大O表示法描述了函数在运行时刻需要消耗的时间,参考See http://en.wikipedia.org/wiki/Big_O_notation)。在效率上,任何非线性的运行时间虽然说明程序可用,但都有可提升的空间。
在被指出代码中有小的问题时,可以修正它们。
写的代码干净易读。如果使用了位操作或把所有的代码都写在一行,则绝对不是一个好现象。代码即使在功能上可以正常工作,读起来也会令人呕吐。
在输入为一个A或null的时候,走查代码确保能正常工作。
更优秀的候选人会做的更多一些。
考虑使用64位整型int64作为计数器变量和返回值的类型,为了以后的兼容性和避免用户使用非常长的字符串而导致溢出。
针对分布式的计数计算而准备一些代码。一些对MapReduce不熟悉的候选人,会针对大字符串并行计算使用自己的简单变量来提高响应速度。
在代码注释中对条件假设和常量做解释说明。
在有很多不同的数据输入时可以走查代码,修复所发现的错误。不懂得如何发现和修复缺陷的SET候选人不是合格的候选人。
在被要求去做功能测试之前就去做相应的测试。测试不应是被要求了才去做的事情。
在被要求停止之前,不停地尝试优化解决方案。在经过区区几分钟的编码和简单测试之后,没人敢说他的代码就是完美的。程序的稳定性和韧性比功能正确要重要的多。
现在,我们想看候选人是否可以测试他们自己写的代码。令人费解或复杂棘手的测试代码是世界上最差的代码,但这也比没有测试代码强。在Google,如果测试运行失败,需要清楚地知道测试代码在做什么。否则,这个测试就应该被禁止掉,或是被标记为怪异的测试,或是忽略这个测试的运行失败。如果这样的事情发生了,这是编写出坏代码的SWE的责任,或是代码审查时给予通过投票的SET/SWE的失误。
SET应该可以用黑盒测试方法做测试,假设其他人已经实现了功能;也可以用白盒测试的方式,考虑其内部的实现可以知道哪些用例是无关的。
通常情况下,普通的候选人会这样做。
他们会比较有条理地或体系化地提供特定的字符串(如不同的字符串大小)而不是随机的字符串。
专注于产生有意义的测试数据。考虑如何去运行大型测试和使用真实环境的数据做测试。
更优秀的候选人会这样做的更多一些。
在并发线程中调用这个函数,去查看在串扰(cross talk)、死锁和内存泄露方面是否存在问题。
构建长时间持续运行的测试场景。例如在一个while(true)循环中调用函数,并确保他们在不间断地长时间运行过程中保持功能正常。
在构建测试用例、测试数据的产生方法、验证和执行上保持浓厚的兴趣。
优秀候选人的例子
by Jason Arbon
最近有一个候选人(后来被证明他在实际工作上的表现也确实令人吃惊)在被问到如何针对这个返回值为64位整形的API做边界测试时,他很快地意识到由于时间和空间的限制,不可能使用物理的方法做测试。但为了做完这个题目和出于好奇心,在思考这个级别的扩展性时尝试使用非常大量的数据来做这个测试,并提出使用Google的网页索引作为输入数据来源。
他是如何验证这个结果的呢?他建议使用一个并行来实现,从而保证产生两份相同的结果。他也考虑到使用统计学上抽样的方法:大写字母A在网页上出现的期望频率。由于我们知道网页索引后的数量,计算后的数字应该比较接近。这正是Google思考测试的方式。即便我们不会真的构建这样庞大的测试,思考这些解决方案一般也会对正常规模的测试工作提供有意义或有效的借鉴。
在面试中需要另外考虑的是文化上是否匹配。SET候选人在面试过程中是否在技术上有好奇心?当面对一些新想法的时候,候选人是否能够把它融入到解决方案里呢?又是如何处理有歧义的地方的?是否熟悉质量方面的理论学术方法?是否理解质量度量或其他领域的自动化?例如土木工程或航空工程方面的自动化。当你发现在实现中存在缺陷时,是否心存戒备,思路又是否足够开阔?候选人不必具备所有的这些特质,但仍是越多越好。最后还要考虑在日常的工作中,我们是否愿意和这个人一起工作。
需要着重强调的一点是,如果某人在应聘SET岗位的时候没有具备足够强的编码能力,这并不意味着此人不是一个合格的TE。我们已雇佣到的一些优秀的TE,之前都是来应聘SET岗位的。
一个有趣的现象值得我们注意,Google在SET招聘过程中经常会与优秀的候选人失之交臂,原因是这些人最后成为非测试类的SWE或对测试过度专注的TE。我们希望SET的候选人具有多样性,他们可能会在以后工作上成为同事。SET是一个真正的混合体,但有些时候这也会导致一些令人不悦的面试得分。我们想确保的一点是,这些低分是由于我们的面试官在使用严格的SET考核标准而导致。
正如Patrick Copeland在序言中说的那样,关于SET的招聘目前还有一些不同的观点。如果SET是一个优秀的编程者,他就应该只去做功能开发的工作吗?SWE也是很难雇佣到的。如果他们擅长做测试,就应该只是专注于解决纯粹的测试问题么?事实总是存在于两者之间。
招聘优秀的SET是一件很麻烦的事情,但这是值得的。一个明星级的SET能够对一个团队产生巨大的影响。