重构测试体系构筑

4 篇文章 0 订阅

        重构是很有价值的工具,但只有重构不行。要正确地进行重构,前提是得有一套稳固的测试集合,以帮助我们发现难以避免的疏漏。

        编写优良的测试程序,可以极大提高我们的编程速度,即使不进行重构也一样如此。

1. 自测试代码的价值

        如果你认真观察大多数程序员的时间,就会发现,他们编写代码的时间仅占所有时间的很少一部分。有时来决定下一步干什么,有时花在设计上,但是花费在调试上的时间是最多的。

        一套测试就是一个强大的bug侦测器,能够大大缩减查找bug所需的时间。确保所有测试都完全自动化,让它们检查自己的测试结果。

        编写测试代码还能帮我们把注意力集中于接口而非实现(这永远是意见好事)。预先写好的测试代码也为我们的工作安上一个明确的结束标志:一旦测试代码正常运行,工作就可以结束了。

        先写测试再写业务代码,即测试驱动开发。这种编程方式依赖于这个短循环:先编写一个失败的测试,编写代码使测试通过,然后进行重构以保证代码的整洁。这个“测试、编码、重构”的循环应该在每个小时内都完成很多次。

2. 待测试示例代码

// class Province...
class Province {
  constructor(doc) {
    this._name = doc.name;
    this._producers = [];
    this._totalProduction = 0;
    this._demand = doc.demand;
    this._price = doc.price;
    doc.producers.forEach(d => this.addProducer(new Producer(this, d)));
  }

  get name() { return this._name; }
  get producers() { return this._producers.slice(); }
  get totalProduction() { return this._totalProduction; }
  set totalProduction(arg) { this._totalProduction = arg; }
  get demand() { return this._demand; }
  set demand(arg) { this._demand = arg; }
  get price() { return this._price; }
  set price(arg) { this._price = parseInt(arg); }
  get shortfall() {
    return this._demand - this.totalProduction;
  }
  get profit() {
    return this.demandValue - this.demandCost;
  }
  get demandCost() {
    let remianingDemand = this.demand;
    let result = 0;
    this.producers
      .sort((a, b) => a.cost - b.cost)
      .forEach(p => {
        const contribution = Math.min(remianingDemand, p.production);
        remianingDemand -= contribution;
        result += contribution * p.cost;
      });
    return result;
  }
  get demandValue() {
    return this.satisfiedDemand * this.price;
  }
  get satisfiedDemand() {
    return Math.min(this._demand, this.totalProduction);
  }

  addProducer(arg) {
    this._producers.push(arg);
    this._totalProduction += arg.production;
  }

}
// 测试数据
function sampleProvinceData() {
  return {
    name: 'Asia',
    producers: [
      {name: 'Byzantium', cost: 10, production: 9},
      {name: 'Attalia', cost: 12, production: 10},
      {name: 'Sinope', cost: 10, production: 6},
    ],
    demand: 30,
    price: 20
  }
}
// class Producer...
class Producer {
  constructor(aProvince, data) {
    this._province = aProvince;
    this._cost = data.cost;
    this._name = data.name;
    this._production = data.production || 0;
  }
  get name() { return this._name; }
  get cost() { return this._cost; }
  set cost() { this._cost = parseInt(arg); }

  get production() { return this._production; }
  set production(amountStr) {
    const amount = parseInt(amountStr);
    const newProduction = Number.isNaN(amount) ? 0 : amount;
    this._province.totalProduction += newProduction - this._production;
    this._production = newProduction;
  }
}

3. 测试代码

        测试代码前,需要一个测试框架。Javascript的测试框架有很多,这里选用Mocha。Mocha框架组织测试代码的方式将其分组,每一组包含一套相关的测试。测试需要写在一个it块中。还需要设置一些测试夹具(fixture),即测试所需的数据和对象等;然后验证测试夹具是否具备某些特征(就本例而言是验证算出的缺额应该是期望的值)。

        频繁地运行测试。对于你处理的代码,与其对应的测试至少每个几分钟就要运行一次,每天至少运行一次所有的测试。

describe('province', function() {
  let asia;
  // 创建测试夹具,beforeEach子句会在每个测试之前运行一遍,将asia变量清空,每次
  // 都给它赋一个新值,这样保证了测试的独立性。
  beforeEach(function() {
    asia = new Province(sampleProvinceData());
  });
  it('shortfall', function() {
    // assert.equal(asia.shortfall, 5); // assert风格
    expect(asia.shortfall).equal(5); // expect风格
  });
  it('profit', function() {
    expect(asia.profit).equal(230);
  });
  // 修改测试夹具
  it('change production', function() {
    asia.producers[0].production = 20;
    expect(asia.shortfall).equal(-6);
    expect(asia.profit).equal(292);
  });
  // 边界场景测试
  it('zero demand', function() {
    asia.demand = 0;
    expect(asia.shortfall).equal(-25);
    expect(asia.profit).equal(0)
  })
  it('zero demand', function() {
    asia.demand = -1;
    expect(asia.shortfall).equal(-26);
    expect(asia.profit).equal(-10)
  })
  // 设值函数接受的字符串是从UI上的字段读来的,有可能为空字符串,
  // 因此,同样需要测试来保证对空字符串的处理符合逻辑
  it('empty string demand', function() {
    asia.demand = '';
    expect(asia.shortfall).NaN;
    expect(asia.profit).NaN;
  });
})
// 探测边界条件
describe('no producers', function() {
  let noProducers;
  beforeEach(function() {
    const data = {
      name: 'No producers',
      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);
  });
})

4. 测试远不止如此

        测试既是重构所必要的基础保障,本身也是一个有价值的工具。如今一个架构的好坏,很大程度要取决于它的可测试性

        一个常见的问题是,“要写多少测试才算足够?”这个问题没有很好的衡量标准。一个测试集是否足够好,最好的衡量标准其实是主观的,请你问问自己:如果有人在代码里引入了一个缺陷,你有多大的自信它能被测试集揪出来?这种信心难以被定量分析,盲目自信不应该被计算在内,但自测试代码的全部目标,就是要帮你获得这种自信。

        测试同样可能过犹不及。测试写得太多的一个征兆是,相比要改的代码,我在改动测试上花费了更多的时间。不过尽管过度测试时有发生,相比测试不足的情况还是稀少的多。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

~卷心菜~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值