Practical symfony

第八天:单元测试

You are currently browsing
the website for symfony 1

Visit the Symfony2 website


About

You are currently reading "Practical symfony" which is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.

Jobeet Links

Master symfony

Be trained by SensioLabs experts (2 to 6 day sessions -- French or English).
trainings.sensiolabs.com

Books on symfony

Learn more about symfony with the official guides.
books.sensiolabs.com

L'audit Qualité par SensioLabs

200 points de contrôle de votre applicatif web.
audit.sensiolabs.com

Chapter Content

symfony中的测试

单元测试

lime测试框架

运行单元测试

测试slugify方法

为新功能添加测试

为调试bug添加测试

Propel单元测试

数据库配置

测试数据

测试JobeetJob

测试其他Propel类

打包测试

明天见

symfony training
Be trained by symfony experts
Feb 21: Köln (Getting Started with Symfony2 - English)
Feb 27: Köln (Mastering Symfony2 - English)
Mar 05: Köln (Web Development with Symfony2 - Deutsch)
Mar 05: Montreal (Web Development with Symfony2 - English)
Mar 05: Montreal (Getting Started with Symfony2 - English)
and more...

Search


powered by google
You are currently browsing "Practical symfony" in for the 1.2 version - Propel edition - Switch to ORM: - Switch to language:
Creative Commons License This work is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License.
This version of symfony is not maintained anymore.
If some of your projects still use this version, consider upgrading as soon as possible.
Practical symfony (Propel edition)
Support symfony!
Buy this book
or donate.
Buy Practical symfony (Propel edition) from amazon.com

过去两天,我们回顾我们过去5天学过的内容,增加和修改了一些功能。在这个过程中, 我们也接触到symfony一些高级功能。

今天我们将学习一些和前学到的、完全不同的内容:自动化测试。因为将包括很多内容, 我们需要花费2天的时间学习这些内容。

symfony中的测试

symfony有2种不同种类的自动化测试:单元测试(unit tests)和功能测试( functional tests)。

单元测试检验每个方法和函数是否正常工作,每个测试必须尽可能的独立。

功能测试检验整体程序运行过程是否正确。

symfony所有测试都在test/目录下,有两个子目录。一个存储单元测试文件 (test/unit/),另一个存储功能测试文件(test/functional/)。

今天的课程讲解单元测试,明天讲功能测试。

单元测试

写单元测试是网站开发中最艰难环节之一,也是最好的习惯之一。当网页开发者没有 真正检验自己的工作,就会出现问题: 我添加功能前写测试程序吗?我需要测试什么? 我的测试是否需要涵盖各种特殊情况?我如何确定所有内容都通过了测试?但通常一个 最基础的问题是:从哪开始?

symfony提倡的是一种实用主义的方式:有一点总比没有强。你有许多没有测试的代码吗? 没问题,你不需要一套完整测试套件,就可以从测试中获益。你可以在发现bug时,再开始 添加测试。久而久之,你的代码将变得更好,代码覆盖度将提高,你对代码将更有信心。 以实用主义的方式开始测试程序,不久之后你就会感到适应的。下面我们为新功能写测试。 我保证你很快就会上瘾的。

大部分测试库存在的问题是,巨大的学习成本。这也是symfony为什么提供非常简单的 测试库——lime的原因,它将让编写测试程序非常轻松。

Even if this tutorial describes the lime built-in library extensively, you can use any testing library, like the excellent PHPUnit library.

lime测试框架

所有使用lime框架进行的单元测试,都以相同的代码开始:

require_once dirname(__FILE__).'/../bootstrap/unit.php';
 
$t = new lime_test(1, new lime_output_color());

首先,引用unit.php引导文件进行简单的初始化。然后,创建实例化lime_test对象, 该对象第一个参数表示需要执行多个测试项。

如果最终执行测试的数量与该参数值不同,lime会输出一条警告信息。(例如 一个测试产生了php致命错误)。

测试程序调用方法或函数,并让其使用预定义值执行,然后将执行结果与预期的 结果相比较 以判断测试是否通过。

为更容易进行结果比较,lime_test对象提供了几个方法:

方法 描述
ok($test) 测试一个条件如果为true,测试通过。
is($value1, $value2) 比较两个值如果相等(==),测试通过。
isnt($value1, $value2) 比较两个值如果不相等,测试通过。
like($string, $regexp) 一个字符串与正则表达式匹配,测试通过。
unlike($string, $regexp) 一个字符串与正则表达式不匹配,测试通过。
is_deeply($array1, $array2) 检查两个数组有相同值

你可能想知道为什么lime定义这么多测试方法,因为所有的测试都可以通过ok() 一个方法实现。采取供选择的方法的好处在于,假如测试失败会有更明确的错误信息, 可以提高测试可读性。

lime_test对象也提供了其他方便的测试方法:

方法 描述
fail() 总是失败,用于测试异常。
pass() 总是通过,用于测试异常。
skip($msg, $nb_tests) 作为$nb_tests计数,用于条件测试。
todo() 作为一个测试的计数,用于测试仍然被写。

如果没有进行任何测试,comment($msg)方法最后会输出一条注释。

运行单元测试

所有的单元测试文件存储在test/unit/目录下。按约定,测试文件命名方式是”类名+Test”。 你可以按喜欢的方式组织目录中文件,但我们还是建议你仿照lib/的目录结构。

To illustrate unit testing, we will test the Jobeet class.

创建test/unit/JobeetTest.php文件,复制以下内容到文件中:

// test/unit/JobeetTest.php
require_once dirname(__FILE__).'/../bootstrap/unit.php';
 
$t = new lime_test(1, new lime_output_color());
$t->pass('This test always passes.');

你可以直接运行文件,来执行测试:

$ php test/unit/JobeetTest.php

或使用test:unit命令:

$ php symfony test:unit Jobeet

Tests on the command line

Windows命令行没有代码高亮显示。

测试slugify方法

让我们以Jobeet::slugify()单元测试,作为我们的开始。

我们在第5天创建slugify()方法来清理字符串,让它可以安全地包含在URL中。这个方法 包含一些基础的转化,如将非ASCII字符转换成(-)或将字符串转换为小写字母。

Input Output
Sensio Labs sensio-labs
Paris, France paris-france

用下面的代码替换测试文件中的内容:

// test/unit/JobeetTest.php
require_once dirname(__FILE__).'/../bootstrap/unit.php';
 
$t = new lime_test(6, new lime_output_color());
 
$t->is(Jobeet::slugify('Sensio'), 'sensio');
$t->is(Jobeet::slugify('sensio labs'), 'sensio-labs');
$t->is(Jobeet::slugify('sensio   labs'), 'sensio-labs');
$t->is(Jobeet::slugify('paris,france'), 'paris-france');
$t->is(Jobeet::slugify('  sensio'), 'sensio');
$t->is(Jobeet::slugify('sensio  '), 'sensio');

如果你仔细观察我们写到测试,你将注意到每行代码只测试一种情况。你必须记住这条规则: 一个测试项只测试一种情况。

现在可以运行测试文件。如果所有测试项通过,最后将显示“绿色条”表示通过,否则出现 “红色条”警告你有没通过的测试项。

slugify() tests

如果一个测试项测试失败,将输出一些提示信息,描述失败原因;但如果文件中有上百个 测试项,从中找出错误信息将是很难的一件事。

所有lime测试方法都有一个字符串作为最后一个参数,这个参数用来显示说明信息。用来 添加该测试项的描述信息。它也可以作为方法预期行的格式文档,让我们添加一些描述信息 到slugify测试文件:

require_once dirname(__FILE__).'/../bootstrap/unit.php';
 
$t = new lime_test(6, new lime_output_color());
 
$t->comment('::slugify()');
$t->is(Jobeet::slugify('Sensio'), 'sensio', '::slugify() converts all characters to lower case');
$t->is(Jobeet::slugify('sensio labs'), 'sensio-labs', '::slugify() replaces a white space by a -');
$t->is(Jobeet::slugify('sensio   labs'), 'sensio-labs', '::slugify() replaces several white spaces by a single -');
$t->is(Jobeet::slugify('  sensio'), 'sensio', '::slugify() removes - at the beginning of a string');
$t->is(Jobeet::slugify('sensio  '), 'sensio', '::slugify() removes - at the end of a string');
$t->is(Jobeet::slugify('paris,france'), 'paris-france', '::slugify() replaces non-ASCII characters by a -');

slugify() tests with messages

这些说明字符串在你试图指出该测试项的功能时,尤其有用。你可以观察出这些字符的 输出格式:以方法名开头,后接该方法应如何行为的描述。

为新功能添加测试

假如给slugify()传递一个空的字符串,它将返回一个空字符串。你可以添加这样的 测试,它也会通过测试。但是一个空字符串在URL中没有意义。让我们修改一下 slugify()方法,假如是空字符的话返回n-a字符串。

你可以先写测试代码,然后更新方法,或者相反。先写测试代码会给你信心:即将 加入的功能的确可以运行。

$t->is(Jobeet::slugify(''), 'n-a', '::slugify() converts the empty string to n-a');

This development methodology, where you first write tests then implement features, is known as Test Driven Development (TDD).

如果你现在运行测试,会显示一条红色的错误提示。因为这个新功能还没有添加到 slugify()方法中,或者已经添加的新功能没有通过测试。

现在更新Jobeet类,在开始处加入下面的条件语句:

// lib/Jobeet.class.php
static public function slugify($text)
{
  if (empty($text))
  {
    return 'n-a';
  }
 
  // ...
}

现在你必须更新测试计划中测试量,这个测试才可以通过。否则,你将看到一条类似的 提示:你计划进行6个测试,1个额外运行。更新计划测试量很重要,因为如果测试脚本 提前结束,你会马上知道。

为调试bug添加测试

让我们讨论这种情况,测试已经通过了不过一个用户向你提交了bug:一些招聘信息指向404页面。 你经过调查,错误的原因是这些招聘信息含有空公司名、职位或本地化字符。这不可能啊?因为你 从头到尾检查了整个数据库,并没有发现空字段。最后你发现,当字符串中全部都是非ASCII字符时, slugify()会将它转换成空字符串。然后你很高兴地打开Jobeet类修改代码…不过这不是个好注意。 在修复bug之前,我们应该先进行一下测试:

$t->is(Jobeet::slugify(' - '), 'n-a', '::slugify() converts a string that only contains non-ASCII characters to n-a');

slugify() bug

如我们所料,测试没有通过,现在修改slugify()方法,将空字符检查添加到后面:

static public function slugify($text)
{
  // ...
 
  if (empty($text))
  {
    return 'n-a';
  }
 
  return $text;
}

新的测试也通过。现在,虽然我们的测试代码的覆盖率已经达到了100%,但slugify() 仍然存在bug。

当你写测试代码的时候,你不可能考虑到所有的特殊情况,而且测试时它们也没有出现问题。 但是一旦你发现bug时,一定要在修复之前先写测试代码。这意味着随着时间的推移,你的 代码将变得越来越好。

Propel单元测试

数据库配置

因为需要数据库连接,所以Propel模型类的单元测试稍微复杂一些。你已经有了开发用的数据库, 但作为良好习惯,你应该创建一个专用的测试数据库。

在第1天的课程中,我们介绍过环境是改变程序设置一种手段。默认的,所有测试程序都运行在测试 环境中,所以我们需要给测试环境配置一个不同的数据库:

$ php symfony configure:database --env=test "mysql:host=localhost;dbname=jobeet_test" root mYsEcret

env选项告诉命令,这个数据库配置只用于测试(test)环境。我们在第3天使用这个命令时没有 带任何env选项,所以那个数据库配置被应用到所有环境。

如果你好奇,打开config/databases.yml配置文件,看看symfony改变依赖的环境是多么容易。

现在我们已经配置了数据库,现在用propel:insert-sql导入:

$ mysqladmin -uroot -pmYsEcret create jobeet_test
$ php symfony propel:insert-sql --env=test

测试数据

我们有了专用测试数据库,现在需要导入一些数据。第3天时候我们用propel:data-load导入过数据。但是对测试环境来说,这些数据需要在每次使用时自动加载。

propel:data-load内部使用sfPropelData类加载数据:

$loader = new sfPropelData();
$loader->loadData(sfConfig::get('sf_test_dir').'/fixtures');

sfConfig对象可以用来获取项目子目录的完整路径。使用它来引用路径, 要比硬编码路径更容易修改目录结构。

loadData()方法将一个目录或文件作为第一个参数。也可以用包含多个目录、文件的数组作为参数。

我们已经在data/fixtures/目录下创建了一些初始数据。单元测试和功能测试使用的初始化数据 保存test/fixtures/目录下。

现在复制data/fixtures/文件到test/fixtures/目录中。

测试JobeetJob

我们为JobeetJob模型类添加一些单元测试。

我们所有的Propel单元测试将以相同的代码开始,在test/bootstrap/下创建Propel.php文件:

// test/bootstrap/Propel.php
include(dirname(__FILE__).'/unit.php');
 
$configuration = ProjectConfiguration::getApplicationConfiguration( 'frontend', 'test', true);
 
new sfDatabaseManager($configuration);
 
$loader = new sfPropelData();
$loader->loadData(sfConfig::get('sf_test_dir').'/fixtures');

这些代码的功能显而易见:

只有存在需要运行的SQL语句时,Propel才会连接数据库。

现在万事俱备,我们可以开始测试JobeetJob类啦。

首先,我们需要创建在test/unit/model目录下JobeetJobTest.php文件:

// test/unit/model/JobeetJobTest.php
include(dirname(__FILE__).'/../../bootstrap/Propel.php');
 
$t = new lime_test(1, new lime_output_color());

然后,我们给getCompanySlug()添加测试:

$t->comment('->getCompanySlug()');
$job = JobeetJobPeer::doSelectOne(new Criteria());
$t->is($job->getCompanySlug(), Jobeet::slugify($job->getCompany()), '->getCompanySlug() return the slug for the company');

注意,我们值测试getCompanySlug()方法生成slug是否正确,因为其它测试项刚才已经测试过了。

save()测试,稍微有些复杂:

$t->comment('->save()');
$job = create_job();
$job->save();
$expiresAt = date('Y-m-d', time() + 86400 * sfConfig::get('app_active_days'));
$t->is($job->getExpiresAt('Y-m-d'), $expiresAt, '->save() updates expires_at if not set');
 
$job = create_job(array('expires_at' => '2008-08-08'));
$job->save();
$t->is($job->getExpiresAt('Y-m-d'), '2008-08-08', '->save() does not update expires_at if set');
 
function create_job($defaults = array())
{
  static $category = null;
 
  if (is_null($category))
  {
    $category = JobeetCategoryPeer::doSelectOne(new Criteria());
  }
 
  $job = new JobeetJob();
  $job->fromArray(array_merge(array(
    'category_id'  => $category->getId(),
    'company'      => 'Sensio Labs',
    'position'     => 'Senior Tester',
    'location'     => 'Paris, France',
    'description'  => 'Testing is fun',
    'how_to_apply' => 'Send e-Mail',
    'email'        => 'job@example.com',
    'token'        => rand(1111, 9999),
    'is_activated' => true,
  ), $defaults), BasePeer::TYPE_FIELDNAME);
 
  return $job;
}

每次添加测试,不要忘记更新lime_test构造方法中的计划测试量。对于JobeetJobTest你需要 将它从1改为3

测试其他Propel类

你现在可以为其它Propel类添加测试,因为你现在已经习惯了编写单元测试的过程, 它非常简单。你可以通过SVN得到我们今天初始化数据和相关的单元测试(release_day_08标签下)。

打包测试

可以用test:unit命令(task)执行全部单元测试:

$ php symfony test:unit

命令输出每个测试文件是否通过:

Unit tests harness

If the test:unit task returns a "dubious status" for a file, it indicates that the script died before end. Running the test file alone will give you the exact error message.

明天见

虽然测试程序非常重要,但可能有些人跳过了今天的课程。我很高兴你没有。

当然,学习symfony意味着要学习它提供的所有好的功能,同时也要学习基本的开发原理(philosophy) 和良好的编程习惯(best practices),测试就是其中之一。或快或慢,单元测试会为你节省开发时间。 让你对自己的代码有坚定信心,不再害怕重构代码。单元测试是一个安全守卫,它会在 你出现问题时提醒你。symfony框架本身的测试超过9000条。

明天我们为jobcategory模块写一些功能测试。在这之前花些时间,给Jobeet模型类 写更多的单元测试。

第九天:功能测试 »
« 第七天:创建分类页

Questions & Feedback

If you find a typo or an error, please register and open a ticket.

If you need support or have a technical question, please post to the official user mailing-list.