💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、豆包、星火、月之暗面及文生图、文生视频 广告
到目前为止我的测试都聚焦于正常的行为上,这通常也被称为“正常路径”(happy path),它指的是一切工作正常、用户使用方式也最符合规范的那种场景。同时,把测试推到这些条件的边界处也是不错的实践,这可以检查操作出错时软件的表现。 无论何时,当我拿到一个集合(比如说此例中的生产商集合)时,我总想看看集合为空时会发生什么。 ``` describe('no producers', function() {  let noProducers;  beforeEach(function() {   const data = {    name: "No proudcers",    producers: [],    demand: 30,    price: 20   };   noProducers = new Province(data);  });  it('shortfall', function() {   expect(noProducers.shortfall).equal(30);  });  it('profit', function() {   expect(noProducers.profit).equal(0);  }); ``` 如果拿到的是数值类型,`0`会是不错的边界条件: ##### describe('province'... ``` it('zero demand', function() {  asia.demand = 0;   expect(asia.shortfall).equal(-25);   expect(asia.profit).equal(0);  }); ``` 负值同样值得一试: ##### describe('province'... ``` it('negative demand', function() {  asia.demand = -1;  expect(asia.shortfall).equal(-26);  expect(asia.profit).equal(-10); }); ``` 测试到这里,我不禁有一个想法:对于这个业务领域来讲,提供一个负的需求值,并算出一个负的利润值意义何在?最小的需求量不应该是0吗?或许,设值方法需要对负值有些不同的行为,比如抛出错误,或总是将值设置为0。这些问题都很好,编写这样的测试能帮助我思考代码本应如何应对边界场景。 设值函数接收的字符串是从UI上的字段读来的,它已经被限制为只能填入数字,但仍然有可能是空字符串,因此同样需要测试来保证代码对空字符串的处理方式符合我的期望。 > ![](https://box.kancloud.cn/9cf522e33e311401bf0d755d003df8ea_19x20.jpeg) 考虑可能出错的边界条件,把测试火力集中在那儿。 ##### describe('province'... ``` it('empty string demand', function() {  asia.demand = "";  expect(asia.shortfall).NaN;  expect(asia.profit).NaN; }); ``` 可以看到,我在这里扮演“程序公敌”的角色。我积极思考如何破坏代码。我发现这种思维能够提高生产力,并且很有趣——它纵容了我内心中比较促狭的那一部分。 这个测试结果很有意思: ``` describe('string for producers', function() {  it('', function() {   const data = {    name: "String producers",    producers: "",    demand: 30,    price: 20   };   const prov = new Province(data);   expect(prov.shortfall).equal(0);  }); ``` 它并不是抛出一个简单的错误说缺额的值不为0。控制台的报错输出实际如下: ``` '''''''''!  9 passing (74ms)  1 failing  1) string for producers :    TypeError: doc.producers.forEach is not a function     at new Province (src/main.js:22:19)     at Context.<anonymous> (src/tester.js:86:18) ``` Mocha把这也当作测试失败(failure),但多数测试框架会把它当作一个错误(error),并与正常的测试失败区分开。“失败”指的是在验证阶段中,实际值与验证语句提供的期望值不相等;而这里的“错误”则是另一码事,它是在更早的阶段前抛出的异常(这里是在配置阶段)。它更像代码的作者没有预料到的一种异常场景,因此我们不幸地得到了每个JavaScript程序员都很熟悉的错误(“`...is not a function`”)。 那么代码应该如何处理这种场景呢?一种思路是,对错误进行处理并给出更好的出错响应,比如说抛出更有意义的错误信息,或是直接将`producers`字段设置为一个空数组(最好还能再记录一行日志信息)。但维持现状不做处理也说得通,也许该输入对象是由可信的数据源提供的,比如同个代码库的另一部分。在同一代码库的不同模块之间加入太多的检查往往会导致重复的验证代码,它带来的好处通常不抵害处,特别是你添加的验证可能在其他地方早已做过。但如果该输入对象是由一个外部服务所提供,比如一个返回JSON数据的请求,那么校验和测试就显得必要了。不论如何,为边界条件添加测试总能引发这样的思考。 如果这样的测试是在重构前写出的,那么我很可能还会删掉它。重构应该保证可观测的行为不发生改变,而类似的错误已经超越可观测的范畴。删掉这条测试,我就不用担心重构过程改变了代码对这个边界条件的处理方式。 > 如果这个错误会导致脏数据在应用中到处传递,或是产生一些很难调试的失败,我可能会用引入断言(302)手法,使代码不满足预设条件时快速失败。我不会为这样的失败断言添加测试,它们本身就是一种测试的形式。 什么时候应该停下来?我相信这样的话你已经听过很多次:“任何测试都不能证明一个程序没有bug。”确实如此,但这并不影响“测试可以提高编程速度”。我曾经见过好几种测试规则建议,其目的都是保证你能够测试所有情况的一切组合。这些东西值得一看,但是别让它们影响你。当测试数量达到一定程度之后,继续增加测试带来的边际效用会递减;如果试图编写太多测试,你也可能因为工作量太大而气馁,最后什么都写不成。你应该把测试集中在可能出错的地方。观察代码,看哪儿变得复杂;观察函数,思考哪些地方可能出错。是的,你的测试不可能找出所有bug,但一旦进行重构,你可以更好地理解整个程序,从而找到更多bug。虽然在开始重构之前我会确保有一个测试套件存在,但前进途中我总会加入更多测试。 > ![](https://box.kancloud.cn/9cf522e33e311401bf0d755d003df8ea_19x20.jpeg) 不要因为测试无法捕捉所有的bug就不写测试,因为测试的确可以捕捉到大多数bug。