1

This is probably an easy one for some of you. I'm trying to test a protected method on a small DB connection class I have.

Relevant code is as follows:

class DbConnect{

/**
 *    Connexion  MSSQL local
 */
protected function localConnect($localconfig){
    $connectionInfo = array("UID" => $localconfig->uid, 
                            "PWD" =>$localconfig->pwd, 
                            "Database"=> $localconfig->DB);

   $this->localConnection = sqlsrv_connect($localconfig->serverName,
                                           $connectionInfo);

   if( $this->localConnection === false ){
       $sql_error = sqlsrv_errors();
       throw new DBException("Error in DB Connection.\r\n
                              SQL ERROR:" . $sql_error);
   }
}
}

To test the method, I had the bright idea (probably from a post here somewhere) to subclass and call from there. I created a subclass, right at bottom of my test file. I obviously could not override the visibility of the method to public, so decided another approach in the stub: declare a public method that calls the parent's protected localConnect method:

 class DBConnectStub extends DBconnect{

   public function callLocalConnect($localConfig){
        parent::localConnect($localConfig);
    }
}

My test now looks like this:

/**
 * @expectedException DBException
 */
public function test_localConnectError(){

  $localconfig = (object) array ( 'serverName' => 'nohost', 
                                   'uid' => 'nouid',
                                  'pwd' => 'noPwd',
                                  'DB' => 'noDB'

                         );  

  $db = DbConnectStub::getInstance($localconfig, array());
  $db->callLocalConnect($localConfig);
  unset($db);

}

The weird part, when I run the test, php spits out:

Fatal error: Call to undefined method DbConnect::callLocalConnect() in C:\tirelinkCRMsync\test \tirelinkCRMSync\DBConnectTest.php on line 82.

The object is properly instanciated, but why is the method not defined, surely there is a detail that has eluded me. Is this approach valid or is there a better way?

hakre
  • 193,403
  • 52
  • 435
  • 836
Stephane Gosselin
  • 9,030
  • 5
  • 42
  • 65

2 Answers2

4

I'm trying to test a protected method [...]

DON'T

It's as simple as that. Just don't. Protected methods are not part of the classes public API and therefore you should not make assumptions on how they work when trying to make sure your class works.

You should be able to change your code (implementation of your public functions) without adapting your tests. Thats what your tests are made for, so that you can change your code and you are sure that it still works. You can't be sure your code still works like before when you change your code and your tests at the same time!

See: Sebastian Bergmann -Testing Your Privates.html

So: Just because the testing of protected and private attributes and methods is possible does not mean that this is a "good thing".

and: Best practices to test protected methods with PHPUnit - on abstract classes

What this post also mentions is to just use

$method = new ReflectionMethod(
    'Foo', 'doSomethingPrivate'
);
$method->setAccessible(TRUE);

Which is easier than to create a subclass for every method you want to test.


Pedantic side node:

Imho it should be $this->localConnect and not parent::localConnect because parent:: is only for calling the same method of the parent class. (Doesn't matter much, just confusing, for me at least).

Community
  • 1
  • 1
edorian
  • 38,542
  • 15
  • 125
  • 143
  • Great advise here, I would pay attention to this. – John Cartwright May 25 '11 at 14:02
  • Thanks edorian, besides I hit another wall on this as the constructer is private anyhow so php chokes with: Fatal error: Call to private DbConnect::__construct() from context 'DBConnectStub' in C:\tirel inkCRMsync\test\tirelinkCRMSync\DBConnectTest.php on line 103 – Stephane Gosselin May 25 '11 at 14:13
  • Concerning the testing of private/protected methods, this singleton establishes 2 database connection to 2 different databases at same time. The client classes do not access directly these methods, but shouldn't they be tested by the initial developper to ensure they work as expected for client classes when deployed? – Stephane Gosselin May 25 '11 at 14:16
  • because you can't call private methods of objects you inherit from, yeah. Like it should work ;) `->setAccessible` will still work but $everything_i_said_above :) ------- Second comment: a) Don't use singletons (but we/SO told you that before). b) The class has a `"getInstance"` method that should return a DB object. The class doesn't do anything more than that so there is nothing more to test. If i understood you correctly. – edorian May 25 '11 at 14:18
  • Hey. ;o) If I was a genie and could snap my fingers and remove all singletons from this 3 year old legacy code written by 3-4 different people believe me it would of been done long ago. I am in midst of eliminating the 20+ object registry that is holding some of these singletons including the DbConnect (don't ask me why *sigh*) by implementing DI containers and doing plain object injection where DI container is overkill. I can't refactor everything in one go. I greatly appreciate your feedback, every word is in consideration! Understand I am doing the best I can with what I have, my friend. – Stephane Gosselin May 25 '11 at 14:36
  • I know applications just look like say do. But **focus on not testing stuff that you shouldn't be doing in the first place**. By testing protected & private methods you usually just waste time without all the cool fancy testing benefits. Time you could spend on rewriting stuff or making money ;) – edorian May 25 '11 at 16:08
  • 1
    I understand better now after re-reading and rethinkink it over. Thanks for insisting on this because it _now makes a lot sense!_ If I test all exposed methods on an object, I should _only_ care about what comes out of those public methods! Not *how* the public methods got to the result. If the public methods fail, then and only then should I peek into black-boxed code.I wanted to re-read the part on how testing privates affects code-coverage before calling this a day but can't find the post on that. Also came back to post you a *major* thank-you for giving me a better grasp on the subject. – Stephane Gosselin May 25 '11 at 16:23
0

This may be a stupid question, but did you override DbConnectStub::getInstance for it to return a Stub instance ?

class DBConnectStub extends DBconnect{
 public static function getInstance ()
 {
    //whatever process to create the instance (and not the parent method call that will return a DBConnect instance)
 } 

 public function callLocalConnect($localConfig){
    parent::localConnect($localConfig);
 }
}
Gérald Croës
  • 3,799
  • 2
  • 18
  • 20
  • Hey. Not stupid at all! I'm nearing the end of a 16 hour coding spree so this one got past me. I had the idea that using $c = `__CLASS__`; self::$instance = new $c($params); would set the class dynamically for subs but obviously it doesn't. Thanks for the clue-in my friend. – Stephane Gosselin May 25 '11 at 13:44