读程序员的制胜技笔记06_测试(下)

发布时间 2023-11-08 07:17:16作者: 躺柒

1. 决定测试对象

1.1. 确保团队产出可靠的测试

1.1.1. 从成品代码中随机删掉几行,然后运行测试

1.1.2. 如果代码在这种情况下依然测试通过,就意味着程序员写的代码失败了

1.2. 规范是很好的出发点,但你不容易在行业内找到相关规范

1.3. 尊重边界

1.3.1. 为网络游戏检查用户是否达到法定年龄

1.3.1.1. 假设18岁是你游戏用户的法定年龄

1.3.2. 拦截算法

1.3.2.1. C#

public static bool IsLegalBirthdate(DateTime birthdate) {
  const int legalAge = 18;
  var now = DateTime.Now;
  int age = now.Year - birthdate.Year;
  if (age == legalAge) {  ⇽--- 代码里的条件判定
    return now.Month > birthdate.Month
      || (now.Month == birthdate.Month
          && now.Day > birthdate.Day);
  }
  return age > legalAge;
}  ⇽--- 
1.3.2.1.1. 不需要测试公元1年1月1日到9999年12月31日之间所有可能的DateTime值(有360多万个)
1.3.2.1.2. 只需要测试7个不同的输入
1.3.2.1.3. 通过条件语句将输入范围进行分割的操作称为“边界条件”(boundary conditional)
1.3.2.1.3.1. 定义了输入数据的边界

1.3.3. 数据绑定测试(data-bound test),参数化测试可以为你节省大量的时间

1.3.4. C#

[TestCase(18, 0, -1, ExpectedResult = true)]
[TestCase(18, 0, 0, ExpectedResult = false)]
[TestCase(18, 0, 1, ExpectedResult = false)]
[TestCase(18, -1, 0, ExpectedResult = true)]
[TestCase(18, 1, 0, ExpectedResult = false)]
[TestCase(19, 0, 0, ExpectedResult = true)]
[TestCase(17, 0, 0, ExpectedResult = false)]
public bool IsLegalBirthdate_ReturnsExpectedValues(
  int yearDifference, int monthDifference, int dayDifference) {
  var now = DateTime.Now;
  var input = now.AddYears(-yearDifference)  ⇽--- 在这准备我们的实际输入
    .AddMonths(monthDifference)
    .AddDays(dayDifference);  ⇽--- 
  return DateTimeExtensions.IsLegalBirthdate(input);
}

1.4. 代码覆盖率

1.4.1. 代码覆盖率是一种魔法,而且和其他魔法一样,具有被夸大的成分

1.4.2. 在开发环境中很少有开箱即用的代码覆盖率测量工具

1.4.2.1. 存在于Visual Studio的“天价版本”中

1.4.2.2. 存在于其他付费的第三方工具中

1.4.2.2.1. NCrunch、dotCover、and NCover

1.4.2.3. Codecov是一个可以与你的在线代码库对接的服务,并且它有免费计划可选

1.4.2.4. Visual Studio Code中的Coverlet库和代码覆盖率报告扩展可以在.NET本地进行免费的代码覆盖率测量

1.4.3. 代码覆盖率工具能为工作开一个好头,但它们在显示实际测试覆盖率方面并不完全有效

2. 不要写测试

2.1. 不要写代码

2.1.1. 如果一段代码不存在,它就不需要被测试。被删掉的代码是没有bug的。

2.1.2. 被删掉的代码是没有bug的

2.1.3. 你可能想写自定义的正则表达式来验证URL,而你所需要做的只是利用System.Uri类

2.1.4. 第三方代码并不能保证是完美的,也不一定适合你的目的

2.2. 不要一次写完所有的测试

2.2.1. 80%的后果是20%的原因所导致的

2.2.1.1. 帕累托法则(Pareto principle)

2.2.2. bug不会连续出现

2.2.3. 不是每一行代码都有相同的概率产生一个错误

2.2.4. 冒烟测试(smoke testing)

2.2.5. 对关键的、共享的组件有良好的测试覆盖率比100%的代码覆盖率更重要

3. 让编译器测试你的代码

3.1. 在强类型语言中,你可以利用类型系统来减少需要的测试数量

3.2. 正则表达式的“神话”

3.2.1. 正则表达式无疑是计算机科学史上最牛的发明之一

3.2.2. 斯蒂芬·科尔·克莱因(Stephen Cole Kleene)

3.2.3. 正则表达式不能感知上下文

3.2.3.1. 不能用一个正则表达式来寻找HTML文档中最内层的标签

3.2.3.2. 不能检测不匹配的关闭标签

3.2.4. 不适合用于复杂的解析任务

3.2.5. 可以用来解析具有非嵌套结构的文本

3.2.6. 在它适合的情况下还是有让人惊讶的性能的

3.2.7. 如果存在更简单的替代方法,你就不应该使用正则表达式

3.2.7.1. 常见的字符串检查操作,如StartsWith、EndsWith、IndexOf、LastIndexOf、IsNullOrEmpty以及IsNullOrWhiteSpace

3.2.7.2. 倾向于使用所提供的方法,而不是正则表达式

3.3. 消除null检查

3.3.1. 对Username类所在的项目启用可空引用,那我们就根本不用为null情况写测试

3.3.2. 例外是,当我们在写一个公共API的时候,它可能不会在可感知可空引用(nullable-reference-aware)的代码中运行

3.3.2.1. 还得进行null检查

3.3.3. 在恰当的时候将Username声明为结构(struct),它就会成为一个值类型,这也让它免去了进行null检查的操作

3.4. 消除范围检查

3.4.1. 使用无符号整数类型,可以减小可能的无效输入值的范围

3.4.1.1. 当你使用无符号类型时,你往函数引入一个负值就会引起编译错误

3.4.1.2. 如果要传一个负值,只能通过明确的类型转换才可以达成

3.4.1.3. 验证负值参数就再也不是函数的任务了

3.4.2. byte类型

3.4.2.1. 不是int类型

3.4.2.2. 标签的数量根本没有超过255个

3.4.2.2.1. 需要超过255个条目,你就必须用short或int来替换所有对byte的引用

3.4.2.3. 避免了输入值可能是负值或者太大的情况

3.4.2.4. 不用再对输入值进行验证了

3.4.2.4.1. 省掉了一次测试,而且输入值的范围也大为改善,显著减少了错误的发生

3.4.3. 你的舒适度和你的时间才是最重要的,而使用数据类型来让数据自带有效值范围带来的好处只是其次

3.5. 消除有效值检查

3.5.1. 单独的File.Create、File.OpenRead和File.OpenWrite方法了,这可以避免添加额外参数和对该参数的解析操作,就不可能传递错误的参数了

3.5.2. 在C#中,一个常见的技术是使用布尔参数来改变运行函数的逻辑

3.5.2.1. C#

public IList<Tag> GetTrendingTags(byte numberOfItems,
  bool sortByTitle, bool yesterdaysTags) {  ⇽--- 更多参数
  var query = yesterdaysTags  ⇽--- 更多条件句
    ? db.GetTrendingTagTable()
    : db.GetYesterdaysTrendingTagTable();
  if (sortByTitle) {  ⇽--- 
    query = query.OrderBy(p => p.Title);
  }
  return query.Take(numberOfItems).ToList();
}

3.5.3. 互相独立的函数

3.5.3.1. C#

public IList<Tag> GetTrendingTags(byte numberOfItems) {  ⇽--- 我们通过函数名而不是参数来分隔函数

  return db.GetTrendingTagTable()
    .Take(numberOfItems)
    .ToList();
}
public IList<Tag> GetTrendingTagsByTitle(  ⇽--- 
  byte numberOfItems) {
  return db.GetTrendingTagTable()
    .OrderBy(p => p.Title)
    .Take(numberOfItems)
    .ToList();
}
public IList<Tag> GetYesterdaysTrendingTags(byte numberOfItems) {  ⇽--- 
  return db.GetYesterdaysTrendingTagTable()
    .Take(numberOfItems)
    .ToList();
}

3.5.3.2. 省了一次测试

4. 命名测试

4.1. 一个名称可以包含很多信息

4.1.1. 被测试的函数的名称

4.1.2. 输入和输出状态

4.1.3. 预期的行为

4.2. 使用“A_B_C”的格式来命名测试

5. 总结

5.1. 先让自己能够不写测试,通过思考过程来克服之前对测试的偏见

5.2. 以测试驱动开发和类似的条框会让你更讨厌写测试

5.3. 写测试的工作时间可以通过测试框架大大缩短,特别是参数化、数据驱动的测试

5.4. 通过正确分析函数输入的边界值,可以大大减少测试的数量

5.5. 正确使用数据类型可以让你避免写许多不必要的测试

5.6. 测试不只可以确保代码的质量,也可以帮助你提高自己的开发技能和产出