Discussion of testing theory and practice, including methodologies (such as TDD, BDD, DDD, Agile, XP) and software - anything to do with testing goes here. (Formerly "The Testing Side of Development")
class MySQLDB
{
public function __construct ($name, $pass, $address)
{
$this->_username = $name;
$this->_password = $pass;
$this->_address = $address;
$this->connect();
}
public function connect ()
{
$this->_link = mysql_pconnect(/* etc. */);
if (!$this->_link)
{
throw new MySQLDBException(/* etc. */);
}
}
}
public function testObjectInstantiationWithNoParams ()
{
$this->expectError('Missing argument 1 for MySQLDB::__construct(), '
. 'called in /var/www/localhost/htdocs/MySQLDBUnitTestCase.php '
. 'on line 32 and defined');
$this->expectError('Missing argument 2 for MySQLDB::__construct(), '
. 'called in /var/www/localhost/htdocs/MySQLDBUnitTestCase.php '
. 'on line 32 and defined');
$this->expectError('Missing argument 3 for MySQLDB::__construct(), '
. 'called in /var/www/localhost/htdocs/MySQLDBUnitTestCase.php '
. 'on line 32 and defined');
$this->expectError('Undefined variable: name');
$this->expectError('Undefined variable: pass');
$this->expectError('Undefined variable: address');
$this->expectException('MySQLDBException');
$mysql = new MySQLDB();
$this->assertNull($mysql);
}
Now the problem is, the test fails on the expectException() and assertNull() because the constructor doesn't die when I fail to provide arguments - despite not having default params. (expectException was resolved by revoking some privileges in mysql, which then equates $mysql to null, but if I didn't throw and exception it doesn't, fyi.)
Is there a way to tighten this up? I'd like for the object to not instantiate if the arguments are missing, without the need for "if ($param1 == null)" in any class I wish to fit this behaviour.
Are you sure the expectException() isn't catching the error that was thrown by the "Missing arguments" error? I reckon it is. Once expectException() has caught what you epxected, it's then going to allow subsequent exceptions to be thrown. Refer the the other expectException() thread posted recently to see how I do it and give that a go... it will almost certainly fix it.
The "error" is most definitely that the object is still instantiating. I guess this should really be in php code/t+d or even on php.net bugzilla and not here in the testing forum, as it's not really a testing problem - it's a behavioural problem. I.e. I would prefer if "Missing Argument" was of E_FATAL and not E_WARNING.
The exception, when thrown, does prevent the instantiation - but if (in a different class) I did not throw an exception, it would still instantiate.. for example:
You realise the entire test could document the method just using the expectException() call and nothing else? The rest are platform specific error messages which are going to be generated by PHP anyway so why bother testing them?
By definition a class is instantiated the moment you use the "new" keyword on a valid class name. Then it calls the constructor - hence the errors are method level - there's no way to prevent instantiation. Best way is to check the initial conditions and throw an exception - which can rely on internal checks, or as is currently the case, whether the initial state can be completed (i.e. making that database persistent connection).
Once an exception is thrown from the constructor, I presume the initialisation is a failure and the object discarded - so an assertNull() should also pass once the constructor throws an exception (esp. since PHP considers uncaught exceptions as being fatal).
require_once 'MySQLDB.php';
class Test_Db extends UnitTestCase {
public function testObjectInstantiationWithNoParams ()
{
$this->expectException('MySQLDBException');
$mysql = new MySQLDB();
}
}
I missed your last reply - but to run a constructor, the object must first be instantiated - so missing arguments aren't technically fatal since they don't prevent an object from existing and are hence recoverable.
class Foo
{
public function __construct ($param)
{}
public function bar ($param)
{}
}
$foo = new Foo(); // E_WARNING
$foo->bar(); // E_FATAL
?>
And yes, the errors are platform dependant.. but my test fails if I don't have them - unexpected error "Warning: etc." but sure, I could change them to regex asserts.
I don't get your logic ~ole. It's not very readable and some of it is superfluous. If you get into the catch() block you know instantly that the exception was thrown, if you get (inside the try{} block) beyond the point the exception was expected, then it didn't work. You appear to be creating a bit of a higgldy-piggldy mess
try {
$foo = new Foo();
$this->fail("This should never execute");
} catch (Exception $e) {
$this->assertTrue(!isset($foo));
}
//And even my version of this is superfluous
$foo = new Foo(1, 2, 3);
$this->assertIsA($foo, "Foo");
You can't make assertions from within the catch block because then if an exception isn't caught the assertion will never be made.
And generally I prefer to make assertions as opposed to fail() - come to think of it can you fail multiple times in a single method? If you can I might eat my words on that last point.
Anyway I wasn't using SimpleTest for that last example so I couldn't have fail()'d even if I wanted to and no I'm even going to consider die()
Ah ok sorry, I assumed we were talking simpletest. fail() and pass() are just the notifiers for the reporter. They can be called as many times as needed.
And yes, the errors are platform dependant.. but my test fails if I don't have them - unexpected error "Warning: etc." but sure, I could change them to regex asserts.
The test I added doesn't fail... Don't know why but it doesn't - SimpleTest might be doing some swallowing until it finds the expected Exception and then not reporting the error stack that built up behind it (since uncaught Exceptions are fatal).
On the other issue - constructors are not methods. They have no return type, and occur when creating an object, versus being called *after* an object is created. Somewhere in the PHP source there's a good reason no doubt - I'm not about to look for it, so maybe a post to the internals list?
Because a constructor is part of the public interface - you always have the option of passing it parameters. Making the constructor private removes it from the public space, which means a class can't be initialised. The only way around that is to call the constructor privately at the class level (where it exists) using a static method for example - the basic premise of the Singleton.
d11wtq@w3style:~/public_html$ php foo.php
Warning: Missing argument 1 for Foo::__construct(), called in /Users/d11wtq/public_html/foo.php on line 7 and defined in /Users/d11wtq/public_html/foo.php on line 4
Warning: Missing argument 2 for Foo::__construct(), called in /Users/d11wtq/public_html/foo.php on line 7 and defined in /Users/d11wtq/public_html/foo.php on line 4
Warning: Missing argument 3 for Foo::__construct(), called in /Users/d11wtq/public_html/foo.php on line 7 and defined in /Users/d11wtq/public_html/foo.php on line 4
object(Foo)#1 (0) {
}
d11wtq@w3style:~/public_html$
SimpleTest swallows those warning though probably.
Which is exactly my point.. the constructor is public interface, providing you supply the required params - the behaviour I seek is to not instantiate the object if the constructor is missing arguments. This is resolved by including a challenge for each required param, but it just seems wrong to have to do it that way. I can't think of any circumstance where not supplying required parameters would be recoverable. If I were going for polymorphism, they would be optional params.
I shall have a look on php's bugzilla to see if anyone has raised this point, if they haven't I'll raise it myself (and probably get shot down in flames.)