0


【The Art of Unit Testing 3_自学笔记04】第二章:编写第一个单元测试(下)

1023程序员节指定活动图片

文章目录

(接上篇)

2.6 试用 beforeEach() 消除冗余代码 Trying the beforeEach() route

利用

beforeEach()

可将上面的重复代码提取出来(L2-3、L5-9):

describe('PasswordVerifier',()=>{let verifier;beforeEach(()=> verifier =newPasswordVerifier1());// 创建一个 verifier 实例供每个测试用例引用describe('with a failing rule',()=>{let fakeRule, errors;beforeEach(()=>{fakeRule=input=>({ passed:false, reason:'fake reason'});// 在当前 describe 方法内创建一个伪规则备用
      verifier.addRule(fakeRule);});it('has an error message based on the rule.reason',()=>{
      errors = verifier.verify('any value');expect(errors[0]).toContain('fake reason');});it('has exactly one error',()=>{const errors = verifier.verify('any value');expect(errors.length).toBe(1);});});});

上述重构的问题:

  1. errors 数组未在 beforeEach() 方法中重置,后续会带来问题;
  2. Jest 默认以并行方式运行测试,可能导致 verifier 的值被并行运行的其他测试重写,从而破坏当前测试状态。

2.6.1 beforeEach() 的滚屏疲劳效应 beforeEach() and scroll fatigue

无论是查看

verifier

的声明还是对其添加的校验规则,

it()

方法都无法直接提供相关信息,只能上翻到

beforeEach()

进行查看,然后再切回

it()

。这将导致 滚屏疲劳(scroll fatigue 效应。

beforeEach()

的设计或许对制作测试报表很有用,但对于需要不断查找某段代码出处的人而言无疑是痛苦的。

滥用

beforeEach()

,可能陷入更严重、更不易重构的代码冗余。

beforeEach()

往往会沦为测试文件的“垃圾桶”,里面充斥着各种初始化逻辑——测试需要的东西、干扰其他测试的东西、甚至是没人使用的东西(未及时清理)。

比如,将

AAA

模式中的准备(

Arrange

)和执行(

Act

)丢给

beforeEach()

,看似消除了冗余,其实导致了更严重的代码重复:

describe('PasswordVerifier',()=>{let verifier;beforeEach(()=> verifier =newPasswordVerifier1());describe('with a failing rule',()=>{let fakeRule, errors;beforeEach(()=>{fakeRule=input=>({ passed:false, reason:'fake reason'});
      verifier.addRule(fakeRule);
      errors = verifier.verify('any value');});it('has an error message based on the rule.reason',()=>{expect(errors[0]).toContain('fake reason');});it('has exactly one error',()=>{expect(errors.length).toBe(1);});});});

此时再加几个测试,滥用

beforeEach()

的弊端就显现出来了:

describe('PasswordVerifier',()=>{let verifier;beforeEach(()=> verifier =newPasswordVerifier1());describe('with a failing rule',()=>{let fakeRule, errors;beforeEach(()=>{fakeRule=input=>({ passed:false, reason:'fake reason'});
      verifier.addRule(fakeRule);
      errors = verifier.verify('any value');});it('has an error message based on the rule.reason',()=>{expect(errors[0]).toContain('fake reason');});it('has exactly one error',()=>{expect(errors.length).toBe(1);});});describe('with a passing rule',()=>{let fakeRule, errors;beforeEach(()=>{fakeRule=input=>({ passed:true, reason:''});
      verifier.addRule(fakeRule);
      errors = verifier.verify('any value');});it('has no errors',()=>{expect(errors.length).toBe(0);});});describe('with a failing and a passing rule',()=>{let fakeRulePass, fakeRuleFail, errors;beforeEach(()=>{fakeRulePass=input=>({ passed:true, reason:'fake success'});fakeRuleFail=input=>({ passed:false, reason:'fake reason'});
      verifier.addRule(fakeRulePass);
      verifier.addRule(fakeRuleFail);
      errors = verifier.verify('any value');});it('has one error',()=>{expect(errors.length).toBe(1);});it('error text belongs to failed rule',()=>{expect(errors[0]).toContain('fake reason');});});});

此时不仅冗余严重,滚动疲劳效应也更显著了。因此

beforeEach()

在作者这里很不受待见。

2.7 尝试工厂方法消除冗余代码 Trying the factory method route

此时可以尝试工厂方法,将校验工具的实例化和校验规则的配置都放进工厂方法里:

constmakeVerifier=()=>newPasswordVerifier1();constpassingRule=(input)=>({ passed:true, reason:''});constmakeVerifierWithPassingRule=()=>{const verifier =makeVerifier();
  verifier.addRule(passingRule);return verifier;};constmakeVerifierWithFailedRule=(reason)=>{const verifier =makeVerifier();constfakeRule=input=>({ passed:false, reason: reason });
  verifier.addRule(fakeRule);return verifier;};

然后在测试用例中确保每个测试都按照 工具实例化校验输入执行断言 的结构进行重构,将得到更加紧凑的单元测试,同时滚屏疲劳的问题也得到了良好控制:

describe('PasswordVerifier',()=>{describe('with a failing rule',()=>{it('has an error message based on the rule.reason',()=>{const verifier =makeVerifierWithFailedRule('fake reason');const errors = verifier.verify('any input');expect(errors[0]).toContain('fake reason');});it('has exactly one error',()=>{const verifier =makeVerifierWithFailedRule('fake reason');const errors = verifier.verify('any input');expect(errors.length).toBe(1);});});describe('with a passing rule',()=>{it('has no errors',()=>{const verifier =makeVerifierWithPassingRule();const errors = verifier.verify('any input');expect(errors.length).toBe(0);});});describe('with a failing and a passing rule',()=>{it('has one error',()=>{const verifier =makeVerifierWithFailedRule('fake reason');
      verifier.addRule(passingRule);const errors = verifier.verify('any input');expect(errors.length).toBe(1);});it('error text belongs to failed rule',()=>{const verifier =makeVerifierWithFailedRule('fake reason');
      verifier.addRule(passingRule);const errors = verifier.verify('any input');expect(errors[0]).toContain('fake reason');});});});

可以看到,重构后的测试代码不含

beforeEach()

方法,所有相关信息都可以从

it()

方法直接获取。这里的关键,是将各测试的状态严格限制在

it()

方法内,而不是放在嵌套的

describe()

方法内。

2.8 回到 test() 方法 Going full circle to test()

如果只要求简洁,对测试的结构性和层次性要求不高,则可以用

test()

方法来编写测试用例。结合刚才的工厂方法,写作:

test('pass verifier, with failed rule, has an error message based on the rule.reason',()=>{const verifier =makeVerifierWithFailedRule('fake reason');const errors = verifier.verify('any input');expect(errors[0]).toContain('fake reason');});test('pass verifier, with failed rule, has exactly one error',()=>{const verifier =makeVerifierWithFailedRule('fake reason');const errors = verifier.verify('any input');expect(errors.length).toBe(1);});test('pass verifier, with passing rule, has no errors',()=>{const verifier =makeVerifierWithPassingRule();const errors = verifier.verify('any input');expect(errors.length).toBe(0);});test('pass verifier, with passing  and failing rule, has one error',()=>{const verifier =makeVerifierWithFailedRule('fake reason');
  verifier.addRule(passingRule);const errors = verifier.verify('any input');expect(errors.length).toBe(1);});test('pass verifier, with passing  and failing rule, error text belongs to failed rule',()=>{const verifier =makeVerifierWithFailedRule('fake reason');
  verifier.addRule(passingRule);const errors = verifier.verify('any input');expect(errors[0]).toContain('fake reason');});

那种写法更合适,需要自行决定。

2.9 重构为参数化的测试 Refactoring to parameterized tests

所谓 参数化的测试(parameterized tests),就是一种特殊的软件测试技术,用于在同一测试用例中运行多个不同的 输入组合,从而有效地减少代码冗余、提高测试的覆盖率和可维护性。

Jest

支持好几种参数化测试,书中介绍了两个,随书源码则给出了三个。首先构造一个新的目标函数

oneUpperCaseRule

// password-rules.jsconstoneUpperCaseRule=(input)=>{return{
    passed:(input.toLowerCase()!== input),
    reason:'at least one upper case needed'};};

module.exports ={
  oneUpperCaseRule
};

然后导入单元测试文件

__tests__/password-rules.spec.js

const{ oneUpperCaseRule }=require('../password-rules');describe('v1 one uppercase rule',()=>{test('given no uppercase, it fails',()=>{const result =oneUpperCaseRule('abc');expect(result.passed).toEqual(false);});test('given one uppercase, it passes',()=>{const result =oneUpperCaseRule('Abc');expect(result.passed).toEqual(true);});test('given a different uppercase, it passes',()=>{const result =oneUpperCaseRule('aBc');expect(result.passed).toEqual(true);});});

可以看到,上述测试用例出现了大量代码冗余,这里其实只测试了一种情况:对存在大写字母的输入内容进行测试。

为此,

Jest

提供了

test.each()

方法,可以简化上述写法。

第一种:将 输入 以数组形式传入

describe('v2 one uppercase rule',()=>{test('given no uppercase, it fails',()=>{const result =oneUpperCaseRule('abc');expect(result.passed).toEqual(false);});

  test.each(['Abc','aBc'])('given one uppercase, it passes',(input)=>{const result =oneUpperCaseRule(input);expect(result.passed).toEqual(true);});});

第二种:将 输入和期望值 以数组形式同时传入

describe('v3 one uppercase rule',()=>{
  test.each([['Abc',true],['aBc',true],['abc',false]])('given %s, %s ',(input, expected)=>{const result =oneUpperCaseRule(input);expect(result.passed).toEqual(expected);});});

第三种:将 输入和期望值

Jest

表格形式拼接(仅在源代码中展示,原书未介绍)

describe('v4 one uppercase rule, with the fancy jest table input',()=>{
  test.each`
    input | expected
    ${'Abc'} | ${true}${'aBc'} | ${true}${'abc'} | ${false}`('given $input',({ input, expected })=>{const result =oneUpperCaseRule(input);expect(result.passed).toEqual(expected);});});

注意:第三种写法中的模板字符串两边 没有使用 小括号!!

如果选用的测试框架不支持参数化测试的语法,也可以借助原生 JS 的循环遍历来实现:

describe('v5 one uppercase rule, with vanilla JS test.each',()=>{const tests ={
    Abc:true,
    aBc:true,
    abc:false};for(const[input, expected]of Object.entries(tests)){test(`given ${input}, ${expected}`,()=>{const result =oneUpperCaseRule(input);expect(result.passed).toEqual(expected);});}});

警告

参数化测试固然方便,使用不当则可能严重降低测试的可读性与可维护性(双刃剑)。

2.10 对抛出错误的检查 Checking for expected thrown errors

改造

verify()

方法,让它抛出一个错误(第 12 行):

classPasswordVerifier1{constructor(){this.rules =[];}addRule(rule){this.rules.push(rule);}verify(input){if(this.rules.length ===0){thrownewError('There are no rules configured');}const errors =[];this.rules.forEach(rule=>{const result =rule(input);if(result.passed ===false){
        errors.push(result.reason);}});return errors;}}

module.exports ={ PasswordVerifier1 };

这类测试的一种传统写法,是放入

try-catch

结构:

test('verify, with no rules, throws exception',()=>{const verifier =makeVerifier();try{
    verifier.verify('any input');fail('error was expected but not thrown');}catch(e){expect(e.message).toContain('no rules configured');}});

上述代码中的

fail()

函数是

Jasmine

框架的历史遗留 API,目前已不在

Jest

官方 API 文档中维护,官方建议用

expect.assertions(1)

进行替换,并且在未触发

catch()

块运行时让测试不通过。不推荐使用

fail()

try-catch

结构。

推荐写法:从

expect()

断言中调用

toThrowError()

方法:

test('verify, with no rules, throws exception',()=>{const verifier =makeVerifier();expect(()=> verifier.verify('any input')).toThrowError(/no rules configured/);});

**关于

Jest

快照(snapshots)**

主要用于

React

等框架,让当前渲染出的组件与该组件的一个快照进行比较。但由于不够直观,容易测试一些无关内容,可读性和可维护性都不高,因此 并不推荐使用。一旦不慎,还很容易造成滥用,让测试可信度大打折扣:

it('renders',()=>{expect(<MyComponent/>).toMatchSnapshot();});

2.11 设置不同的测试配置 Setting test categories

可以通过两种方式让

Jest

启用不同的配置文件:

  • 命令行参数:使用 --testPathPattern 参数,详见 Jest 官方文档:https://jestjs.io/docs/cli
  • 使用独立的 npm 运行脚本。

第二种方法,先在各自的配置文件中设置好具体的配置内容(

testRegex

):

// jest.config.integration.jsvar config =require('./jest.config');
config.testRegex ="integration\\.js$";
module.exports = config;// jest.config.unit.jsvar config =require('./jest.config');
config.testRegex ="unit\\.js$";
module.exports = config;

然后在

npm

脚本中进行配置:

//Package.json// ..."scripts":{"unit":"jest -c jest.config.unit.js","integ":"jest -c jest.config.integration.js"// ...
标签: 1024程序员节

本文转载自: https://blog.csdn.net/frgod/article/details/143217957
版权归原作者 安冬的码畜日常 所有, 如有侵权,请联系我们删除。

“【The Art of Unit Testing 3_自学笔记04】第二章:编写第一个单元测试(下)”的评论:

还没有评论