# 软件测试简介

> 原文： [https://www.fuzzingbook.org/html/Intro_Testing.html](https://www.fuzzingbook.org/html/Intro_Testing.html)

在进入本书的中心部分之前，让我们介绍软件测试的基本概念。 为什么根本需要测试软件？ 一个测试软件如何？ 如何判断测试是否成功？ 如何知道一个人是否测试足够？ 在本章中，让我们回顾最重要的概念，并同时熟悉Python和交互式笔记本。

## 简单测试

让我们从一个简单的例子开始。 您的同事已被要求实现平方根函数$ \ sqrt {x} $。 （让我们暂时假设环境还没有。）在研究了 [Newton-Raphson方法](https://en.wikipedia.org/wiki/Newton%27s_method)之后，她提出了以下Python代码，并声称实际上`my_sqrt()`这个函数 计算平方根。

```py
def my_sqrt(x):
    """Computes the square root of x, using the Newton-Raphson method"""
    approx = None
    guess = x / 2
    while approx != guess:
        approx = guess
        guess = (approx + x / approx) / 2
    return approx

```

现在，您的工作是确定此功能是否真正按照其要求执行。

### 了解Python程序

如果您不熟悉Python，则可能首先必须了解上述代码的功能。 我们非常推荐 [Python教程](https://docs.python.org/3/tutorial/)来了解Python的工作原理。 对于您来说，理解以上代码最重要的是以下三个：

1.  Python通过*缩进*构造程序，因此函数和`while`主体是通过缩进来定义的；
2.  Python是*动态类型的*，这意味着`x`，`approx`或`guess`之类的变量类型是在运行时确定的。
3.  Python的大多数语法功能均受其他通用语言的启发，例如控制结构（`while`，`if`），分配（`=`）或比较（`==`，`!=`和`<`）。

这样，您已经可以理解上述代码的作用：从`x / 2`的`guess`开始，它在`approx`中计算越来越好的近似值，直到`approx`的值不再改变为止。 这是最终返回的值。

### 运行功能

要确定`my_sqrt()`是否正常工作，我们可以*用一些值测试*。 例如，对于`x = 4`，它将产生正确的值：

```py
my_sqrt(4)

```

```py
2.0

```

`my_sqrt(4)`上方的上部（所谓的*单元格*）是Python解释器的输入，默认情况下，*将对其进行评估*。 下部（`2.0`）是其输出。 我们可以看到`my_sqrt(4)`产生正确的值。

显然`x = 2.0`也是如此：

```py
my_sqrt(2)

```

```py
1.414213562373095

```

### 与笔记本交互

如果您正在交互式笔记本中阅读此内容，则还可以尝试使用`my_sqrt()`和其他值。 单击上面带有`my_sqrt()`调用的单元格之一，然后将值更改为`my_sqrt(1)`。 按`Shift+Enter`（或单击播放符号）执行它并查看结果。 如果收到错误消息，请转到上面带有`my_sqrt()`定义的单元格，然后首先执行此操作。 您也可以一次运行*所有*单元； 有关详细信息，请参见“笔记本”菜单。 （实际上，您也可以通过单击文本来更改文本，以及诸如此类的核心内容错误。）

执行单个单元格不会执行其他单元格，因此，如果您的单元格基于尚未执行的另一个单元格中的定义，则会收到错误消息。 您可以从菜单中选择`Run all cells above`以确保设置了所有定义。

还要记住，除非覆盖，否则所有定义都将在执行过程中保留。 有时，它有助于*重新启动内核*（即从头启动Python解释器），以摆脱较旧的多余定义。

### 调试功能

要查看`my_sqrt()`的工作方式，一种简单的策略是在关键位置插入`print()`语句。 例如，您可以记录`approx`的值，以查看每次循环迭代如何接近实际值：

```py
def my_sqrt_with_log(x):
    """Computes the square root of x, using the Newton–Raphson method"""
    approx = None
    guess = x / 2
    while approx != guess:
        print("approx =", approx)  # <-- New
        approx = guess
        guess = (approx + x / approx) / 2
    return approx

```

```py
my_sqrt_with_log(9)

```

```py
approx = None
approx = 4.5
approx = 3.25
approx = 3.0096153846153846
approx = 3.000015360039322
approx = 3.0000000000393214

```

```py
3.0

```

交互式笔记本还允许启动交互式*调试器* –在单元格顶部插入“魔术线” `%%debug`，然后查看会发生什么情况。 不幸的是，交互式调试器会干扰我们的动态分析技术，因此我们主要使用日志记录和断言进行调试。

### 检查功能

让我们回到测试。 我们可以阅读并运行代码，但上述`my_sqrt(2)`的值实际上正确吗？ 我们可以轻松地通过利用$ \ sqrt {x} $再次平方为$ x $来进行验证，换句话说$ \ sqrt {x} \ times \ sqrt {x} = x $。 让我们来看看：

```py
my_sqrt(2) * my_sqrt(2)

```

```py
1.9999999999999996

```

好的，我们确实有一些舍入错误，但是，这似乎很好。

现在我们要做的是，我们已经对上述程序进行了*测试*：*在给定的输入上执行了*，并且*检查了*其结果是否正确 。 在程序投入生产之前，这种测试是质量保证的最低限度。

## 自动化测试执行

到目前为止，我们已经手动测试了上述程序，即，手动运行该程序并手动检查其结果。 这是一种非常灵活的测试方法，但从长远来看，它效率很低：

1.  手动地，您只能检查非常有限的执行及其结果
2.  对程序进行任何更改后，您必须重复测试过程

这就是为什么*自动执行*测试非常有用的原因。 一种简单的方法是让计算机首先进行计算，然后让计算机检查结果。

例如，这段代码自动测试$ \ sqrt {4} = 2 $是否成立：

```py
result = my_sqrt(4)
expected_result = 2.0
if result == expected_result:
    print("Test passed")
else:
    print("Test failed")

```

```py
Test passed

```

这个测试的好处是我们可以一次又一次地运行它，从而确保至少正确计算了4的平方根。 但是，仍然存在许多问题：

1.  单个测试需要*五行代码*
2.  我们不在乎舍入错误
3.  我们只检查单个输入（和单个结果）

让我们一一解决这些问题。 首先，让我们使测试更加紧凑。 几乎所有的编程语言都可以自动检查条件是否成立，如果条件不成立则停止执行。 这称为*断言*，对于测试非常有用。

在Python中，`assert`语句带有条件，如果条件为true，则什么也不会发生。 （如果一切正常运行，则不要打扰。）但是，如果条件评估为false，则`assert`会引发异常，表明测试刚刚失败。

在我们的示例中，我们可以使用`assert`轻松检查`my_sqrt()`是否产生上述预期结果：

```py
assert my_sqrt(4) == 2

```

当您执行此行代码时，什么都不会发生：我们只是证明（或断言）我们的实现确实产生$ \ sqrt {4} = 2 $。

但是请记住，浮点计算可能会导致舍入误差。 因此，我们不能简单地比较两个浮点数是否相等。 相反，我们将确保它们之间的绝对差保持在某个阈值（通常表示为$ \ epsilon $或`epsilon`）之下。 这是我们可以做到的：

```py
EPSILON = 1e-8

```

```py
assert abs(my_sqrt(4) - 2) < EPSILON

```

我们也可以为此目的引入一个特殊功能，现在对具体值进行更多测试：

```py
def assertEquals(x, y, epsilon=1e-8):
    assert abs(x - y) < epsilon

```

```py
assertEquals(my_sqrt(4), 2)
assertEquals(my_sqrt(9), 3)
assertEquals(my_sqrt(100), 10)

```

似乎可以工作，对吧？ 如果我们知道计算的预期结果，则可以一次又一次地使用此类断言，以确保我们的程序正确运行。

## 生成测试

还记得属性$ \ sqrt {x} \ times \ sqrt {x} = x $普遍适用吗？ 我们还可以使用一些值来显式测试它：

```py
assertEquals(my_sqrt(2) * my_sqrt(2), 2)
assertEquals(my_sqrt(3) * my_sqrt(3), 3)
assertEquals(my_sqrt(42.11) * my_sqrt(42.11), 42.11)

```

似乎仍然有效，对不对？ 不过最重要的是，我们可以很容易地测试$ \ sqrt {x} \ times \ sqrt {x} = x $的数千个值：

```py
for n in range(1, 1000):
    assertEquals(my_sqrt(n) * my_sqrt(n), n)

```

用100个值测试`my_sqrt()`需要多少时间？ 让我们来看看。

我们使用自己的 [`Timer`模块](Timer.html)测量经过的时间。 为了能够使用`Timer`，我们首先导入我们自己的实用程序模块，该模块允许我们导入其他笔记本。

```py
import [fuzzingbook_utils](https://github.com/uds-se/fuzzingbook/tree/master/notebooks/fuzzingbook_utils)

```

```py
from [Timer](Timer.html) import Timer

```

```py
with Timer() as t:
    for n in range(1, 10000):
        assertEquals(my_sqrt(n) * my_sqrt(n), n)
print(t.elapsed_time())

```

```py
0.020416864000253554

```

10,000个值大约需要百分之一秒，因此单次执行`my_sqrt()`需要1/1000000秒或大约1微秒。

让我们重复随机选择10,000个值。 Python `random.random()`函数返回0.0到1.0之间的随机值：

```py
import [random](https://docs.python.org/3/library/random.html)

```

```py
with Timer() as t:
    for i in range(10000):
        x = 1 + random.random() * 1000000
        assertEquals(my_sqrt(x) * my_sqrt(x), x)
print(t.elapsed_time())

```

```py
0.02402487299968925

```

一秒钟之内，我们现在测试了10,000个随机值，并且每次实际上都正确计算了平方根。 我们只要对`my_sqrt()`进行任何更改，就可以重复此测试，每次都可以增强我们对`my_sqrt()`可以正常工作的信心。 但是请注意，尽管在生成随机值时随机函数*是无偏的*，但不太可能生成会极大改变程序行为的特殊值。 我们将在下面稍后讨论。

## 运行时验证

除了编写和运行`my_sqrt()`的测试外，我们还可以将*集成到执行中。* 这样，将自动检查`my_sqrt()`的每个*调用。*

这样的*自动运行时检查*非常易于实现：

```py
def my_sqrt_checked(x):
    root = my_sqrt(x)
    assertEquals(root * root, x)
    return root

```

现在，无论何时我们使用`my_sqrt_checked()` $ \ dots $计算一个根

```py
my_sqrt_checked(2.0)

```

```py
1.414213562373095

```

我们已经知道结果是正确的，并且对于每次新的成功计算都是如此。

如上所述，自动运行时检查假设有两件事：

*   必须能够*制定*这样的运行时检查。 始终有可能要检查具体的值，但是以抽象方式制定所需的属性可能非常复杂。 实际上，您需要确定哪些属性最关键，并为它们设计适当的检查。 另外，运行时检查可能不仅取决于本地属性，还取决于程序状态的多个属性，所有这些属性都必须确定。

*   必须能够*承担*这种运行时检查。 在`my_sqrt()`的情况下，检查不是很昂贵； 但是，即使经过简单的操作，如果我们不得不检查大型数据结构，检查的费用很快就会变得昂贵。 在实践中，通常会在生产过程中禁用运行时检查，为了提高效率而牺牲了可靠性。 另一方面，一套全面的运行时检查是发现错误并快速调试它们的好方法。 您需要确定在生产期间仍需要多少种这样的功能。

## 系统输入与功能输入

此时，我们可以将`my_sqrt()`提供给其他程序员，然后他们可以将其嵌入其代码中。 在某些时候，它必须处理来自*第三方*的输入，即不受程序员的控制。

让我们通过假设一个*程序* `sqrt_program()`来模拟此*系统输入*，该程序的输入是第三方控制的字符串：

```py
def sqrt_program(arg):
    x = int(arg)
    print('The root of', x, 'is', my_sqrt(x))

```

我们假设`sqrt_program`是一个程序，它可以从命令行接受系统输入，如

```py
$ sqrt_program 4
2

```

我们可以通过一些系统输入轻松调用`sqrt_program()`：

```py
sqrt_program("4")

```

```py
The root of 4 is 2.0

```

有什么问题？ 好吧，问题在于我们不检查外部输入的有效性。 例如，尝试调用`sqrt_program(-1)`。 怎么了？

实际上，如果您使用负数调用`my_sqrt()`，它将进入无限循环。 由于技术原因，本章不能有无限循环（除非我们希望代码永远运行）。 因此，我们使用特殊的`with ExpectTimeOut(1)`构造在一秒钟后中断执行。

```py
from [ExpectError](ExpectError.html) import ExpectTimeout

```

```py
with ExpectTimeout(1):
    sqrt_program("-1")

```

```py
Traceback (most recent call last):
  File "<ipython-input-25-add01711282b>", line 2, in <module>
    sqrt_program("-1")
  File "<ipython-input-22-53e8ec8bb3ca>", line 3, in sqrt_program
    print('The root of', x, 'is', my_sqrt(x))
  File "<ipython-input-1-47185ad159a1>", line 5, in my_sqrt
    while approx != guess:
  File "<ipython-input-1-47185ad159a1>", line 5, in my_sqrt
    while approx != guess:
  File "<string>", line 16, in check_time
TimeoutError (expected)

```

上面的消息是*错误消息*，表明出了点问题。 它列出了错误发生时处于活动状态的函数和行的*调用堆栈*。 最底部的行是最后执行的行； 上面的几行代表函数调用–在我们的例子中，最高为`my_sqrt(x)`。

我们不希望我们的代码以异常终止。 因此，在接受外部输入时，我们必须确保已对​​其进行正确验证。 例如，我们可以写：

```py
def sqrt_program(arg):
    x = int(arg)
    if x < 0:
        print("Illegal Input")
    else:
        print('The root of', x, 'is', my_sqrt(x))

```

然后我们可以确定`my_sqrt()`仅根据其规范被调用。

```py
sqrt_program("-1")

```

```py
Illegal Input

```

可是等等！ 如果不使用数字调用`sqrt_program()`，该怎么办？ 然后，我们将尝试转换非数字字符串，这也会导致运行时错误：

```py
from [ExpectError](ExpectError.html) import ExpectError

```

```py
with ExpectError():
    sqrt_program("xyzzy")

```

```py
Traceback (most recent call last):
  File "<ipython-input-29-8c5aae65a938>", line 2, in <module>
    sqrt_program("xyzzy")
  File "<ipython-input-26-ea86281b33cf>", line 2, in sqrt_program
    x = int(arg)
ValueError: invalid literal for int() with base 10: 'xyzzy' (expected)

```

这是一个还会检查输入错误的版本：

```py
def sqrt_program(arg):
    try:
        x = float(arg)
    except ValueError:
        print("Illegal Input")
    else:
        if x < 0:
            print("Illegal Number")
        else:
            print('The root of', x, 'is', my_sqrt(x))

```

```py
sqrt_program("4")

```

```py
The root of 4.0 is 2.0

```

```py
sqrt_program("-1")

```

```py
Illegal Number

```

```py
sqrt_program("xyzzy")

```

```py
Illegal Input

```

现在我们已经看到，在系统级别，程序必须能够优雅地处理任何类型的输入，而永远不会进入不受控制的状态。 当然，这对程序员来说是负担，他们必须努力使自己的程序在所有情况下都健壮起来。 但是，这种负担在生成软件测试时成为*的好处*：如果程序可以处理任何类型的输入（可能带有明确定义的错误消息），我们也可以*将其发送给任何类型的输入* 。 但是，当使用生成的值调用函数时，我们必须*知道*其确切的前提条件。

## 测试限制

尽管在测试方面做出了最大的努力，但请记住，您始终在检查*有限*输入集的功能。 因此，可能总是存在*未经测试的*输入，其功能可能仍会失败。

例如，在`my_sqrt()`的情况下，计算$ \ sqrt {0} $将除以零：

```py
with ExpectError():
    root = my_sqrt(0)

```

```py
Traceback (most recent call last):
  File "<ipython-input-34-24ede1f53910>", line 2, in <module>
    root = my_sqrt(0)
  File "<ipython-input-1-47185ad159a1>", line 7, in my_sqrt
    guess = (approx + x / approx) / 2
ZeroDivisionError: float division by zero (expected)

```

到目前为止，在我们的测试中，我们尚未检查此条件，这意味着以$ \ sqrt {0} = 0 $为基础的程序将意外失败。 但是，即使我们已经设置了随机生成器以产生0–1000000而不是1–1000000的输入，它随机产生零值的机会仍然是百万分之一。 如果对于几个单独的值，函数的行为完全不同，则纯随机测试几乎没有机会产生这些值。

我们当然可以相应地修复该功能，记录`x`的可接受值并处理特殊情况`x = 0`：

```py
def my_sqrt_fixed(x):
    assert 0 <= x
    if x == 0:
        return 0
    return my_sqrt(x)

```

这样，我们现在可以正确地计算$ \ sqrt {0} = 0 $：

```py
assert my_sqrt_fixed(0) == 0

```

非法值现在导致异常：

```py
with ExpectError():
    root = my_sqrt_fixed(-1)

```

```py
Traceback (most recent call last):
  File "<ipython-input-37-55b1caf1586a>", line 2, in <module>
    root = my_sqrt_fixed(-1)
  File "<ipython-input-35-f3e21e80ddfb>", line 2, in my_sqrt_fixed
    assert 0 <= x
AssertionError (expected)

```

尽管如此，我们必须记住，尽管广泛的测试可以使我们对程序的正确性有很高的信心，但它不能保证所有将来的执行都是正确的。 即使检查每个结果的运行时验证也只能保证*如果*产生结果，则结果将是正确的； 但不能保证将来的执行不会导致检查失败。 在撰写本文时，我*相信* `my_sqrt_fixed(x)`是$ \ sqrt {x} $的正确实现，但我不能100％确定。

使用Newton-Raphson方法，我们仍然有很大的机会实际*证明*实现是正确的：实现简单，数学上的理解很深。 las，只有少数域才是这种情况。 如果我们不想使用完整的正确性证明，我们进行测试的最佳机会是

1.  在几个精心选择的输入上测试程序； 和
2.  全面自动地检查结果。

这是我们在本课程其余部分中要做的：设计可帮助我们彻底测试程序的技术，以及可帮助我们检查程序状态正确性的技术。 请享用！

## 经验教训

*   测试的目的是执行一个程序，以便我们发现错误。
*   测试执行，测试生成和检查测试结果可以自动化。
*   测试是*不完整的*； 它不提供100％保证代码没有错误的保证。

## 后续步骤

从这里开始，您可以继续

*   [使用*模糊测试*测试带有随机输入的程序](Fuzzer.html)

享受阅读！

## 背景

关于软件测试和分析的工作很多。 对于这本书，我们很高兴推荐“软件测试和分析” [[Pezzè*等人*，2008年。](http://ix.cs.uoregon.edu/~michal/book/)]作为该领域的介绍； 其强大的技术重点非常适合我们的方法。 使用软件测试的全面方法（包括心理学和组织知识）的其他重要必读材料包括“软件测试的艺术” [ [Myers *等人*，2004。](https://dl.acm.org/citation.cfm?id=983238)]以及“ 软件测试技术” [ [Beizer *等人*，1990。](https://dl.acm.org/citation.cfm?id=79060)]。

## 练习

### 练习1：测试Shellsort

考虑 [Shellsort](https://en.wikipedia.org/wiki/Shellsort) 函数的以下实现，获取元素列表并（大概）对其进行排序。

```py
def shellsort(elems):
    sorted_elems = elems.copy()
    gaps = [701, 301, 132, 57, 23, 10, 4, 1]
    for gap in gaps:
        for i in range(gap, len(sorted_elems)):
            temp = sorted_elems[i]
            j = i
            while j >= gap and sorted_elems[j - gap] > temp:
                sorted_elems[j] = sorted_elems[j - gap]
                j -= gap
            sorted_elems[j] = temp

    return sorted_elems

```

第一个测试表明`shellsort()`可能实际起作用：

```py
shellsort([3, 2, 1])

```

```py
[1, 2, 3]

```

该实现使用*列表*作为参数`elems`（将其复制到`sorted_elems`）以及固定列表`gaps`。 列表的工作方式与其他语言的*数组*相似：

```py
a = [5, 6, 99, 7]
print("First element:", a[0], "length:", len(a))

```

```py
First element: 5 length: 4

```

`range()`函数返回一个可迭代的元素列表。 与上述实现一样，它通常与`for`循环结合使用。

```py
for x in range(1, 5):
    print(x)

```

```py
1
2
3
4

```

#### 第1部分：手动测试用例

现在，您的工作就是使用各种输入全面测试`shellsort()`。

首先，使用许多手动编写的测试用例设置`assert`语句。 选择测试用例，以便涵盖极端情况。 使用`==`比较两个列表。

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/Intro_Testing.ipynb#Exercises) to work on the exercises and see solutions.

#### 第2部分：随机输入

其次，创建随机列表作为`shellsort()`的参数。 使用以下辅助谓词来检查结果是否为（a）排序的，和（b）对原始结果的排列。

```py
def is_sorted(elems):
    return all(elems[i] <= elems[i + 1] for i in range(len(elems) - 1))

```

```py
is_sorted([3, 5, 9])

```

```py
True

```

```py
def is_permutation(a, b):
    return len(a) == len(b) and all(a.count(elem) == b.count(elem) for elem in a)

```

```py
is_permutation([3, 2, 1], [1, 3, 2])

```

```py
True

```

从一个随机列表生成器开始，使用`[]`作为空列表，并使用`elems.append(x)`将元素`x`追加到列表`elems`。 使用以上帮助器功能评估结果。 生成并测试1,000个列表。

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/Intro_Testing.ipynb#Exercises) to work on the exercises and see solutions.

### 练习2：二次求解器

给定一个等式$ ax ^ 2 + bx + c = 0 $，我们希望针对给定$ a $，$ b $和$ c $的值，找到$ x $的解。 以下代码应该使用等式$$ x = \ frac {-b \ pm \ sqrt {b ^ 2-4ac}} {2a} $$

```py
def quadratic_solver(a, b, c):
    q = b * b - 4 * a * c
    solution_1 = (-b + my_sqrt_fixed(q)) / (2 * a)
    solution_2 = (-b - my_sqrt_fixed(q)) / (2 * a)
    return (solution_1, solution_2)

```

```py
quadratic_solver(3, 4, 1)

```

```py
(-0.3333333333333333, -1.0)

```

但是，以上实现并不完整。 您可以触发

1.  除以零； 和
2.  违反`my_sqrt_fixed()`的前提。

如何做到这一点，又如何预防呢？

#### 第1部分：查找触发错误的输入

对于上述两种情况，请分别确定触发错误的`a`，`b`和`c`的值。

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/Intro_Testing.ipynb#Exercises) to work on the exercises and see solutions.

#### 第2部分：解决问题

适当地扩展代码，以便处理案件。 对于不存在的值，返回`None`。

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/Intro_Testing.ipynb#Exercises) to work on the exercises and see solutions.

#### 第3部分：奇数

通过随机输入发现这些条件的机会是什么？ 假设一个人每秒可以进行十亿次测试，那么一个错误被触发平均需要等待多长时间？

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/Intro_Testing.ipynb#Exercises) to work on the exercises and see solutions.