13

FWIW I'm using SimpleTest 1.1alpha.

I have a singleton class, and I want to write a unit test that guarantees that the class is a singleton by attempting to instantiate the class (it has a private constructor).

This obviously causes a Fatal Error:

Fatal error: Call to private FrontController::__construct()

Is there any way to "catch" that Fatal Error and report a passed test?

Stephen
  • 18,827
  • 9
  • 60
  • 98
  • There is no Unit in Simple Test ;) – Gordon Jan 20 '11 at 23:25
  • @Gordon I *see* the pun, but I don't get it. – Stephen Jan 20 '11 at 23:26
  • 1
    Maybe [this answer](http://stackoverflow.com/questions/4624093/what-unit-testing-in-php-to-start/4625909#4625909) can explain it – Gordon Jan 20 '11 at 23:30
  • Oldschool unit test frameworks are unfit for that. Write a [PHPT](http://qa.php.net/write-test.php) for that test and mingle it into a PHPUnit/SimpleTest case using a regex on the output. – mario Jan 20 '11 at 23:44

3 Answers3

13

No. Fatal error stops the execution of the script.

And it's not really necessary to test a singleton in that way. If you insist on checking if constructor is private, you can use ReflectionClass:getConstructor()

public function testCannotInstantiateExternally()
{
    $reflection = new \ReflectionClass('\My\Namespace\MyClassName');
    $constructor = $reflection->getConstructor();
    $this->assertFalse($constructor->isPublic());
}

Another thing to consider is that Singleton classes/objects are an obstacle in TTD since they're difficult to mock.

Andy Fleming
  • 7,655
  • 6
  • 33
  • 53
Mchl
  • 61,444
  • 9
  • 118
  • 120
5

Here's a complete code snippet of Mchl's answer so people don't have to go through the docs...

public function testCannotInstantiateExternally()
{
    $reflection = new \ReflectionClass('\My\Namespace\MyClassName');
    $constructor = $reflection->getConstructor();
    $this->assertFalse($constructor->isPublic());
}
PressingOnAlways
  • 11,948
  • 6
  • 32
  • 59
3

You can use a concept like PHPUnit's process-isolation.

This means the test code will be executed in a sub process of php. This example shows how this could work.

<?php

// get the test code as string
$testcode = '<?php new '; // will cause a syntax error

// put it in a temporary file
$testfile = tmpfile();
file_put_contents($testfile, $testcode);

exec("php $tempfile", $output, $return_value);

// now you can process the scripts return value and output
// in case of an syntax error the return value is 255
switch($return_value) {
    case 0 :
        echo 'PASSED';
        break;
    default :
        echo 'FAILED ' . $output;

}

// clean up
unlink($testfile);
hek2mgl
  • 152,036
  • 28
  • 249
  • 266
  • In practice, this **only** works for detecting syntax errors, because (1) a script does not usually survive isolated, (2) it's unfeasible to bootstrap an entire application like this, (3) it does not create a testing/repeatable context, (4) not having all the context set up could cause false fatal errors like _undefined function_. Thus, instead of executing `php $tempfile` it's better to execute `php --no-php-ini --syntax-check $tempfile`. http://php.net/manual/en/features.commandline.options.php – aercolino Jan 15 '15 at 19:17
  • Can you prove that? I don't think so – hek2mgl Jan 15 '15 at 21:12
  • Well, it "works" in the console because I see the fatal error. The $return is always 255 for the six uncatchable errors and always 0 otherwise. I think I need a shutdown handler to get to the error code. -- as for PHPUnit, even if I @runInSeparateProcess a single test causing a fatal error, it always appears as an 'E'. -- Your idea is interesting, and I upped you before. But to make it work, I think I need to dig into PHPUnit and write a patch or plugin. I wonder why nobody did it before. Is it unreasonable to expect a script to fail? – aercolino Jan 20 '15 at 23:44
  • Now I got your concerns. Will play around with PHPUnit a little bit, probably write some code and give you a feedback.. – hek2mgl Jan 22 '15 at 11:09
  • I've found there is a customizable isolation [template](https://github.com/sebastianbergmann/phpunit/blob/master/src/Util/PHP/Template/TestCaseMethod.tpl.dist). There we need to `register_shutdown_function('__phpunit_shutdown', $test, $result)`. `__phpunit_shutdown($test, $result)` (in case of error) only prints a serialized array like usual, but with an added `error` key set to `error_get_last()`. Then we can add support for an `@expectedShutdownError ` from `PHPUnit_Util_PHP::runTestJob`, which would call `processChildResult` with doctored arguments (mainly stderr = ''). – aercolino Jan 22 '15 at 17:08
  • Nice research! Unfortunately the file name of the template is hardcoded in the source code. Also the `@expectedShutdownError` would need to get implemented. We would need to fork PHPUnit in order to make the change happen (or am I missing something?) – hek2mgl Jan 22 '15 at 17:28
  • The isolation template is customizable [by default](https://github.com/sebastianbergmann/php-text-template/blob/master/Text/Template.php#L64). -- The rest is difficult to do from userland. We'd put a class in the middle of `PHPUnit_Util_PHP` and its descendants (`..._Default` and `..._Windows`) to override `runTestJob` as outlined above. Then, from the test case class, we'd override `run` so that [here](https://github.com/sebastianbergmann/phpunit/blob/master/src/Framework/TestCase.php#L691) we can instantiate our middle class. – aercolino Jan 22 '15 at 19:28
  • If you like so, let's create a fork! (... and discuss details there). Thinking the idea is good and it will not require too much changes (and work to do). – hek2mgl Jan 22 '15 at 19:32
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/69412/discussion-between-hek2mgl-and-ando). – hek2mgl Jan 22 '15 at 19:53