How is testing the registry pattern or singleton hard in PHP?
While it's true that "you can write and run tests aside of the actual program execution so that you are free to affect the global state of the program and run some tear downs and initialization per each test function to get it to the same state for each test.", it is tedious to do so. You want to test the TestSubject in isolation and not spend time recreating a working environment.
Example
class MyTestSubject
{
protected $registry;
public function __construct()
{
$this->registry = Registry::getInstance();
}
public function foo($id)
{
return $this->doSomethingWithResults(
$registry->get('MyActiveRecord')->findById($id)
);
}
}
To get this working you have to have the concrete Registry
. It's hardcoded, and it's a Singleton. The latter means to prevent any side-effects from a previous test. It has to be reset for each test you will run on MyTestSubject. You could add a Registry::reset()
method and call that in setup()
, but adding a method just for being able to test seems ugly. Let's assume you need this method anyway, so you end up with
public function setup()
{
Registry::reset();
$this->testSubject = new MyTestSubject;
}
Now you still don't have the 'MyActiveRecord' object it is supposed to return in foo
. Because you like Registry, your MyActiveRecord actually looks like this
class MyActiveRecord
{
protected $db;
public function __construct()
{
$registry = Registry::getInstance();
$this->db = $registry->get('db');
}
public function findById($id) { … }
}
There is another call to Registry in the constructor of MyActiveRecord. You test has to make sure it contains something, otherwise the test will fail. Of course, our database class is a Singleton as well and needs to be reset between tests. Doh!
public function setup()
{
Registry::reset();
Db::reset();
Registry::set('db', Db::getInstance('host', 'user', 'pass', 'db'));
Registry::set('MyActiveRecord', new MyActiveRecord);
$this->testSubject = new MyTestSubject;
}
So with those finally set up, you can do your test
public function testFooDoesSomethingToQueryResults()
{
$this->assertSame('expectedResult', $this->testSubject->findById(1));
}
and realize you have yet another dependency: your physical test database wasn't setup yet. While you were setting up the test database and filled it with data, your boss came along and told you that you are going SOA now and all these database calls have to be replaced with Web service calls.
There is a new class MyWebService
for that, and you have to make MyActiveRecord use that instead. Great, just what you needed. Now you have to change all the tests that use the database. Dammit, you think. All that crap just to make sure that doSomethingWithResults
works as expected? MyTestSubject
doesn't really care where the data comes from.
Introducing mocks
The good news is, you can indeed replace all the dependencies by stubbing or mock them. A test double will pretend to be the real thing.
$mock = $this->getMock('MyWebservice');
$mock->expects($this->once())
->method('findById')
->with($this->equalTo(1))
->will($this->returnValue('Expected Unprocessed Data'));
This will create a double for a Web service that expects to be called once during the test with the first argument to method findById
being 1. It will return predefined data.
After you put that in a method in your TestCase, your setup
becomes
public function setup()
{
Registry::reset();
Registry::set('MyWebservice', $this->getWebserviceMock());
$this->testSubject = new MyTestSubject;
}
Great. You no longer have to bother about setting up a real environment now. Well, except for the Registry. How about mocking that too. But how to do that. It's hardcoded so there is no way to replace at test runtime. Crap!
But wait a second, didn't we just say MyTestClass doesn't care where the data comes from? Yes, it just cares that it can call the findById
method. You hopefully think now: why is the Registry in there at all? And right you are. Let's change the whole thing to
class MyTestSubject
{
protected $finder;
public function __construct(Finder $finder)
{
$this->finder = $finder;
}
public function foo($id)
{
return $this->doSomethingWithResults(
$this->finder->findById($id)
);
}
}
Byebye Registry. We are now injecting the dependency MyWebSe… err… Finder?! Yeah. We just care about the method findById
, so we are using an interface now
interface Finder
{
public function findById($id);
}
Don't forget to change the mock accordingly
$mock = $this->getMock('Finder');
$mock->expects($this->once())
->method('findById')
->with($this->equalTo(1))
->will($this->returnValue('Expected Unprocessed Data'));
and setup() becomes
public function setup()
{
$this->testSubject = new MyTestSubject($this->getFinderMock());
}
Voila! Nice and easy and. We can concentrate on testing MyTestClass now.
While you were doing that, your boss called again and said he wants you to switch back to a database because SOA is really just a buzzword used by overpriced consultants to make you feel enterprisey. This time you don't worry though, because you don't have to change your tests again. They no longer depend on the environment.
Of course, you still you have to make sure that both MyWebservice and MyActiveRecord implement the Finder interface for your actual code, but since we assumed them to already have these methods, it's just a matter of slapping implements Finder
on the class.
And that's it. Hope that helped.
Additional Resources:
You can find additional information about other drawbacks when testing Singletons and dealing with global state in
- Testing Code That Uses Singletons
This should be of most interest, because it is by the author of PHPUnit and explains the difficulties with actual examples in PHPUnit.
Also of interest are:
- TotT: Using Dependency Injection to Avoid Singletons
- Singletons are Pathological Liars
- Flaw: Brittle Global State & Singletons
How to test PHP PDO Singleton Class?
I think you are misapplying the Singleton pattern here.
Nevertheless, testing Singletons is possible. Quoting Testing Code that uses Singletons
PHPUnit has a backup/restore mechanism for static attributes of classes.
This is yet another feature of PHPUnit that makes the testing of code that uses global state (which includes, but is not limited to, global and superglobal variables as well as static attributes of classes) easier.
Also see http://www.phpunit.de/manual/current/en/fixtures.html#fixtures.global-state
The
@backupStaticAttributes
annotation that is discussed in the section called “@backupStaticAttributes” can be used to control the backup and restore operations for static attributes. Alternatively, you can provide a blacklist of static attributes that are to be excluded from the backup and restore operations like this
So if you wanted to disable the backup, you'd do
class MyPdoTest extends PHPUnit_Framework_TestCase
{
protected $backupStaticAttributesBlacklist = array(
'dbConnection' => array('instance')
);
// more test code
}
Also have a look at the chapter on Database Testing
Is there a use-case for singletons with database access in PHP?
Okay, I wondered over that one for a while when I first started my career. Implemented it different ways and came up with two reasons to choose not to use static classes, but they are pretty big ones.
One is that you will find that very often something that you are absolutely sure that you'll never have more than one instance of, you eventually have a second. You may end up with a second monitor, a second database, a second server--whatever.
When this happens, if you have used a static class you're in for a much worse refactor than if you had used a singleton. A singleton is an iffy pattern in itself, but it converts fairly easily to an intelligent factory pattern--can even be converted to use dependency injection without too much trouble. For instance, if your singleton is gotten through getInstance(), you can pretty easily change that to getInstance(databaseName) and allow for multiple databases--no other code changes.
The second issue is testing (And honestly, this is the same as the first issue). Sometimes you want to replace your database with a mock database. In effect this is a second instance of the database object. This is much harder to do with static classes than it is with a singleton, you only have to mock out the getInstance() method, not every single method in a static class (which in some languages can be very difficult).
It really comes down to habits--and when people say "Globals" are bad, they have very good reasons to say so, but it may not always be obvious until you've hit the problem yourself.
The best thing you can do is ask (like you did) then make a choice and observe the ramifications of your decision. Having the knowledge to interpret your code's evolution over time is much more important than doing it right in the first place.
Best practice on PHP singleton classes
An example singleton classes in php:
Creating the Singleton design pattern in PHP5 : Ans 1 :
Creating the Singleton design pattern in PHP5 : Ans 2 :
Singleton is considered "bad practice".
Mainly because of this: How is testing the registry pattern or singleton hard in PHP?
why are singleton bad?
why singletons are evil?
A good approach: Dependency Injection
Presentation on reusability: Decouple your PHP code for reusability
Do you need a dependency injection container
Static methods vs singletons choose neither
The Clean Code Talks - "Global State and Singletons"
Inversion of Control Containers and the Dependency Injection pattern
Wanna read more? :
What are the disadvantages of using a PHP database class as a singleton?
Database abstraction class design using PHP PDO
Would singleton be a good design pattern for a microblogging site?
Modifying a class to encapsulate instead of inherit
How to access an object from another class?
Testing Code That Uses Singletons
A Singleton decision diagram (source):
Is DI the only solution to Singleton and/or static objects?
Logging is usually the example where static singletons are OK. You don't need to mock your logging anyway, do you?
How Bad Are Singletons?
Because it's relatively easy to work with singletons, and working without takes much more detailed planning of your application's structure. I asked a question about alternatives some time ago, and got interesting answers.
PHP global object
If you are trying to use the $database
object from inside a method of a class, you must use the global
keyword, so the $database
variable is visible from the method :
class User {
function myMethod() {
global $database;
// Work with $database
}
}
For more informations, take a look at the Variable scope section of the manual.
Another (better) solution, considering you are use a singleton, would be to get that object from the singleton :
class User {
function myMethod() {
$database = Database::Singleton();
// Work with $database
}
}
PHP Singleton design pattern inheritance error
Inheriting Singleton class
in PHP is difficult, event in PHP 7.0, but you can do this with some changes on your class to work.
first make your Singleton
class
to abstract
abstract class Singleton {
}
change your $instance
variable to array $instance(s)
private $instances = [];
Now change getInstance()
method like below
public static function getInstance() {
if (!isset(self::$instances[static::class]) {
self::$instances[static::class] = new static();
}
return self::$instances[static::class];
}
And change your test
remember now you can't call
Singleton:: getInstance()
due to abstract
class SingletonChild extends Singleton {
}
class SingletonChildTwo extends SingletonChild {
}
$obj = SingletonChild::getInstance();
$obj_two = SingletonChildTwo::getInstance();
var_dump($obj === SingletonChild::getInstance()); // true
var_dump($obj === $obj_two); // will -> false
Related Topics
Phpmyadmin Automatic Logout Time
How to Access a Property of an Object (Stdclass Object) Member/Element of an Array
PHP String Manipulation: Extract Hrefs
How to Remove the Leading Character from a String
Fastest Hash for Non-Cryptographic Uses
Sum Specific Values in a Multidimensional Array (Php)
Errorbag Is Always Empty in Laravel 5.2
Display Custom Order Meta Data Value in Email Notifications Woocommerce
MySQL and PHP - Insert Null Rather Than Empty String
Laravel Advanced Wheres How to Pass Variable into Function
How to Check If a Directory Exists? "Is_Dir", "File_Exists" or Both
Loadhtml Libxml_Html_Noimplied on an HTML Fragment Generates Incorrect Tags
Colorizing Windows Command Line Output from PHP
How to Compare 2 HTML Pages, and Output Only the Different Bits in Ruby or PHP