ChatGPT解决这个技术问题 Extra ChatGPT

Best practices to test protected methods with PHPUnit

I found the discussion on Do you test private method informative.

I have decided, that in some classes, I want to have protected methods, but test them. Some of these methods are static and short. Because most of the public methods make use of them, I will probably be able to safely remove the tests later. But for starting with a TDD approach and avoid debugging, I really want to test them.

I thought of the following:

Method Object as adviced in an answer seems to be overkill for this.

Start with public methods and when code coverage is given by higher level tests, turn them protected and remove the tests.

Inherit a class with a testable interface making protected methods public

Which is best practice? Is there anything else?

It seems, that JUnit automatically changes protected methods to be public, but I did not have a deeper look at it. PHP does not allow this via reflection.

Maybe he wants to test if a private property is being set correctly and the only way of testing using only the setter function is to make the private property public and checking the data
And so this is discussion-style and thus not constructive. Again :)
You can call it against the rules of the site, but just calling it "not constructive" is ... it's insulting.
@Visser, It's insulting himself ;)
It's entirely possible that he may not want to test the public class interface as such, but the interface that it presents to child classes(which can access protected methods)

u
uckelman

If you're using PHP5 (>= 5.3.2) with PHPUnit, you can test your private and protected methods by using reflection to set them to be public prior to running your tests:

protected static function getMethod($name) {
  $class = new ReflectionClass('MyClass');
  $method = $class->getMethod($name);
  $method->setAccessible(true);
  return $method;
}

public function testFoo() {
  $foo = self::getMethod('foo');
  $obj = new MyClass();
  $foo->invokeArgs($obj, array(...));
  ...
}

To quote the link to sebastians blog: "So: Just because the testing of protected and private attributes and methods is possible does not mean that this is a "good thing"." - Just to keep that in mind
I would contest that. If you don't need your protected or private methods to work, don't test them.
Just to clarify, you don't need to be using PHPUnit for this to work. It'll also work with SimpleTest or whatever. There's nothing about the answer that is dependent on PHPUnit.
You should not test protected/private members directly. They belong to the internal implementation of the class, and should not be coupled with the test. This makes refactoring impossible and eventually you don't test what needs to be tested. You need to test them indirectly using public methods. If you find this difficult, almost sure that there is a problem with the composition of the class and you need to separate it to smaller classes. Keep in mind that your class should be a black box for your test - you throw in something and you get something back, and that's all!
@gphilip To me, protected method is also a part of public api because any third party class can extend it and use it without any magic. So I think only private methods fall into the category of methods not to be directly tested. protected and public should be directly tested.
C
Community

teastburn has the right approach. Even simpler is to call the method directly and return the answer:

class PHPUnitUtil
{
  public static function callMethod($obj, $name, array $args) {
        $class = new \ReflectionClass($obj);
        $method = $class->getMethod($name);
        $method->setAccessible(true);
        return $method->invokeArgs($obj, $args);
    }
}

You can call this simply in your tests by:

$returnVal = PHPUnitUtil::callMethod(
                $this->object,
                '_nameOfProtectedMethod', 
                array($arg1, $arg2)
             );

This is a great example, thanks. The method should be public instead of protected, shouldn't it?
Good point. I actually use this method in my base class that I extend my test classes from, in which case this makes sense. The name of the class would be wrong here though.
I made the exact same piece of code based on teastburn xD
t
troelskn

You seem to be aware already, but I'll just restate it anyway; It's a bad sign, if you need to test protected methods. The aim of a unit test, is to test the interface of a class, and protected methods are implementation details. That said, there are cases where it makes sense. If you use inheritance, you can see a superclass as providing an interface for the subclass. So here, you would have to test the protected method (But never a private one). The solution to this, is to create a subclass for testing purpose, and use this to expose the methods. Eg.:

class Foo {
  protected function stuff() {
    // secret stuff, you want to test
  }
}

class SubFoo extends Foo {
  public function exposedStuff() {
    return $this->stuff();
  }
}

Note that you can always replace inheritance with composition. When testing code, it's usually a lot easier to deal with code that uses this pattern, so you may want to consider that option.


You can just directly implement stuff() as public and return parent::stuff(). See my response. It seems I'm reading things too quickly today.
You're right; It's valid to change a protected method into a public one.
I don't agree that it is a bad sign. Let's make a difference between TDD and Unit Testing. Unit testing should test private methods imo, since these are units and would benefit just in the same way as unit testing public methods benefit from unit testing.
Protected methods are part of the interface of a class, they are not simply implementation details. The whole point of protected members are so that subclassers (users in their own right) can use those protected methods inside class exstions. Those clearly need to be tested.
"The aim of a unit test, is to test the interface of a class..." Where did you get that conclusion from? In my opinion unit test is about testing the small unit of your code. Likely to be a function. Whether it's private, protected or public that's actually an implementation detail. Regardless of whether your unit is part of public interface or not, if it's tested it is protected from undesired changes which might break its logic. Have you ever had to fix a bug in a private method? If so, would the unit test help to avoid that bug? I can't believe people are debating this.
D
Dharman

I'd like to propose a slight variation to getMethod() defined in uckelman's answer.

This version changes getMethod() by removing hard-coded values and simplifying usage a little. I recommend adding it to your PHPUnitUtil class as in the example below or to your PHPUnit_Framework_TestCase-extending class (or, I suppose, globally to your PHPUnitUtil file).

Since MyClass is being instantiated anyways and ReflectionClass can take a string or an object...

class PHPUnitUtil {
    /**
     * Get a private or protected method for testing/documentation purposes.
     * How to use for MyClass->foo():
     *      $cls = new MyClass();
     *      $foo = PHPUnitUtil::getPrivateMethod($cls, 'foo');
     *      $foo->invoke($cls, $...);
     * @param object $obj The instantiated instance of your class
     * @param string $name The name of your private/protected method
     * @return ReflectionMethod The method you asked for
     */
    public static function getPrivateMethod($obj, $name) {
      $class = new ReflectionClass($obj);
      $method = $class->getMethod($name);
      $method->setAccessible(true);
      return $method;
    }
    // ... some other functions
}

I also created an alias function getProtectedMethod() to be explicit what is expected, but that one's up to you.


S
Sliq

I think troelskn is close. I would do this instead:

class ClassToTest
{
   protected function testThisMethod()
   {
     // Implement stuff here
   }
}

Then, implement something like this:

class TestClassToTest extends ClassToTest
{
  public function testThisMethod()
  {
    return parent::testThisMethod();
  }
}

You then run your tests against TestClassToTest.

It should be possible to automatically generate such extension classes by parsing the code. I wouldn't be surprised if PHPUnit already offers such a mechanism (though I haven't checked).


Heh... it seems I'm saying, use your third option :)
Yes, that is exactly my third option. I am pretty sure, that PHPUnit does not offer such a mechanism.
This won't work, you can't override a protected function with a public function with the same name.
I might be wrong, but I don't think this approach can work. PHPUnit (as far as I've ever used it) requires that your test class extend another class that provides the actual testing functionality. Unless there's a way around that I'm not sure I can see how this answer can be used. phpunit.de/manual/current/en/…
FYI this onl works for protected methods, not for private ones
s
sunwukung

I'm going to throw my hat into the ring here:

I've used the __call hack with mixed degrees of success. The alternative I came up with was to use the Visitor pattern:

1: generate a stdClass or custom class (to enforce type)

2: prime that with the required method and arguments

3: ensure that your SUT has an acceptVisitor method which will execute the method with the arguments specified in the visiting class

4: inject it into the class you wish to test

5: SUT injects the result of operation into the visitor

6: apply your test conditions to the Visitor's result attribute


D
David Harkness

You can indeed use __call() in a generic fashion to access protected methods. To be able to test this class

class Example {
    protected function getMessage() {
        return 'hello';
    }
}

you create a subclass in ExampleTest.php:

class ExampleExposed extends Example {
    public function __call($method, array $args = array()) {
        if (!method_exists($this, $method))
            throw new BadMethodCallException("method '$method' does not exist");
        return call_user_func_array(array($this, $method), $args);
    }
}

Note that the __call() method does not reference the class in any way so you can copy the above for each class with protected methods you want to test and just change the class declaration. You may be able to place this function in a common base class, but I haven't tried it.

Now the test case itself only differs in where you construct the object to be tested, swapping in ExampleExposed for Example.

class ExampleTest extends PHPUnit_Framework_TestCase {
    function testGetMessage() {
        $fixture = new ExampleExposed();
        self::assertEquals('hello', $fixture->getMessage());
    }
}

I believe PHP 5.3 allows you to use reflection to change the accessibility of methods directly, but I assume you'd have to do so for each method individually.


The __call() implementation works great! I tried to vote up, but I unset my vote until after I tested this method and now I'm not allowed to vote due to a time limit in SO.
The call_user_method_array() function is deprecated as of PHP 4.1.0 ... use call_user_func_array(array($this, $method), $args) instead. Note that if you are using PHP 5.3.2+ you can use Reflection to gain access to protected/private methods and attributes
@nuqqsa - Thanks, I updated my answer. I have since written a generic Accessible package that uses reflection to allow tests to access private/protected properties and methods of classes and objects.
This code doesn't work for me on PHP 5.2.7 -- the __call method does not get invoked for methods that the base class defines. I can't find it documented, but I'm guessing this behavior was changed in PHP 5.3 (where I've confirmed it works).
@Russell - __call() only gets invoked if the caller does not have access to the method. Since the class and its subclasses have access to the protected methods, calls to them won't go through __call(). Can you post your code that doesn't work in 5.2.7 in a new question? I used the above in 5.2 and only moved to using reflection with 5.3.2.
A
Anirudh Zala

I suggest following workaround for "Henrik Paul"'s workaround/idea :)

You know names of private methods of your class. For example they are like _add(), _edit(), _delete() etc.

Hence when you want to test it from aspect of unit-testing, just call private methods by prefixing and/or suffixing some common word (for example _addPhpunit) so that when __call() method is called (since method _addPhpunit() doesn't exist) of owner class, you just put necessary code in __call() method to remove prefixed/suffixed word/s (Phpunit) and then to call that deduced private method from there. This is another good use of magic methods.

Try it out.


makes it harder to find the references to the private method calls, no?
A
AlexeyP0708

Alternative.The code below is provided as an example. Its implementation can be much broader. Its implementation that will help you test private methods and replacing a private property .

    <?php
    class Helper{
        public static function sandbox(\Closure $call,$target,?string $slaveClass=null,...$args)
        {
            $slaveClass=!empty($slaveClass)?$slaveClass:(is_string($target)?$target:get_class($target));
            $target=!is_string($target)?$target:null;
            $call=$call->bindTo($target,$slaveClass);
            return $call(...$args);
        }
    }
    class A{
        private $prop='bay';
        public function get()
        {
            return $this->prop;    
        }
        
    }
    class B extends A{}
    $b=new B;
    $priv_prop=Helper::sandbox(function(...$args){
        return $this->prop;
    },$b,A::class);
    
    var_dump($priv_prop);// bay
    
    Helper::sandbox(function(...$args){
        $this->prop=$args[0];
    },$b,A::class,'hello');
    var_dump($b->get());// hello

А
Артем Вирский

You can use Closure as in the code below

<?php

class A
{
    private string $value = 'Kolobol';
    private string $otherPrivateValue = 'I\'m very private, like a some kind of password!';

    public function setValue(string $value): void
    {
        $this->value = $value;
    }

    private function getValue(): string
    {
        return $this->value . ': ' . $this->getVeryPrivate();
    }

    private function getVeryPrivate()
    {
        return $this->otherPrivateValue;
    }
}

$getPrivateProperty = function &(string $propName) {
    return $this->$propName;
};

$getPrivateMethod = function (string $methodName) {
    return Closure::fromCallable([$this, $methodName]);
};

$objA = new A;
$getPrivateProperty = Closure::bind($getPrivateProperty, $objA, $objA);
$getPrivateMethod = Closure::bind($getPrivateMethod, $objA, $objA);
$privateByLink = &$getPrivateProperty('value');
$privateMethod = $getPrivateMethod('getValue');

echo $privateByLink, PHP_EOL; // Kolobok

$objA->setValue('Zmey-Gorynich');
echo $privateByLink, PHP_EOL; // Zmey-Gorynich

$privateByLink = 'Alyonushka';
echo $privateMethod(); // Alyonushka: I'm very private, like a some kind of password!

D
Dan

I made a class for invoking easily private methods (static and non-static) for unit-testing purposes:

class MethodInvoker
{
    public function invoke($object, string $methodName, array $args=[]) {
        $privateMethod = $this->getMethod(get_class($object), $methodName);

        return $privateMethod->invokeArgs($object, $args);
    }

    private function getMethod(string $className, string $methodName) {
        $class = new \ReflectionClass($className);
        
        $method = $class->getMethod($methodName);
        $method->setAccessible(true);
    
        return $method;
    }
}

Example of usage:

class TestClass {
    private function privateMethod(string $txt) {
        print_r('invoked privateMethod: ' . $txt);
    }
}

(new MethodInvoker)->invoke(new TestClass, 'privateMethod', ['argument_1']);

关注公众号,不定期副业成功案例分享
Follow WeChat

Success story sharing

Want to stay one step ahead of the latest teleworks?

Subscribe Now