问题
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?
回答1:
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!
回答2:
I have unsuccessfully tried to use PHPUnit with Codeigniter. For example if I wanted to test my CI Models, I ran into the problem of how I will get an instance of that model as it somehow needs the whole CI framework to load it. Consider how you load a model for instance:
$this->load->model("domain_model");
The problem is that if you look at the super class for a load method you won't find it. It's not as straightforward if you're testing Plain Old PHP Objects where you can mock your dependencies easily and test functionality.
Hence, I have settled for CI's Unit testing class.
my apps grab a bunch of data from the database then display it in some sort of table.
If you're testing your controllers you're essentially testing the business logic (if you have) in there as well as the sql query that "grabs a bunch of data" from the database. This is already integration testing.
Best way to is to test the CI model first to test the grabbing of data --- this will be useful if you a have a very complicated query -- and then the controller next to test the business logic that is applied to the data grabbed by the CI Model. It is a good practice to test only one thing at a time. So what will you test? The query or the business logic?
I am assuming that you want to test the grabbing of data first, the general steps are
Get some test data and setup your database, tables etc.
Have some mechanism to populate the database with test data as well as delete it after the test. PHPUnit's Database extension has a way to do this although I don't know if that is supported by the framework you posted. Let us know.
Write your test, pass it.
Your test method might look like this:
// 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.
Just in case you want to use CI's unit testing class, here is a modified code snippet of one test I wrote using it:
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));
}
来源:https://stackoverflow.com/questions/8395147/how-do-i-use-phpunit-with-codeigniter