10
votes

I know it's possible to test private/protected methods with PHPUnit using reflection or other workarounds.

But most sources tell me that it's not best practice to write tests for private methods inside of a class.

You are supposed to test the class as if it were a "black box" — you just test for expected behavior by comparing inputs with outputs disregarding the internal mechanics. Writing tests for classes should also notify you to unused private methods, by showing lack of code coverage.

When I test my class and generate an HTML report, it shows the private methods as not covered by tests, even though the lines from which they are called are absolutely executed/covered. I know that the private methods are executed, because if they weren't the assertions on my class would not pass.

Is this expected behavior in PHPUnit? Can I strive for 100% coverage, while still testing private methods only indirectly?

Some simplified example code (using RestBundle in Symfony2):

class ApiController extends FOSRestController {

/*
 * @REST\View()
 * @REST\Get("/api/{codes}")
 */
public function getCodesAction($codes) {
    $view = new View();
    $view->setHeader('Access-Control-Allow-Origin', '*');
    $view->setData(array('type' => 'codes','data' => $this->_stringToArray($codes)));
    $view->setFormat('json')->setHeader('Content-Type', 'application/json');
    return $this->handleView($view);
}

private function _stringToArray($string){
    return explode('+',$string);
}

The public function shows as "covered", the private function is indirectly covered but shows colored red in PHPUnit reports.

Test:

class ApiControllerTest extends WebTestCase {

    public function test_getCodesAction(){
        $client = static::createClient();
        $client->request('GET', '/api/1+2+3');
        $this->assertContains('{"type": "codes", "data": [1,2,3]}', $client->getResponse()->getContent());
    }

}

This is just a silly example of course, I could just as well include the explode() right there in the public function; But the controllers I'm writing tests for contain much more intricate and re-usable private functions which transform data in more complex ways (but are still side-effect free).

1
Testing private methods isn't a problem by itself - doing such tests is not a bad practice. It's having private methods in the first place that's considered harmful, because they're hard to test. - Narf
@narf, I'd disagree with that. testing private methods makes for brittle tests and I would consider it a bad code smell if you think your only option is to resort to testing by reflection. Private methods are not harmful, they are a good way of organizing the code inside a class. - Sam Holder
@SamHolder Brittle is better than nothing ... there's no logic in having a test to be a bad practice and you can't convince me in that. :) Anyway, what, how and to what extent to test is largely an opinion-based topic, so I'd rather agree to disagree with you. :) - Narf
@narf you should test based on publicly verifiable behaviour. Private methods are an implementation detail. - Sam Holder
@narf shame that you are closed minded on the subject. You seem to be taking my position as 'don't test the code in private methods' when actually it is 'test all the code through public methods. if you happen to have put some of that code in private methods called by the public methods, that is fine. irrelevant, but fine'. Please see my answers here and here - Sam Holder

1 Answers

4
votes

In Phpunit you can specify the Covered Methods with special annotation, as descrived in the doc.

You can do something like this:

    class ApiControllerTest extends WebTestCase {

        /**
         * @covers ApiController::getCodesAction
         * @covers ApiController::_stringToArray
         */
        public function test_getCodesAction(){
            $client = static::createClient();
            $client->request('GET', '/api/1+2+3');
            $this->assertContains('{"type": "codes", "data": [1,2,3]}', $client->getResponse()->getContent());
        }

    }

Hope this help