如何将 PHPUnit 与 CodeIgniter 一起使用?

2022-08-30 09:36:29

我读过和阅读有关PHPUnit,SimpleTest和其他单元测试框架的文章。它们听起来都很棒!我终于让PHPUnit与Codeigniter一起工作,这要归功于 https://bitbucket.org/kenjis/my-ciunit/overview

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

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

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

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

我想做测试驱动开发,我已经阅读了PHPUnits网站上的教程,但这个例子再次显得如此抽象。我的大多数代码图功能都显示数据。

有没有一本书或一个很好的教程,里面有PHPUnit测试的实际应用和例子?


答案 1

看来你理解了如何编写测试和单元测试的基本结构/语法 CodeIgniter 代码不应该与测试非 CI 代码有任何不同,所以我想把重点放在你潜在的关注点/问题上......

不久前,我在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

  • 请注意嵌套语句。测试意味着你用某种测试覆盖了每个可执行行。嵌套 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真正开始闪耀的地方。你可以很好地测试过程代码,但是对于一个具有相互依赖部件的大型项目,测试有一种方法可以强制执行良好的设计。我知道对于一些程序性的人来说,这可能是巨魔诱饵,但哦,好吧。

你测试得越多,你就越会发现自己在写代码,并问自己,“我能测试这个吗?如果没有,你可能会改变当时的结构。

但是,代码不必是基本的即可测试。存根和模拟允许您测试外部操作,其成功或失败完全失控。您可以创建夹具来测试数据库操作和几乎任何其他内容。

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

最后,这里有几个链接,它们真正帮助我开始以测试友好的方式思考。第一个是开玩笑的列表,如果你想编写可测试的代码,不要做什么。事实上,如果你浏览整个网站,你会发现很多有用的东西,这将有助于你走上100%代码覆盖率的道路。另一篇有用的文章是关于依赖注入的讨论

祝你好运!


答案 2

我尝试过将PHPUnit与Codeigniter一起使用,但没有成功。例如,如果我想测试我的CI模型,我遇到了如何获取该模型的实例的问题,因为它需要整个CI框架来加载它。例如,考虑如何加载模型:

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

问题是,如果您查看加载方法的超类,您将找不到它。如果您正在测试普通的旧PHP对象,那么情况就不那么简单了,您可以在其中轻松模拟依赖项并测试功能。

因此,我选择了CI的单元测试类

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

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

最好的方法是首先测试 CI 模型以测试数据的抓取---如果您有一个非常复杂的查询,这将非常有用 - 然后是控制器来测试应用于 CI 模型抓取的数据的业务逻辑。一次只测试一个事物是一种很好的做法。那么,您将测试什么呢?查询还是业务逻辑?

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

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

  2. 有一些机制来用测试数据填充数据库,并在测试后将其删除。PHPUnit的数据库扩展有一种方法可以做到这一点,尽管我不知道你发布的框架是否支持它。请告诉我们。

  3. 编写测试,通过测试。

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

// 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));

}

推荐