代码覆盖率测试:从误传到现实

有一段时间,程序员获得的报酬直接与他们编写的代码行数挂钩。他们被视为工作在小隔间里的源代码生产机器,这导致他们仅将编程视为一天八小时的工作,做完之后一段时间就将其遗忘。

但时代变了。大部分的小隔间工作场所消失了,程序员开始爱上他们的工作。随着敏捷技术和软件工艺运动的出现,许多对程序员和编程有用的新工具随之出现。TDD正逐渐成为代码编写的实际方式,甚至向隔间世界最幽暗角落的程序员揭露了SCRUM和Kanban的秘密。

自动测试和测试驱动开发(TDD)是敏捷给我们程序员提供了一些关键技术。本文的主题是使用实现这些技术的工具产生测试代码覆盖。

定义

“在计算机科学中,代码覆盖是一种度量,用来描述程序源代码经过特定测试套件测试的程度。”~维基百科

上述定义,摘自维基百科,是描述代码覆盖含义的一种最简单方式。从根本上说,你的工程中包含大量产品代码,也有很多测试代码。测试代码执行产品代码,测试覆盖意味着你的产品代码有多少经过测试。

信息可以通过各种方式呈现,从简单的百分比到直观的图表,甚至在你最喜欢的集成开发环境中实时高亮显示。

让我们实际检查一下

我们使用PHP语言来阐明代码。此外,我们需要PHPUnit和XDebug来进行代码测试和覆盖数据收集。

源代码

下面是我们使用的源代码。你可以在附件文档中找到。

class WordWrap {

      public function wrap($string = '', $cols) {
            $string = trim($string);
            if (strlen($string) > $cols) {
                $lastSpaceIndex = strrpos(substr($string, 0, $cols), ' ');
                if ($lastSpaceIndex !== false && substr($string, $cols, 1) != ' ') {
                   return substr($string, 0, $lastSpaceIndex) . "n" . $this->wrap(substr($string, $lastSpaceIndex), $cols);
                } else {
                   return substr($string, 0, $cols) . "n" . $this->wrap(substr($string, $cols), $cols);
                }
             }

             return $string;
       }
}

上述代码包含了一个简单函数,将文本封装成每行指定数量字符。

测试代码

我们使用测试驱动开发(TDD)编写了下面的代码,代码的覆盖率为100%。这意味着运行测试,我们执行了每一行源代码。

require_once __DIR__ . '/../WordWrap.php';

class WordWrapTest extends PHPUnit_Framework_TestCase {

    function testItCanWrap() {
        $w = new WordWrap();

        $this->assertEquals('', $w->wrap(null, 0));
        $this->assertEquals('', $w->wrap('', 0));
        $this->assertEquals('a', $w->wrap('a', 1));
        $this->assertEquals("anb", $w->wrap('a b', 1));
        $this->assertEquals("a bnc", $w->wrap('a b c', 3));
        $this->assertEquals("anbcnd", $w->wrap('a bc d', 3));
    }
}

在命令行接口(CLI)中使用文本覆盖执行测试

获取覆盖数据的一种方法是在CLI(命令行接口)中执行测试,分析输出。对于这个例子,假设我们使用的是类UNIX系统(Linux,MacOS、FreeBSD等)。Windows用户需要稍微调整路径和可执行文件的名称,但应该相当类似。

让我们打开控制台程序,切换到测试文件夹目录下。然后,选择生成纯文本覆盖数据选项,运行phpunit程序。

phpunit --coverage-text=./coverage.txt ./WordWrapTest.php

大部分安装了XDebug系统应该能工作,但在某些情况下,可能会遇到与时区相关的错误。

PHP Warning: date(): It is not safe to rely on the system's timezone settings.
You are *required* to use the date.timezone setting or the date_default_timezone_set() function.
In case you used any of those methods and you are still getting this warning, you most likely
misspelled the timezone identifier. We selected the timezone 'UTC' for now, but please set
date.timezone to select your timezone. in phar:///usr/share/php/phpunit/phpunit.phar/
PHP_CodeCoverage-1.2.10/PHP/CodeCoverage/Report/Text.php on line 124

在php.ini文件中按要求设置很容易解决这个问题。你可以在这张列表中找到你应该指定的时区。我来自罗马尼亚,所以我使用以下设置:

date.timezone = Europe/Bucharest

现在,如果你再一次运行phpunit命令,你应该不会看到错误消息。取而代之的是显示测试结果。

PHPUnit 3.7.20 by Sebastian Bergmann.
..
Time: 0 seconds, Memory: 5.00Mb
OK (2 tests, 7 assertions)

覆盖数据将输出到指定文本文件中。

$ cat ./coverage.txt

Code Coverage Report
2014-03-02 13:48:11

Summary:
Classes: 100.00% (1/1)
Methods: 100.00% (1/1)
Lines: 2.68% (14/522)

WordWrap
Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 7/ 7)

让我们一点点分析。

  • 类:指多少类经过测试,有多少被覆盖。WordWrap是我们唯一的类。
  • 方法:和类一样。我们只有wrap()函数,没有其他函数。
  • 行:和上面相同,但是指代码行数。这里我们有很多行,因为概要部分包含PHPUnit自身的所有行。
  • 然后是每个类都有一个部分。在我们的例子中,只有唯一的WordWrap。每一部分都包含自身的函数和行数细节。

基于这些观察,我们可以得出结论,代码100%通过测试覆盖。和我们之前预期分析的覆盖率数据吻合。

生成HTML覆盖输出

只要改变PHPUnit的一个简单参数,可以生成工整的HTML输出。

$ mkdir ./coverage
$ phpunit --coverage-html ./coverage ./WordWrapTest.php

如果你查看coverage目录,你会发现里面有很多文件。因为文件太多,这里没有贴出文件列表。取而代之,我将在web浏览器中给你展示。

这和上述文本版本概要部分等同。我们可以通过链接放大并查看细节。

在IDE内覆盖

如果你的代码只能通过SHH或者网络访问远程服务器并在上面编译,前面的例子很有趣,也非常有用。但是,如果所有这些信息都在你的集成开发环境中是不是更棒?

如果你使用PHPStorm,一切都会在单击的过程中完成!选择执行覆盖测试,所有信息神奇般地立刻呈现。

覆盖信息将在你的集成开发环境的许多地方以不同的方式呈现:

  1. 测试覆盖百分比会在每一个目录和文件周围显示。
  2. 在编辑器中编写代码时,行号的左边的绿色或红色方块会标记每一行。绿色代表经过测试的行,红色代表未经测试的行。无实际代码的行(空行,只有花括号或圆括号,类或函数声明)不会有任何标记。
  3. 右边栏是文件浏览器,你可以按覆盖率快速浏览和排序文件。
  4. 在测试输出栏中,你会看到一行文本告诉你代码覆盖率已产生。

代码覆盖的虚构

这个强大的工具同时到达程序员和管理者手中,不可避免会出现一些误传。在程序员拒绝按照编写的代码行数获得酬劳或者管理者意识到实现一个系统非常简单之后,一些人开始按照代码覆盖率给程序员发放酬劳。代码覆盖越高意味着程序员越小心,对吗?这是一个误传。代码覆盖不是编写代码质量的度量。

有时程序员倾向于认为100%覆盖的代码没有bug。这是另一个误传。代码覆盖仅仅是告诉你你已经测试过每一行代码。代码覆盖是已执行代码行数的度量。它不是正确实现代码行数的度量。例如,写了一半的算法,使用定义一半的测试将会得到100%覆盖率。但这并不意味着该算法已完成或是能正常工作。

最后,实现系统非常容易。当然,如果你使用TDD,你自然会有很高的代码覆盖值。对整个工程而言,100%覆盖是不可能的。但是对于小的模块或类,获取100%的覆盖非常容易。就拿我们的源代码来说,假设你没有进行任何测试。执行所有代码的最简单测试是什么?

function testItCanWrap() {
    $w = new WordWrap();
    $this->assertEquals("a bnc", $w->wrap('a b c', 3));
    $this->assertEquals("anbcnd", $w->wrap('a bc d', 3));
}

就是这样。两句断言就可以实现完全覆盖。这不是我们想要的。这样的测试偏离描述性和完整性,太荒谬了。

代码覆盖的实现

代码覆盖是一种状态指示器,而不是衡量性能或正确性的单元。

代码覆盖率是给程序员参考的,而不是给管理者参考的。它是我们发现代码中问题的方法。该方法可以发现过时的,未测试的类。还可以发现未经测试执行可能导致问题的路径。

在实际项目中,代码覆盖率总是低于100%。取得完全覆盖是不可能的,如果取得,那也是非常罕见的。然而,为了获取98%的覆盖率,你必须将目标定在100%。其它任何目标都没有意义。

下面是Syneto的StorageOS配置程序的代码覆盖率。

整体覆盖率大约只有35%,但结果需要说明一下。大部分模块都处于绿色状态,超过70%的覆盖率。然而只有一个文件夹,Vmware,降低了整体平均值。这模块中有很多的类,类中只定义了通信API。那些类没有测试的必要。它们是由可靠代码自动生成。程序员知道这些代码,他们也知道如何解释结果。管理者可能会坚持测试,因为代码显示为红色状态,这对于不了解程序内部细节的人来说很可疑。测试这部分代码有意义吗?一点也没有!这将是一个无用的测试,除了占用几十秒宝贵的编译时间没有任何的优势。

结语

这就是我们所说的代码覆盖:对程序员来说,它是一个很棒的工具,可以高亮显示可能出现问题的信息源,对大部分管理者来说说却是被曲解的事实,成为限制和度量程序员行为的工具。正如使用的其他工具一样,代码覆盖正确使用很简单,但也容易误用。

加载余下内容▼

相关文章:

;