如何将PHPUnit与CodeIgniter一起使用?

时间:2011-12-06 03:54:55

标签: php unit-testing codeigniter phpunit

我已阅读和阅读有关PHPUnit,SimpleTest和其他单元测试框架的文章。它们听起来都很棒!由于https://bitbucket.org/kenjis/my-ciunit/overview

,我终于让PHPUnit与Codeigniter合作

现在我的问题是,我该如何使用它?

我看到的每个教程都有一些抽象用法,如assertEquals(2, 1+1)或:

public function testSpeakWithParams()
{
    $hello = new SayHello('Marco');
    $this->assertEquals("Hello Marco!", $hello->speak());
}

如果我有一个输出这样一个可预测字符串的函数,那就太好了。通常我的应用程序从数据库中获取大量数据,然后在某种表中显示它。那么我该如何测试Codeigniter的控制器呢?

我想做测试驱动开发,我已经阅读了PHPUnits网站上的教程,但是这个例子再一次看起来很抽象。我的大多数codeigniter函数都在显示数据。

是否有一本书或一本有实际应用的精彩教程和PHPUnit测试的例子?

2 个答案:

答案 0 :(得分:94)

您似乎理解了如何编写测试和单元测试的基本结构/语法CodeIgniter代码与测试非CI代码不应该有任何不同,所以我想专注于您的潜在关注点/问题... < / p>

我不久前和PHPUnit有过类似的问题。作为一个没有经过正式培训的人,我发现进入单元测试心态似乎是抽象的,不自然的。我认为这个的主要原因 - 在我的情况下,也可能是你的问题 - 是你没有专注于真的努力将代码中的问题分开到现在为止。

测试断言似乎是抽象的,因为大多数方法/函数可能执行几个不同的离散任务。成功的测试心态需要改变您对代码的看法。你应该停止以“它有效吗?”来定义成功。相反,你应该问,“它是否有效,它是否能与其他代码一起发挥作用,它是否以一种在其他应用程序中有用的方式设计,我可以验证它是否有效吗?”

例如,下面是一个简化示例,说明您到目前为止编写代码的方式:

function parse_remote_page_txt($type = 'index')
{
  $remote_file = ConfigSingleton::$config_remote_site . "$type.php";
  $local_file  = ConfigSingleton::$config_save_path;

  if ($txt = file_get_contents($remote_file)) {
    if ($values_i_want_to_save = preg_match('//', $text)) {
      if (file_exists($local_file)) {
        $fh = fopen($local_file, 'w+');
        fwrite($fh, $values_i_want_to_save);
        fclose($fh);
        return TRUE;
      } else {
        return FALSE;
      }
  } else {
    return FALSE;
  }  
}

这里究竟发生了什么并不重要。我试图说明为什么这段代码难以测试:

  • 它使用单例配置类来生成值。函数的成功取决于单例的值,当你无法用不同的值实例化新的配置对象时,如何测试这个函数在完全隔离中是否正常工作?更好的选择可能是为您的函数传递一个$config参数,该参数由您可以控制其值的配置对象或数组组成。这被广泛称为“Dependency Injection”,并且在整个互联网上讨论了这种技术。

  • 请注意嵌套的IF语句。测试意味着您通过某种测试覆盖每个可执行行。当您嵌套IF语句时,您正在创建需要新测试路径的新代码分支。

  • 最后,你看到这个函数,虽然看起来做了一件事(解析远程文件的内容)实际上是在执行几个任务吗?如果您热心地分离您的疑虑,您的代码将变得无比可测试。一个更可行的方法来做同样的事情就是......


class RemoteParser() {
  protected $local_path;
  protected $remote_path;
  protected $config;

  /**
   * Class constructor -- forces injection of $config object
   * @param ConfigObj $config
   */
  public function __construct(ConfigObj $config) {
    $this->config = $config;
  }

  /**
   * Setter for local_path property
   * @param string $filename
   */
  public function set_local_path($filename) {
    $file = filter_var($filename);
    $this->local_path = $this->config->local_path . "/$file.html";
  }

  /**
   * Setter for remote_path property
   * @param string $filename
   */
  public function set_remote_path($filename) {
    $file = filter_var($filename);
    $this->remote_path = $this->config->remote_site . "/$file.html";
  }

  /**
   * Retrieve the remote source
   * @return string Remote source text
   */
  public function get_remote_path_src() {
    if ( ! $this->remote_path) {
      throw new Exception("you didn't set the remote file yet!");
    }
    if ( ! $this->local_path) {
      throw new Exception("you didn't set the local file yet!");
    }
    if ( ! $remote_src = file_get_contents($this->remote_path)) {
      throw new Exception("we had a problem getting the remote file!");
    }

    return $remote_src;
  }

  /**
   * Parse a source string for the values we want
   * @param string $src
   * @return mixed Values array on success or bool(FALSE) on failure
   */
  public function parse_remote_src($src='') {
    $src = filter_validate($src);
    if (stristr($src, 'value_we_want_to_find')) {
      return array('val1', 'val2');
    } else {
      return FALSE;
    }
  }

  /**
   * Getter for remote file path property
   * @return string Remote path
   */
  public function get_remote_path() {
    return $this->remote_path;
  }

  /**
   * Getter for local file path property
   * @return string Local path
   */
  public function get_local_path() {
    return $this->local_path;
  }
}

如您所见,这些类方法中的每一个都处理可轻松测试的类的特定函数。远程文件检索是否有效?我们找到了我们试图解析的值吗?等等。突然间,这些抽象断言似乎更有用。

恕我直言,你越是深入研究测试的越多,你就会意识到它更多地是关于良好的代码设计和合理的架构,而不仅仅是确保事情按预期工作。在这里,OOP的好处真正开始闪耀。您可以很好地测试过程代码,但对于具有相互依赖的部件测试的大型项目,可以实现良好的设计。我知道这对于一些程序性人来说可能是巨魔诱饵但是很好。

您测试的越多,您发现自己编写代码并问自己的能力就越高,“我能够测试这个吗?”如果没有,你可能会改变那里的结构。

然而,代码不需要是可测试的基本代码。 Stubbing and mocking允许您测试外部操作,其成功或失败完全失控。您可以创建fixtures来测试数据库操作以及其他任何内容。

我测试的越多,我就越发现如果我在测试某些东西时遇到困难,那很可能是因为我有一个潜在的设计问题。如果我把它拉直,通常会导致测试结果中的所有绿条。

最后,这里有几个链接真正帮助我开始以一种考试友好的方式思考。第一个是a tongue-in-cheek list of what NOT to do if you want to write testable code。事实上,如果你浏览整个网站,你会发现许多有用的东西,这将有助于你在100%代码覆盖率的道路上。另一篇有用的文章是discussion of dependency injection

祝你好运!

答案 1 :(得分:2)

我尝试将PHPUnit与Codeigniter一起使用失败了。例如,如果我想测试我的CI模型,我遇到了如何获取该模型的实例的问题,因为它以某种方式需要整个CI框架来加载它。考虑如何加载模型,例如:

$this->load->model("domain_model");

问题是,如果你看一下加载方法的超类,你将找不到它。如果您正在测试Plain Old PHP Objects,那么您可以轻松地模拟您的依赖项并测试功能,这并不是那么简单。

因此,我已经选择了CI's Unit testing class

my apps grab a bunch of data from the database then display it in some sort of table.

如果您正在测试控制器,那么您实际上是在测试业务逻辑(如果有的话)以及从数据库中“抓取一堆数据”的SQL查询。这已经是集成测试了。

最好的方法是首先测试CI模型以测试数据的获取 - 如果你有一个非常复杂的查询,这将是有用的 - 然后控制器接下来测试应用于的业务逻辑CI模型抓取的数据。 一次只测试一件事是一种好习惯。那么你将测试什么?查询或业务逻辑?

我假设你想先测试抓取数据,一般步骤是

  1. 获取一些测试数据并设置数据库,表格等。

  2. 有一些机制用测试数据填充数据库,并在测试后删除它。 PHPUnit's Database extension有一种方法可以做到这一点,虽然我不知道你发布的框架是否支持它。让我们知道。

  3. 写下你的测试,传递它。

  4. 您的测试方法可能如下所示:

    // At this point database has already been populated
    public function testGetSomethingFromDB() {
        $something_model = $this->load->model("domain_model");
        $results = $something_model->getSomethings();
        $this->assertEquals(array(
           "item1","item2"), $results);
    
    }
    // After test is run database is truncated. 
    

    如果你想使用CI的单元测试类,这里是我用它编写的一个测试的修改代码片段:

    class User extends CI_Controller {
        function __construct() {
            parent::__construct(false);
            $this->load->model("user_model");
            $this->load->library("unit_test");
        }
    
    public function testGetZone() {
                // POPULATE DATA FIRST
        $user1 = array(
            'user_no' => 11,
            'first_name' => 'First',
            'last_name' => 'User'
        );
    
        $this->db->insert('user',$user1);
    
                // run method
        $all = $this->user_model->get_all_users();
                // and test
        echo $this->unit->run(count($all),1);
    
                // DELETE
        $this->db->delete('user',array('user_no' => 11));
    
    }