It's called unittest for a reason. So let's see the unit again:
public function dodo(array $a)
{
foreach ($a as $one) {
return $one;
}
return null;
}
Nothing fancy here, even ignore that the loop is not looping.
So the unit we got, what do we test? Two testcases you've identified, now you want a third test-case to test by invoking the unit with a non-array argument (I've identified even a fourth test-case, but that for later).
[How can I] call the dodo with an argument of a wrong type?
Well, obviously you call it with not an array, for example null
. You did already and it summoned a TypeError.
This should be obvious because of the array typehint on the parameter $a. You also made clear you're aware, and you want to keep it. Nothing special, I'd say.
But somehow this is not the full story for you, as you specifically note:
I want to write test with very high code paths coverage.
You did already. Strictly speaking, you already did too much. For testing the unit, provoking the TypeError is not necessary, as this is an internal detail of the PHP language. While you can technically double-check if that still works, but one benefit of writing code is to express or define something, according to the rules of execution. And you don't want to test if PHP is still working, but dodo(), the unit.
Calling the dodo() function with not an array as first parameter (or also calling it with no parameter at all) will always result in an error.
So with those tests you cover no paths of the dodo() code, but those are paths that do exist (mind how philosophical that statement is), so you increase the code paths coverage, but this can not be calculated, you can only do that, but you can not measure it with code coverage (compare with quantum mechanics).
So if you're interested in using code coverage as a tool, the right result is when testing for such errors (which should not be done, but anyway), to see no lines of dodo()s' code covered. So you can review that calling the dodo() function with a non-array is not entering the function. So you see nothing, but there is (was) something.
This should also give you the confidence that code coverage is working well.
So it looks to me that there is a misunderstanding, that only if there were more lines covered, more code paths would have been covered.
While this is normally true, this is not true when you test for preconditions that are errors. Additionally, this smells as you're testing for internals of the PHP language. Normally, such tests aren't done as unit-tests because they're out of scope, and they don't produce coverage (as you noticed).
Let's make this code path must be covered and more, more, more (better, better, better, faster, faster, faster, bigger, bigger, bigger, feed me I can't get enough) hitting the extreme:
Test with PHP versions the unit is not only an error when calling it, but already when PHP loads the definition of the unit.
Have you done that? I mean, testing for calling dodo() with a non-array is only one error condition and a pretty simple one, this doesn't even cover the case when dodo() can't be defined because of that array typehint.
This is the fourth test-case, and it is one you haven't even thought about: Covering the impossible code path.
Breakout Exercise: Find out with which PHP version the unit is a syntax error, if a Phpunit version exists that runs with that PHP version, Xdebug or a different code coverage obtaining mechanism exists and how code path coverage looks like for syntax errors.
Takeaway: You test your unit, not other internals of the language. That is because you use the language, that's enough test of it (the more civil formulations these produce are "don't test your privates" or "don't test third-party code").
And this is not because we couldn't do it, but because we're not interested in that. Increasing code path coverage for all this is neither giving you more insight, nor better feedback, nor better metrics.
public function dodo(array $a)
{
// @codeCoverageIgnoreStart
foreach ($a as $one) { // @codeCoverageIgnoreEnd
return $one;
}
return null;
}
Path coverage is broken for every foreach over an iterable. Your tests still may be fine and cover all paths, it is only the coverage is unable to report that properly.
public function dodo($a = null)
{
foreach ($a as $one) {
return $one;
}
return null;
}
Path coverage is not broken for every foreach over anything. Your tests, if they cover all paths, would do too much, you can use both array or iterable to define the parameter $a to make PHP TypeError already instead of branching internally into the error condition.
If you need error-free code, you can test with is_iterable():
public function dodo($a = null)
{
if (!is_iterable($a)) {
return $a;
}
foreach ($a as $one) {
return $one;
}
return null;
}
That this last example does not give you 100% path coverage should be the final hint, why 100% path coverage with the current implementation is nothing you want to obtain with code involving foreach.
The best suggestion I can give is not to write tests for the metrics, as it is so much the inverse of how (caution: this is subjective) tests should be written:
- first write the test
- the test must fail for the reason expected
- write the code, as little as necessary to make the test pass
- use the coverage to verify you have not written code that is not necessary (dead code)
- refactor
- commit
Coverage is last, read only and not the driver. Your tests are leading the code, and not the generated coverage leads the tests.
As this is in iterations, it is easy to shift over into having the coverage drive the tests, whenever that happens, we have to reconsider if we are still clear about what we are writing the tests for. A test-first approach may help to guide when hitting such problems.