如何使用PHPUnit和CodeIgniter?

cfh9epnr  于 2022-12-28  发布在  PHP
关注(0)|答案(2)|浏览(117)

我读了又读关于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测试的例子?

gjmwrych

gjmwrych1#

看起来您了解如何编写测试的基本结构/语法,并且单元测试CodeIgniter代码应该与测试非CI代码没有任何不同,因此我想重点讨论您的基本关注点/问题...
不久前我也有过类似的问题。作为一个没有受过正规培训的人,我发现进入单元测试的思维模式一开始似乎很抽象和不自然。我认为主要原因--在我的情况下,可能你也是如此--是你到目前为止还没有真正专注于分离代码中的关注点。
测试Assert看起来很抽象,因为你的大多数方法/函数可能执行几个不同的离散任务。成功的测试心态需要改变你对代码的看法。你不应该再用“它工作吗?”来定义成功,相反,你应该问,“它工作吗?它和其他代码一起运行好吗?它的设计方式是否使其在其他应用程序中有用?我能否验证它是否有效?”
例如,下面是一个简化的示例,说明到目前为止您可能是如何编写代码的:

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;
  }
}

正如您所看到的,这些类方法中的每一个都处理类的一个特定函数,这个函数很容易测试。远程文件检索工作了吗?我们找到了我们试图解析的值了吗?等等。突然之间,那些抽象Assert看起来有用多了。
恕我直言,你越是深入测试,你就越会意识到,好的代码设计和合理的体系结构比简单地确保事情按预期运行更重要。这里是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

ioekq8ef

ioekq8ef2#

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

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

问题是,如果你查看load方法的超类,你不会找到它。如果你测试Plain Old PHP对象,你可以很容易地模拟依赖关系并测试功能,这就不那么简单了。
因此,我选择了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.获取一些测试数据并设置数据库、表格等。
1.有一些机制用测试数据填充数据库,并在测试后删除它。PHPUnit's Database extension有一种方法来做到这一点,虽然我不知道是否支持的框架,你张贴。让我们知道。
1.写你的测试,通过它。
您的测试方法可能如下所示:

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

}

相关问题