unit testing and Static methods
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()
.
Sebastian Bergmann agrees with Misko Hevery and quotes him frequently:
- Stubbing and Mocking Static Methods
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
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.