如何将 PHPUnit 与 CodeIgniter 一起使用?

2022-01-25 00:00:00 unit-testing php phpunit codeigniter

I have read and read articles on PHPUnit, SimpleTest, and other Unit Testing frameworks. They all sound so great! I finally got PHPUnit working with Codeigniter thanks to https://bitbucket.org/kenjis/my-ciunit/overview

Now my question is, how do I use it?

Every tutorial I see has some abstract use like assertEquals(2, 1+1) or:

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

That is great if I had a function that would output such a predictable string. Usually my apps grab a bunch of data from the database then display it in some sort of table. So how do I test Codeigniter's controllers?

I would like to do Test-Driven Development and I have read the tutorial on PHPUnits site, but once again the example seem so abstract. Most of my codeigniter functions are displaying data.

Is there a book or a great tutorial with a practical application and examples of PHPUnit testing?

解决方案

It seems you understand the basic structure/syntax of how to write tests and unit testing CodeIgniter code shouldn't be any different from testing non-CI code, so I want to focus on your underlying concerns/issues ...

I had similar questions not too long ago with PHPUnit. As someone without formal training I found that getting into the Unit Testing mindset seemed abstract and unnatural at first. I think the main reason for this -- in my case, and probably yours too from the question -- is that you haven't focused on REALLY working to separate the concerns in your code up till now.

The testing assertions seem abstract because most of your methods/functions likely perform several different discrete tasks. A successful testing mentality requires a change in how you think about your code. You should stop defining success in terms of "does it work?" Instead you should ask, "does it work, will it play well with other code, is it designed in a way that makes it useful in other applications and can I verify that it works?"

For example, below is a simplified example of how you've probably written code up to this point:

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

Exactly what's going on here isn't important. I'm trying to illustrate why this code is difficult to test:

  • It's using a singleton configuration class to generate values. The success of your function depends on values from the singleton, and how can you test that this function works correctly in complete isolation when you can't instantiate new config objects with different values? A better option might be to pass your function a $config argument that consists of a config object or array whose values you can control. This is broadly termed "Dependency Injection" and there are discussions of this technique all over the interwebs.

  • Notice the nested IF statements. Testing means you're covering every executable line with some sort of test. When you nest IF statements you're creating new branches of code that require a new test path.

  • Finally, do you see how this function, though it seems to be doing one thing (parsing the contents of a remote file) is actually performing several tasks? If you zealously separate your concerns your code becomes infinitely more testable. A much more testable way to do this same thing would be ...


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

As you can see, each of these class methods handles a particular function of the class that is easily testable. Did the remote file retrieval work? Did we find the the values we were trying to parse? Etc. All of a sudden those abstract assertions seem much more useful.

IMHO, the more you delve into testing the more you realize it's more about good code design and sensible architecture than simply making sure things work as expected. And here is where the benefits of OOP really start to shine. You can test procedural code just fine, but for a large project with interdependent parts testing has a way of enforcing good design. I know that may be troll bait for some procedural people but oh well.

The more you test, the more you'll find yourself writing code and asking yourself, "Will I be able to test this?" And if not, you'll probably change the structure then and there.

However, code need not be elementary to be testable. Stubbing and mocking allows you to test external operations the success or failure of which is entirely out of control. You can create fixtures to test database operations and pretty much anything else.

The more I test, the more I realize that if I'm having a tough time testing something it's most likely because I have an underlying design problem. If I straighten that out it usually results in all green bars in my test results.

Finally, here are a couple of links that really helped me to start thinking in a test-friendly fashion. The first one is a tongue-in-cheek list of what NOT to do if you want to write testable code. In fact, if you browse that entire site you'll find lots of helpful stuff that will help set you on the path to 100% code coverage. Another helpful article is this discussion of dependency injection.

Good luck!

相关文章