Let's say I have this very basic class:
class MyClass {
private $_counters = [
'counter1' => 0,
];
public function plus($counter) {
$this->_counters[$counter]++;
}
}
Now there's a change to the code so that it doesn't break if the $counter does not exist:
public function plus($counter) {
if (array_key_exists($counter, $this->_counters)) {
$this->_counters[$counter]++;
}
}
How would I go about testing this for a possible regression that results in an "undefined index" error being thrown?
Would simply calling the function plus() with a non-existing $counter be enough? When all is good nothing happens but if a regression removed the array_key_exists() check for example, the "undefined index" would be thrown.
I'm quite new to unit testing so am I missing a better way to test this situation? It feels a bit weird to write a testcase without asserting something.
Create a test that calls plus() with something that doesn't exist like foo as you stated.
Though testing that an undefined index notice isn't thrown isn't really what you are testing. This specifies that your class contains all the counters in an array. This is an implementation detail of your class that your tests shouldn't care about.
You could write the class such that each of the counters is a property of the class instead. Really want you want to decide is what should happen when an counter that doesn't exist is used in the plus method.
Your tests should test for behaviors of the class. So for non-existent counters you will need to decide what should happen. An exception is thrown, a message logged or nothing at all.
Related
Sorry for the confused title, I wasn't able to find a better one.
I'm on PHP/5.6.14, I have this code:
class Base
{
private $foo; <--- NOTE
public function __construct()
{
$this->foo = "base foo";
}
public final function getFoo()
{
return $this->foo;
}
}
class Derived extends Base
{
public $foo; <--- NOTE
public function __construct($type)
{
parent::__construct();
$this->foo = "derived foo";
$this->somethingUndefined = "dynamically declared"; <--- NOTE
}
}
$base = new Base();
var_dump($base->getFoo());
$derived = new Derived(0);
var_dump($derived->getFoo());
$clonedDerived = clone $derived; <--- NOTE
var_dump($clonedDerived->getFoo());
Running the last getFoo() gives me:
PHP Notice: Undefined property: Derived::$foo in C:..\test.php on
line *the line where getFoo() is implemented in the base class*.
If I put this three conditions together I get the notice:
same public/private field name
having one or more dynamically declared field in the derived class
working on a clone of an instance of the derived class
Removing one or more of them makes the notice go away.
What's going on here? $foo is not static, and it's not undefined...
I'm using a custom error_handler to turn every E_ALL to an exception and can't really ignore this...
EDIT:
This is just an example I wrote to reproduce the issue, real code is much different. I'm NOT trying to expose the private $foo or something. It just happened that Base and Derived were imlemented by different developers that choosed the same name for a field and doing that (along with the other two conditions) caused the Notice. I'm just trying to understand why, since AFAIK it should be perfectly fine.
I believe this to be a bug in PHP, since running that example in PHP7 does not produce this notice. Even more, if you var_dump($this) in getFoo() definition, it is clearly visible that "foo":"Base":private is defined, and foo of object Derived is defined as well, in both PHP >= 7 and PHP < 7. Please see: https://3v4l.org/Ob9m7
However, I believe that, whatever you are trying to do with this piece of code, you are going about it the wrong way. As ArtisticPhoenix already mentioned. I would strongly suggest you reconsider and re-design whatever you are doing. Overriding private members is just not possible as per OOP.
That is the expected behavior, private means this class only. Therefore the private property $foo is not visible to the child class. Changing it to public is not gonna work, likely there is a warning associated to that that you are not seeing. What level error reporting to you have on.
Child classes have to keep at least the same visibility of the parent.
At best you could do it private in the child class, but that is not going to give you access to the same $foo
Notice or not, the readability of the code is poor, when trying to force a private variable to be something else. Which foo do you expect to get? And why not just name it something else in the child class if you need $foo in the parent as a default, just do an if/then in the child's get method.
Essentially your trying to change the value of $foo in the child class when it's private in the parent class. And then retrieve it from the parent. As I mentioned at best you wont get the $foo you expect and it makes the code confusing.
i have the following logic in my model:
if ( $switch_obj->connect() ) {
if ( $data = $switch_obj->showIntAll() ) {
$switch_obj->disconnect();
return $data;
}
else {
$switch_obj->disconnect();
throw new Exception('Empty Data Set');
}
}
else {
throw new Exception('Connection');
}
This switch_obj that's being called has logic in it's constructor and destructor to increment / decrement counters respectively. (saved in a class called testclass).
So each time an object of type testclass is instantiated, a counter is increased. And then when destroyed, it's decremented.
However, I've just discovered a scenario that I'm not handling.
Fatal error: Call to undefined method testclass::showIntAll() in
/var/www/myapp/application/models/test_model.php on line 215
It's clear that I'm calling a method that doesn't exist, which I will resolve. But my question is this: in creating this error, i can see that the counter has already been incremented ... but not decremented because once this error is thrown, it never returns to the destructor method in my class.
How would I program for these types of scenarios? Obviously, in production, I won't get getting errors because of missing methods in testclass... but in case I do get an unexpected error where the testclass constructor is called and then it bombs, I'm just wondering what the best way is to handle this.
You might achieve something with register_shutdown_function. Your constructor could register a clean-up function which would get called if an error would occur. You'd have to be careful not to call the clean-up code twice though (once from the destructor and once from this registered function.
Not a pretty solution, but it could work :)
As far as i can remember destructors are not called on fatal errors
I witnessed some strange behaviour regarding PHP's exception handling in a recent project. Case goes as follows.
In my app, I use namespaces. All classes are in individual source code files. The code relevant to this particular case, is spread over 3 classes.
The "outermost" class is a dispatcher (or router), which wraps the dispatch call inside a try-catch block. The dispatched request, calls a method in a third class, which runs code (wrapped in a try-catch block), which causes an exception.
Because I had omitted a use Exception; statement in the class where the error happens, the thrown exception trickles all the way back to the outermost layer (the dispatcher), where it is caught - causing me to scratch my head why the catch around the code causing the error isn't working.
To me this seems strange. Logically, PHP should in this situation (IMO) throw a Class not found exception/error, leading me to the actual error in my code, instead of trying to "stay alive" as long as possible.
Should this be filed as a bug, or is this expected behaviour?
Edit: Code example
File: class-a.php
<?php
namespace hello\world;
class classA {
protected $b;
public function __construct() {
$this->b = new \hello\world\classB();
}
public function doSomething() {
try {
$this->b->throwException();
} catch (Exception $e) {
}
}
}
File: class-b.php
<?php
namespace hello\world;
class classB
{
public function throwException() {
throw new \Exception("bar closed");
}
}
File: run.php
<?php
include 'class-a.php';
include 'class-b.php';
$a = new \hello\world\classA();
$a->doSomething();
ClassB throws an \Exception in ClassB::doSomething(), for which ClassA has a catch-clause, but because ClassA doesn't declare use Exception or catch (\Exception), the catch doesn't match and execution ends with a Uncaught exception error1. But in my opinion, it should cause a Class not found error.
I might be expecting too much of the permissive PHP compiler, but it would help in tracking down silly errors that should be easy for the compiler to spot.
1 If the $a->doSomething() in run.php was surrounded by a try..catch clause, the Exception would (or at least could) be caught there, since it trickles down the stack.
PHP's exception catching mechanism does not validate that the class you catch actually exists.
It exhibits the same behavior when using typehinting in functions, so I suspect it merely converts the exception/function type hint into a string or something and compares that with the type of the relevant object.
Whether this is a bug or not is questionable. Personally I think it should be classified as a bug, but PHP has all sorts of wonky behaviors :D
I'm struggling to get my head around when to use a couple of the PHP SPL Exceptions, specifically in the below scenario,
class MyClass {
protected $data1;
protected $data2;
public function setData1($data1) {
$this->data1 = $data1;
}
public function setData2($data2) {
$this->data2 = $data2;
}
public function invokeProcess() {
$this->validateData();
}
protected function validateData() {
if(!$this->data1) {
// Which Exception do I throw? See explanation below
}
if($this->data1 && $this->data2) {
// Which Exception do I throw? See explanation below
}
}
}
I have a class which is constructed. The user then sets some data on the object, and invokes a process. The first thing this process does is validate the data on the object to make sure required data is present, data combinations are correct, etc, and if they aren't, an Exception needs to be thrown.
So what Exceptions do I throw?
My validation checks for two scenarios really,
Missing data, a.k.a. data that has not been set.
Bad combination of data.
For #1, I'm torn between BadMethodCallException, RuntimeException, and LogicException. And for #2, I think it's just a LogicException?
So, which ones so I use?
Note: Before anyone asks, I can't have required data as parameters in the constructor due to some data only being required when other data is set, etc.
If you have to use an SPL exception, that would be RuntimeException. That's the one that refers to an error that can only be detected at runtime (such as bad input data).
LogicException would be an inappropriate choice, as it refers to a logic error in your program, not in the data it receives. Think of a LogicException as a panic button when your program detects that a condition that must always be true is not (contrast this with a condition that should be true for the program to perform its intended function).
BadMethodCallException would also be inappropriate since it represents an
Exception thrown if a callback refers
to an undefined method or if some
arguments are missing.
Some data your logic needs may be missing, but there's no method call without the correct number of arguments there.
In your shoes I would either define my own exceptions (derived from RuntimeException), or use RuntimeException directly.
Considering that none of the pre-existing exceptions seems to answer your needs, why not create your own exceptions ?
For instance, you could have :
A super-class called ValidationException, that would extend Exception.
That would be inherited by two sub-classes :
ValidationException_MissingData
ValidatonException_BadCombination
You do not necessarily have to use the pre-existing SPL exceptions, if they don't fit your situation : the exceptions mecanism is powerful enough to let you define whatever you need -- and that's what's done by most Frameworks, for instance.
For more details on the how, see Extending Exceptions.
I would personally throw an InvalidArgumentException in each set*() method.
This question is specific to using PHPUnit.
PHPUnit automatically converts php errors to exceptions. Is there a way to test the return value of a method that happens to trigger a php error (either built-in errors or user generated errors via trigger_error)?
Example of code to test:
function load_file ($file)
{
if (! file_exists($file)) {
trigger_error("file {$file} does not exist", E_USER_WARNING);
return false;
}
return file_get_contents($file);
}
This is the type of test I want to write:
public function testLoadFile ()
{
$this->assertFalse(load_file('/some/non-existent/file'));
}
The problem I am having is that the triggered error causes my unit test to fail (as it should). But if I try to catch it, or set an expected exception any code that after the error is triggered never executes so I have no way of testing the return value of the method.
This example doesn't work:
public function testLoadFile ()
{
$this->setExpectedException('Exception');
$result = load_file('/some/non-existent/file');
// code after this point never gets executed
$this->assertFalse($result);
}
Any ideas how I could achieve this?
There is no way to do this within one unit test. It is possible if you break up testing the return value, and the notice into two different tests.
PHPUnit's error handler catches PHP errors and notices and converts them into Exceptions--which by definition stops program execution. The function you are testing never returns at all. You can, however, temporarily disable the conversion of errors into exceptions, even at runtime.
This is probably easier with an example, so, here's what the two tests should look like:
public function testLoadFileTriggersErrorWhenFileNotFound()
{
$this->setExpectedException('PHPUnit_Framework_Error_Warning'); // Or whichever exception it is
$result = load_file('/some/non-existent/file');
}
public function testLoadFileRetunsFalseWhenFileNotFound()
{
PHPUnit_Framework_Error_Warning::$enabled = FALSE;
$result = load_file('/some/non-existent/file');
$this->assertFalse($result);
}
This also has the added bonus of making your tests clearer, cleaner and self documenting.
Re: Comment:
That's a great question, and I had no idea until I ran a couple of tests. It looks as if it will not restore the default/original value, at least as of PHPUnit 3.3.17 (the current stable release right now).
So, I would actually amend the above to look like so:
public function testLoadFileRetunsFalseWhenFileNotFound()
{
$warningEnabledOrig = PHPUnit_Framework_Error_Warning::$enabled;
PHPUnit_Framework_Error_Warning::$enabled = false;
$result = load_file('/some/non-existent/file');
$this->assertFalse($result);
PHPUnit_Framework_Error_Warning::$enabled = $warningEnabledOrig;
}
Re: Second Comment:
That's not completely true. I'm looking at PHPUnit's error handler, and it works as follows:
If it is an E_WARNING, use PHPUnit_Framework_Error_Warning as an exception class.
If it is an E_NOTICE or E_STRICT error, use PHPUnit_Framework_Error_Notice
Else, use PHPUnit_Framework_Error as the exception class.
So, yes, errors of the E_USER_* are not turned into PHPUnit's *_Warning or *_Notice class, they are still transformed into a generic PHPUnit_Framework_Error exception.
Further Thoughts
While it depends exactly on how the function is used, I'd probably switch to throwing an actual exception instead of triggering an error, if it were me. Yes, this would change the logic flow of the method, and the code that uses the method... right now the execution does not stop when it cannot read a file. But that's up to you to decide whether the requested file not existing is truly exceptional behaviour. I tend to use exceptions way more than errors/warnings/notices, because they are easier to handle, test and work into your application flow. I usually reserve the notices for things like depreciated method calls, etc.
Use a phpunit.xml configuration file and disable the notice/warning/error to Exception conversion. More details in the manual. It's basically something like this:
<phpunit convertErrorsToExceptions="false"
convertNoticesToExceptions="false"
convertWarningsToExceptions="false">
</phpunit>
Instead of expecting a generic "Exception", what about expecting a "PHPUnit_Framework_Error" ?
Something like this might do :
/**
* #expectedException PHPUnit_Framework_Error
*/
public function testFailingInclude()
{
include 'not_existing_file.php';
}
Which, I suppose, might also be written as :
public function testLoadFile ()
{
$this->setExpectedException('PHPUnit_Framework_Error');
$result = load_file('/some/non-existent/file');
// code after this point never gets executed
$this->assertFalse($result);
}
For more informations, see Testing PHP Errors
Especially, it says (quoting) :
PHPUnit_Framework_Error_Notice and
PHPUnit_Framework_Error_Warning represent
PHP notices and warning, respectively.
Looking at the /usr/share/php/PHPUnit/TextUI/TestRunner.php file I have on my system, I see this (line 198 and following) :
if (!$arguments['convertNoticesToExceptions']) {
PHPUnit_Framework_Error_Notice::$enabled = FALSE;
}
if (!$arguments['convertWarningsToExceptions']) {
PHPUnit_Framework_Error_Warning::$enabled = FALSE;
}
So maybe you'll have to pass some kind of parameter to activate that behaviour ? But it seems to be enabled by default...
Actually there is a way to test both the return value and the exception thrown (in this case an error converted by PHPUnit).
You just have to do the following:
public function testLoadFileTriggersErrorWhenFileNotFound()
{
$this->assertFalse(#load_file('/some/non-existent/file'));
$this->setExpectedException('PHPUnit_Framework_Error_Warning'); // Or whichever exception it is
load_file('/some/non-existent/file');
}
Notice that to test for the return value you have to use the error suppression operator on the function call (the # before the function name). This way no exception will be thrown and the execution will continue. You then have to set the expected exception as usual to test the error.
What you cannot do is test multiple exceptions within a unit test.
This answer is a bit late to the party, but anyhow:
You can use Netsilik/BaseTestCase (MIT License) to test directly for triggered Notices/Warnings, without ignoring them or converting them to Exceptions. Because the notices/warnings they are not converted to an Exception, the execution is not halted.
composer require netsilik/base-test-case
Testing for an E_USER_NOTICE:
<?php
namespace Tests;
class MyTestCase extends \Netsilik\Testing\BaseTestCase
{
public function test_whenNoticeTriggered_weCanTestForIt()
{
$foo = new Foo();
$foo->bar();
self::assertErrorTriggered(E_USER_NOTICE, 'The notice message');
}
}
Hope this helps someone in the future.