53

Reading up and picking up on unit testing, trying to make sense of the following post on that explains the hardships of static function calls.

I don't clearly understand this issue. I have always assumed static functions were a nice way of rounding up utility functions in a class. For example, I often use static functions calls to initialise, ie:

Init::loadConfig('settings.php');
Init::setErrorHandler(APP_MODE); 
Init::loggingMode(APP_MODE);

// start loading app related objects ..
$app = new App();

// After reading the post, I now aim for this instead ...

$init = new Init();
$init->loadConfig('settings.php');
$init->loggingMode(APP_MODE);
 // etc ...

But, the few dozen tests I had written for this class are the same. I changed nothing and they still all pass. Am I doing something wrong?

The author of the post states the following:

The basic issue with static methods is they are procedural code. I have no idea how to unit-test procedural code. Unit-testing assumes that I can instantiate a piece of my application in isolation. During the instantiation I wire the dependencies with mocks/friendlies which replace the real dependencies. With procedural programing there is nothing to “wire” since there are no objects, the code and data are separate.

Now, I understand from the post that static methods create dependencies, but don't grasp intuitively why one cannot test the return value of a static method just as easily as a regular method?

I will be avoiding static methods, but I would of liked having an idea of WHEN static methods are useful, if at all. It seems from this post static methods are just about as evil as global variables and should be avoided as much as possible.

Any additional information or links on the subject would be greatly appreciated.

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

3 Answers3

59

Static methods themselves aren't harder to test than instance methods. The trouble arises when a method--static or otherwise--calls other static methods because you cannot isolate the method being tested. Here is a typical example method that can be difficult to test:

public function findUser($id) {
    Assert::validIdentifier($id);
    Log::debug("Looking for user $id");  // writes to a file
    Database::connect();                 // needs user, password, database info and a database
    return Database::query(...);         // needs a user table with data
}

What might you want to test with this method?

  • Passing anything other than a positive integer throws InvalidIdentifierException.
  • Database::query() receives the correct identifier.
  • A matching User is returned when found, null when not.

These requirements are simple, but you must also setup logging, connect to a database, load it with data, etc. The Database class should be solely responsible for testing that it can connect and query. The Log class should do the same for logging. findUser() should not have to deal with any of this, but it must because it depends on them.

If instead the method above made calls to instance methods on Database and Log instances, the test could pass in mock objects with scripted return values specific to the test at hand.

function testFindUserReturnsNullWhenNotFound() {
    $log = $this->getMock('Log');  // ignore all logging calls
    $database = $this->getMock('Database', array('connect', 'query');
    $database->expects($this->once())->method('connect');
    $database->expects($this->once())->method('query')
             ->with('<query string>', 5)
             ->will($this->returnValue(null));
    $dao = new UserDao($log, $database);
    self::assertNull($dao->findUser(5));
}

The above test will fail if findUser() neglects to call connect(), passes the wrong value for $id (5 above), or returns anything other than null. The beauty is that no database is involved, making the test quick and robust, meaning it won't fail for reasons unrelated to the test like network failure or bad sample data. It allows you to focus on what really matters: the functionality contained within findUser().

David Harkness
  • 35,992
  • 10
  • 112
  • 134
  • Hey thanks! Makes a lot more sense the way you laid it out. And the bonus, the example with mock objects. I have just begun trying my hand out at using these a few days ago, but still unclear on how to create various mock "states". Your example gives me a nice starting point for some objects I wanted to mock. – Stephane Gosselin May 11 '11 at 12:25
  • It's the first time I see an assertion called statically, is there a reason you dont use $this->asserNull()? I found it ironically amusing that your last line of code example is a static call in a post that deals with the pains of testing static calls. – Stephane Gosselin May 11 '11 at 12:37
  • 3
    @stefgosselin - Isn't it ironic, don't you think? :) PHPUnit's assertions are static methods defined in `PHPUnit_Framework_Assert` which `TestCase` inherits. While PHP allows you to call static methods using `->`, `self::` is shorter *and* more correct. – David Harkness May 11 '11 at 21:50
  • I think the lesson here is to use dependency injection. A dependency injection container allows a single global object, so it eliminates the need for static classes or singleton objects. What if php had a nice way of passing in dependencies that are static classes though? Always having to create a single object from a class seems wrong especially if the methods are all pure. – Rick Jolly Oct 02 '16 at 17:22
  • @RickJolly Because in OOP, you can't "depend on class". Class is an higher-level abstraction. Objects depend on other objects. You are being misled by wrong word usage. – hijarian Apr 10 '17 at 11:34
  • 1
    @hijarian static methods are called on class, not object and code can depend on them. So code can "depend on class". – Rick Jolly Apr 10 '17 at 15:41
  • @hijarian Since you can call static (and instance) methods dynamically—`$c = Foo::class; $m = 'bar'; $c::$m();` [(run)](https://ideone.com/PYXXeQ)—you can achieve the same effect. – David Harkness Apr 11 '17 at 15:44
  • @RickJolly We are talking about completely orthogonal concepts here. No point in arguing further. – hijarian Apr 12 '17 at 13:00
28

Sebastian Bergmann agrees with Misko Hevery and quotes him frequently:

Unit-Testing needs seams, seams is where we prevent the execution of normal code path and is how we achieve isolation of the class under test. Seams work through polymorphism, we override/implement class/interface and then wire the class under test differently in order to take control of the execution flow. With static methods there is nothing to override. Yes, static methods are easy to call, but if the static method calls another static method there is no way to override the called method dependency.

The main issue with static methods is that they introduce coupling, usually by hardcoding the dependency into your consuming code, making it difficult to replace them with stubs or mocks in your Unit-Tests. This violates the Open/Closed Principle and the Dependency Inversion Principle, two of the SOLID principles.

You are absolutely right that statics are considered harmful. Avoid them.

Check the links for additional information please.

Update: note that while statics are still considered harmful, the capability to stub and mock static methods has been removed as of PHPUnit 4.0

Dimitrios Desyllas
  • 9,082
  • 15
  • 74
  • 164
Gordon
  • 312,688
  • 75
  • 539
  • 559
1

I do not see any problem when testing static methods (at least none that doesn't exists in non-static methods).

  • Mock objects are passed to classes under test using dependency injection.
  • Mock static methods can be passed to classes under test using a suitable autoloader or manipulating the include_path.
  • Late static binding deals with methods calling static methods in the same class.
Oswald
  • 31,254
  • 3
  • 43
  • 68
  • When you say: _Mock static methods can be passed to classes under test using a suitable autoloader or manipulating the include_path._ Do you mean you simply load the needed class that holds your static method as is in your test? – Stephane Gosselin May 11 '11 at 12:40
  • @stef I wouldnt call that *simply*. Having to write a new class just to stub a method call and rewiring the include path to replace a harcoded dependency is a lot of effort which is less painfully solved with PHPUnit's internal mocking facilities and [test helpers](http://sebastian-bergmann.de/archives/885-Stubbing-Hard-Coded-Dependencies.html). If you want "simple" dont use statics and hardcoded dependencies in the first place (just like Hevery and Bergmann suggest). Have a look at the [Clean Code Talks](http://www.youtube.com/results?search_query=The+Clean+Code+Talks&aq=f) – Gordon May 11 '11 at 13:11
  • I understand better now. Although using the class of the SUT is possible -> I thought this is what you meant by hardwiring the include_path, it is not the proper approach.Thanks for the link btw! – Stephane Gosselin May 11 '11 at 18:02
  • Supplying a mock via altering the include path is a huge PITA and doesn't allow different mock abilities per test. Plus it means you can't test the real class in the same test session. And as you said, late static binding doesn't help for one class calling static methods in another which is more typically where you'd want to mock. – David Harkness May 11 '11 at 21:53