编写单元测试时出现令人不快的错误

前几天,我将做一个内部报告,在其中我将告诉我们的开发人员有关编写单元测试时可能发生的令人不愉快的错误。 从我的角度来看,最不愉快的错误是测试通过时,但同时它们做得如此错误,以至于最好不通过。 我决定与所有人分享此类错误的示例。 当然,这方面还有其他要说的。 示例是针对Node.JS和Mocha编写的,但是通常这些错误对于其他任何生态系统都是正确的。

为了使问题更有趣,其中一些以问题代码和扰流器的形式被框起来,将其打开,您将看到问题出在哪里。 因此,我建议您首先查看代码,在其中找到错误,然后打开扰流板。 没有指出解决问题的方法-我建议自己考虑一下。 只是因为我很懒。 列表的顺序没有什么深远的意义,它只是一个顺序,在此顺序中,我回顾了使我们流泪的各种实际问题。 当然,许多事情对您来说似乎显而易见-但即使是经验丰富的开发人员也可能会意外地编写此类代码。


所以走吧

0.缺乏测试


奇怪的是,许多人仍然认为编写测试会降低开发速度。 当然,很明显,必须花费更多的时间编写测试和编写可以测试的代码。 但是经过调试和回归之后,您必须花费更多的时间...

1.缺乏运行测试


如果您有不运行的测试,或者不时运行,则就像没有测试一样。 更糟糕的是-您的测试代码已经过时,并且对安全性有错误的认识。 将代码推送到分支时,测试至少应在CI进程中运行。 更好-在推动之前就在本地。 然后,开发人员将不必在几天之内返回构建,事实证明,构建并没有通过。

2.缺乏覆盖


如果您仍然不知道测试的覆盖范围,那么该是现在就开始阅读。 至少维基百科 。 否则,您的测试很有可能会检查您认为检查的代码强度的10%。 迟早您肯定会踩到它。 当然,即使代码的100%覆盖率也不能以任何方式保证其完全正确性-但这比缺少覆盖率要好得多,因为它将向您显示更多潜在的错误。 难怪最新版本的Node.JS甚至具有内置的读取工具。 总的来说,报道的主题是非常深入和全面的,但是我不会太深入-我想说很多。

3。



const {assert} = require('chai'); const Promise = require('bluebird'); const sinon = require('sinon'); class MightyLibrary { static someLongFunction() { return Promise.resolve(1); // just imagine a really complex and long function here } } async function doItQuickOrFail() { let res; try { res = await MightyLibrary.someLongFunction().timeout(1000); } catch (err) { if (err instanceof Promise.TimeoutError) { return false; } throw err; } return res; } describe('using Timeouts', ()=>{ it('should return false if waited too much', async ()=>{ // stub function to emulate looong work sinon.stub(MightyLibrary, 'someLongFunction').callsFake(()=>Promise.delay(10000).then(()=>true)); const res = await doItQuickOrFail(); assert.equal(res, false); }); }); 


怎么了
单元测试中的超时。

在这里,他们想检查长时间操作的设置超时是否确实有效。 总的来说,这毫无意义-您不应该检查标准库-但是此代码会导致另一个问题-将测试的执行时间增加一秒钟。 似乎并没有那么多...但是将第二秒乘以类似测试的数量,开发人员的数量,每天的启动数量...并且您会了解到,由于这种超时,您每周可能会失去很多工作时间,即使不是每天也是如此。



4。



 const fs = require('fs'); const testData = JSON.parse(fs.readFileSync('./testData.json', 'utf8')); describe('some block', ()=>{ it('should do something', ()=>{ someTest(testData); }) }) 


怎么了
在测试块之外加载测试数据。

乍一看,在哪里读取测试数据似乎无关紧要-在describe中,它阻塞或在模块本身中。 在第二个。 但是,假设您有数百个测试,其中许多测试使用大量数据。 如果将它们加载到测试之外,将导致以下事实:所有测试数据将一直保留在内存中,直到测试执行结束为​​止;随着时间的推移,启动将消耗越来越多的RAM-直到事实证明测试不再运行标准工作机。



5,



 const {assert} = require('chai'); const sinon = require('sinon'); class Dog { // eslint-disable-next-line class-methods-use-this say() { return 'Wow'; } } describe('stubsEverywhere', ()=>{ before(()=>{ sinon.stub(Dog.prototype, 'say').callsFake(()=>{ return 'meow'; }); }); it('should say meow', ()=>{ const dog = new Dog(); assert.equal(dog.say(), 'meow', 'dog should say "meow!"'); }); }); 


怎么了
该代码实际上被存根替换了。

当然,您立即看到了这个荒谬的错误。 在真实的代码中,这当然不是很明显-但是我看到的代码非常残存,以至于我根本没有进行任何测试。



6。



 const sinon = require('sinon'); const {assert} = require('chai'); class Widget { fetch() {} loadData() { this.fetch(); } } if (!sinon.sandbox || !sinon.sandbox.stub) { sinon.sandbox = sinon.createSandbox(); } describe('My widget', () => { it('is awesome', () => { const widget = new Widget(); widget.fetch = sinon.sandbox.stub().returns({ one: 1, two: 2 }); widget.loadData(); assert.isTrue(widget.fetch.called); }); }); 


怎么了
测试之间的依赖关系。

乍一看,很明显他们忘了在这里写

  afterEach(() => { sinon.sandbox.restore(); }); 


但是问题不仅在于此,所有测试都使用相同的沙箱。 而且很容易混淆测试执行环境,使它们开始相互依赖。 之后,将仅以特定顺序开始执行测试,并且通常不清楚要测试什么。

幸运的是,在某些时候,sinon.sandbox被声明为已弃用并已删除,因此您只能在旧项目上遇到这样的问题-但是还有很多其他方法会混淆测试执行环境,以至于以后进行调查时会很痛苦。哪个代码犯有不正确的行为。 顺便说一句,最近在中心上发布了有关“ Ice Factory”等模板的帖子-这不是灵丹妙药,但有时在这种情况下会有所帮助。




7.测试文件中的大量测试数据



我经常看到直接在测试中放置着巨大的JSON文件,甚至XML。 我认为很明显为什么这样做不值得-观看,编辑变得很痛苦,任何IDE都不会为此感到感谢。 如果测试数据较大,请将其从测试文件中删除。

8。


 const {assert} = require('chai'); const crypto = require('crypto'); describe('extraTests', ()=>{ it('should generate unique bytes', ()=>{ const arr = []; for (let i = 0; i < 1000; i++) { const value = crypto.randomBytes(256); arr.push(value); } const unique = arr.filter((el, index)=>arr.indexOf(el) === index); assert.equal(arr.length, unique.length, 'Data is not random enough!'); }); }); 


怎么了
额外的测试。

在这种情况下,开发人员非常担心自己的唯一标识符是否唯一,因此他为此签了一张支票。 通常,这是一个可以理解的愿望-但是最好阅读文档或多次运行这样的测试,而不将其添加到项目中。 在每个版本中运行它都是没有意义的。

好吧,测试中与随机值的联系本身就是通过从头开始进行不稳定的测试来射击自己的好方法。



9.缺乏魔力


使用实时数据库和100%服务运行测试,并在它们上运行测试要容易得多。
但是迟早会恢复成功-数据删除测试将在产品基础上执行,由于合作伙伴服务中断而开始下降,或者您的CI根本没有运行它们的基础。 通常,该项目非常全面,但通常来说-如果您可以模拟外部服务,则最好这样做。

11。


 const {assert} = require('chai'); class CustomError extends Error { } function mytestFunction() { throw new CustomError('important message'); } describe('badCompare', ()=>{ it('should throw only my custom errors', ()=>{ let errorHappened = false; try { mytestFunction(); } catch (err) { errorHappened = true; assert.isTrue(err instanceof CustomError); } assert.isTrue(errorHappened); }); }); 


怎么了
复杂的错误调试。

一切都还不错,但是有一个问题-如果测试突然崩溃,您将看到表格错误

1) badCompare
should throw only my custom errors:

AssertionError: expected false to be true
+ expected - actual

-false
+true

at Context.it (test/011_badCompare/test.js:23:14)


此外,要了解实际发生了哪种错误-您必须重写测试。 因此,在发生意外错误的情况下-尝试让测试者谈论它,而不仅仅是它发生的事实。



12


 const {assert} = require('chai'); function someVeryBigFunc1() { return 1; // imagine a tonn of code here } function someVeryBigFunc2() { return 2; // imagine a tonn of code here } describe('all Before Tests', ()=>{ let res1; let res2; before(async ()=>{ res1 = await someVeryBigFunc1(); res2 = await someVeryBigFunc2(); }); it('should return 1', ()=>{ assert.equal(res1, 1); }); it('should return 2', ()=>{ assert.equal(res2, 2); }); }); 


怎么了
一切都在before块中。

似乎一种很酷的方法是在“之前”块中执行所有操作,因此仅在“它”内部保留检查。
不完全是
因为在这种情况下,混乱不堪,您既无法了解实际执行测试的时间,也无法了解崩溃的原因,也无法了解与一个测试有关的内容以及与另一个测试有关的内容。
因此,测试的所有工作(标准初始化除外)都应在测试内部进行。



13


 const {assert} = require('chai'); const moment = require('moment'); function someDateBasedFunction(date) { if (moment().isAfter(date)) { return 0; } return 1; } describe('useFutureDate', ()=>{ it('should return 0 for passed date', ()=>{ const pastDate = moment('2010-01-01'); assert.equal(someDateBasedFunction(pastDate), 0); }); it('should return 1 for future date', ()=>{ const itWillAlwaysBeInFuture = moment('2030-01-01'); assert.equal(someDateBasedFunction(itWillAlwaysBeInFuture), 1); }); }); 


怎么了
系上日期。

这似乎也是一个明显的错误-但在已经相信明天永远不会到来的疲倦的开发人员中,它也会定期出现。 昨天进展顺利的构建今天突然下降。

请记住,任何日期迟早都会出现-因此,要么将时间仿真与诸如`sinon.fakeTimers`之类的东西一起使用,要么至少将诸如2050年这样的远程日期设置为-让您的后代受到伤害...



14。



 describe('dynamicRequires', ()=>{ it('should return english locale', ()=>{ // HACK : // Some people mutate locale in tests to chinese so I will require moment here // eslint-disable-next-line global-require const moment = require('moment'); const someDate = moment('2010-01-01').format('MMMM'); assert.equal(someDate, 'January'); }); }); 


怎么了
动态加载模块。

如果您拥有Eslint,那么您可能已经禁止了动态依赖关系。 还是不行
我经常看到开发人员试图直接在测试内部加载库或各种模块。 但是,他们通常知道'require'是如何工作的-但他们更喜欢这样的错觉:应该给他们一个干净的模块,到目前为止还没有人混淆。
这种假设很危险,因为在测试过程中附加模块的加载速度较慢,并再次导致行为更加不确定。



15


 function someComplexFunc() { // Imagine a piece of really strange code here return 1; } describe('cryptic', ()=>{ it('success', ()=>{ const result = someComplexFunc(); assert.equal(result, 1); }); it('should not fail', ()=>{ const result = someComplexFunc(); assert.equal(result, 1); }); it('is right', ()=>{ const result = someComplexFunc(); assert.equal(result, 1); }); it('makes no difference for solar system', ()=>{ const result = someComplexFunc(); assert.equal(result, 1); }); }); 


怎么了
难以理解的测试名称。

您一定对明显的事物感到厌倦,对吗? 但是您仍然必须要说一遍,因为许多人不会为测试编写可理解的名称-结果,只有经过大量研究,才能了解特定测试的作用。



16。


 const {assert} = require('chai'); const Promise = require('bluebird'); function someTomeoutingFunction() { throw new Promise.TimeoutError(); } describe('no Error check', ()=>{ it('should throw error', async ()=>{ let timedOut = false; try { await someTomeoutingFunction(); } catch (err) { timedOut = true; } assert.equal(timedOut, true); }); }); 


怎么了
缺少对引发的错误的验证。

通常,您需要检查在某些情况下该函数会引发错误。 但是您始终需要检查这些机器人是否是我们正在寻找的机器人-因为它可能突然发现在另一个地方由于其他原因抛出了另一个错误...



17。



 function someBadFunc() { throw new Error('I am just wrong!'); } describe.skip('skipped test', ()=>{ it('should be fine', ()=>{ someBadFunc(); }); }); 


怎么了
禁用的测试。

当然,当您已经用手多次测试代码,需要紧急滚动代码并且由于某种原因测试无法正常工作时,总会出现这种情况。 例如,由于另一个测试的复杂性不明显,这是我之前写的。 并且测试已关闭。 这是正常的。 不正常-不要立即设置重新启动测试的任务。 如果不这样做,那么禁用测试的数量将成倍增加,并且它们的代码将不断变得过时。 直到唯一的选择–摆出怜悯并将所有这些测试扔掉,因为重新编写它们比理解错误要快。



这样的选择出来了。 所有这些测试都很好地通过了测试,但是它们被设计破坏了。 将您的选项添加到注释或我为收集此类错误而创建的存储库中

Source: https://habr.com/ru/post/zh-CN432092/


All Articles