Drupal7模块开发 编写自动测试

在代码结构里面,有这许多种类的测试.其中两种是最受欢迎的, 单元测试功能测试.

单元测试是专注于那些不连续的代码. 在面向对象的代码里,单元测试的重点是在一个对象里面的每个方法上. 在程序代码中,单元测试专注于函数,甚至偶尔在全局变量中. 其目的仅仅是为了去确认每个单元都能按照预期的职责去运行.

大多数为Drupal编写的测试都不是单元测试.相反,他们是一些功能测试. 也就是说, 测试是为了验证在Drupal中插入的代码是否与预期的功能一样,并且与其他代码相吻合. 这是一个比单元测试更广泛的测试种类. 测试可以精确的测量更大的代码块的准确性.

测试模块已经默认包含在Drupal7中,但是他是没有被默认启用的.一旦他启用了,你就可以在 Configuration->Development 找到Testing的配置页面.

测试应该存在自己的文件中,就像模块的主代码存放在 modulename/modulename.module之中一样,测试文件也应该在
modulename/modulename.test中,这样测试框架变会自动去检索到它

起步:

就像在模块里的其他文件一样,测试文件也必须被写在.info文件当中.所有我们需要加入的文件都必须填写在 .info文件里面的 files 数组之中.

;$Id$
name = First
description = A first module.
core = 7.x
package = Drupal 7 Development
files[] = first.module
files[] = first.test

每当你的模块安装了之后,Drupal都会去缓存.info文件里面的内容.一旦你在.info文件里面添加了一个新的项目,你就必须去重新访问一下Drupal的模块页面,来强制Drupal重新解析.info文件.
(保险起见,这里建议 进入 Configuration->Development->Performance 中Clear all caches)

编写测试部件:

大多数部件遵循下面这个简单的样本:

  1. 创建一个继承DrupalWebTestCase 类
  2. 添加一个getInfo() 函数
  3. 在setUp() 方法里面设置一些必要的配置
  4. 以单词test开始来编写一些方法
  5. 在每个测试方法中,为测试的实际值来使用一个或多个声明

当我们在进行我们自己的测试的时候,我们将按照上面的每一步来测试.

<?php
/**
* @file
* Tests for the first module
*/
class FirstTestCase extends DrupalWebTestCase {
// Methods will go here.
}

这个测试部件继承了一个叫做 DrupalWebTestCase的基类. 这个基类为测试提供了许多工具.测试案例不必要陈列或者使用核心逻辑.
出于这两种原因,每个Drupal测试必须继承这个基类,或者继承另外一个继承了这个基类的其他类.
一旦我们声明了这个类,我们就可以创建我们的第一个方法,getInfo().

命名约定与类
Drupal的function命名都是以小写字母命名的,并且用下划线来分割单词,但是类和方法的命名却不是这样的.类的命名应该以大写字母开头的驼峰式写法,例如”CamelCase”,方法的命名则是以小写字母开头的驼峰试写法,例如 “camelCase”.下划线是不允许出现在类与类方法的名称当中的.

在之前,我们已经看到Drupal使用一些嵌套的数组来传递信息.例如我们之前创建的 first_block_info() 方法就是这样做的.
DrupalWebTestCase::getInfo() 方法里面,同样也是返回一个关联数组.此时返回的信息就是关于这个测试的.

<?php
/**
 * @file
 * Tests for the first module
 */
class FirstTestCase extends DrupalWebTestCase {

    public function setUp() {
      parent::setUp('first');
    }

    public static function getInfo() {
      return array(
        'name' => 'First module block functionality',
        'description' => 'Test blocks in the First module',
        'group' => 'First',
      );
    }
}

这个getInfo()方法返回的是一个带有三个项目的数组,这三个项目分别是:

  1. name: 测试的名称
  2. description:关于这个测试的一句话描述
  3. grup:这个测试所属于的组

这三个项目都是旨在为人类所读取的信息. 前面两个是纯粹的参考信息,第三个是将类似的测试罗列在相同的标题下.
getInfo()函数看上去是不起眼的,但你的测试必须得包含他,也就是说,如果没有这个函数,你的测试将无法被执行 .

设置测试:

<?php
/**
* @file
* Tests for the first module
*/
class FirstTestCase extends DrupalWebTestCase {
  public function setUp() {
    parent::setUp('first');
  }

  public function getInfo() {
    return array(
      'name' => 'First module block functionality',
      'description' => 'Test blocks in the First module.',
      'group' => 'First',
    );
  }
}

setUp()方法不是必须的,但当你一旦使用他的时候,你就必须写一行上述代码,就是如下:

parent::setUp('first');

这是告诉setUp()方法去启用存在于DrupalWebTestCase类里面的setUp()方法.
这么做的目的,是为了去初始化我们要测试的模块,这也意味着我们不需要去启动我们的模块,来进行测试代码的编写.
当你在编写你的测试部件的时候,你也可以在parent::setUp()请求下方编写你自己的配置信息,在之后我们将看到相关例子.

编写测试方法:

测试部件的大多数的方法是测试方法,他们仅仅是用来验证模块是否正常工作.就像你看到的一样,没有任何代码中调用了这些方法.
那么简单测试是如何调用我们的方法呢?就像Drupal的钩子约定一样,任何以test开头的方法是会被测试框架自动执行.

<?php
/**
 * @file
 * Tests for the first module
 */
class FirstTestCase extends DrupalWebTestCase {
    public function setUp() {
      parent::setUp('first');
    }
    public static function getInfo() {
      return array(
        'name' => 'First module block functionality',
        'description' => 'Test blocks in the First module',
        'group' => 'First',
      );
    }
    public function testBlockInfo() {
      $info = module_invoke('first', 'block_info');
      $this->assertEqual(1, count($info),
        t('Module define a block.'));
      $this->assertTrue(isset($info['list_modules']),
        t('Module list exists.'));
    }
    public function testBlockView() {
      $data = module_invoke('first', 'block_view','list_modules');
      $this->assertTrue(is_array($data),
        t('Block returns readerable array.'));
      $this->assertEqual(t('Enabled Modules'), $data['subject'],
        t('Subject is set'));
    }
}

上面的代码包含了两个测试的成员方法,就像他们的名字一样,每个方法都是负责之前创建的两个区块方法的测试.
他们做了三个事情:
首先,他运行了一个叫做 module_invoke()的函数,并且用$info来存储了他的结果.module_invoke()函数主要是用来请求特定的模块里面的特定的钩子. module_invoke()方法有两个参数: 模块的名称,钩子的名称. 类似与module_invoke('first','block_info')这样的请求就好比在请求first_block_info()钩子,这样做的好处就是能确保他被钩子系统所挂载.为达到我们的预期,我们做了一个成对的断言. 测试框架会有效的验证这些断言,如果方法正如预期,则通过,否则失败.
我们先看看 first_block_info() 测试方法
这里做了两个测试:

$this->assertEqual(1, count($info),
  t('Module defines a block.));
$this->assertTrue(isset($info['list_modules']),
  t('Module list exists.'));

(注意,这些代码都为了格式化,把一行分割成了两行)

每个断言都有$this->assertSOMETHING($conditions,$message) 这样的格式,其中 SOMETHING表示断言的种类,$conditions 是断言满足并且可以通过的条件,$message就是一个测试的描述. 在我们的第一个测试中,1count($info) 应该是相等的.断言方法都存在于 DrupalWebTestCase这个父类当中,这个类提供了十几种断言方法来使测试进行的更简单.

在例子中我们的断言是这样的:

  • $this->assertEqual():第一个值(已知)等于第二值(测试值)的断言.
  • $this->assertTrue():给定结果为TRUE的断言.
    第一个断言验证了我们在block_info()钩子里面定义的block,第二个断言验证了这个block的名字是否为list_modules.
    这样,当我们的测试进行的时候,我们就可以确定我们的info钩子是否返回了关于我们的简单的区块的信息.

再看看first_block_view()的测试.

public function testBlockView() {
    $data = module_invoke('first', 'block_view',
        'list_modules');
    $this->assertTrue(is_array($data),
        t('Block returns renderable array.'));
    $this->assertEqual(t('Enabled Modules'), $data['subject'],
        t('Subject is set'));
}

这次我们执行的是 block_view()钩子.并且再次执行了两个断言.第一个断言确认了first_block_view()返回的是否是一个数组.第二个断言验证了他的标题是否与我们预期的一样,为 Enabled Modules.

编写完毕,我们就可以去 Configuration-> Testing 里面去选中我们的测试,并且运行他了,看看结果吧,你的测试是否都通过了呢!