I'm afraid I'm rather new to unit testing.
I have a PHP class called "dbRecord" that I use to abstract database tables/records and am trying to apply unit testing to it. When I do so though, the mysqli object that it uses seems to break.
As an example, I have a user class (an extension of the abstract dbRecord class) that works quite nicely if I create and save a user in a shell. That would be something like this:
$u = new userClass();
$u->setEmail('foo#bar.com');
$u->save();
That works very nicely if I use it in a web site or from a php shell. It creates an object, sets the e-mail value, and then saves it in the database, assigning the auto-incrementing id field back to the $u object.
If I try to create a unit test though, it fails upon saving the record. The error is happening when it tries to use mysqli::real_escape_string, but it happens with any other member of the mysqli object (I have tested that just in case). The error I get is:
mysqli::real_escape_string(): Couldn't fetch mysqli
So I assume there's something I'm failing to understand in the scope of things when doing the unit test. Here's the full code of the test I'm running:
<?php
require_once "../dbTemplate.php";
class TestOfUserClass extends PHPUnit_Framework_TestCase {
function testAssignUserValues(){
$usr = new userClass();
$usr->setEmail('foo#bar.com');
$this->assertTrue($usr->getEmail() == 'foo#bar.com');
$usr->save();
}
}
The code that's actually throwing that exception is in the save call. Here's what it looks like:
public function save(){
if($this->_isNewRecord){
// we're creating a new record
$query = "INSERT INTO `" . $this->_mysqli->real_escape_string($this->_tableName) . "`";
...
At this point I'm rather lost as to why it's not able to use that mysqli instance. Again, this works perfectly under regular usage (either through apache on a web page or through a php shell). It's only when using the test cases that it dies. I tried it with both Simpletest and PHPUnit, with the same results.
The structure of how that mysqli object is accessed goes like this:
In my test file (test1.php), "../dbTemplate.php" is included at the top.
In dbTemplate.php:
the mysqli object is created at the top, assigned to a variable called $mysqli.
the dbRecord abstract class is then defined
in that definition's construct, $this->_mysqli is assigned the value of $mysqli.
The test file is exactly as quoted above.
Then of course, I run "phpunit test1.php"
Does anyone see what I'm doing wrong here?
You will want to instantiate your object being tested in setUp() as opposed to the testing function. If you get an exception there then that usually means the path to dbTemplate.php can't be resolved from the test folder.
Without looking at the guts of your class it's hard to explain exactly, but generally speaking if you are not using autoloading, phpunit will not be able to resolve the locations of other files that are located outside of the testing root folder. Using an autoloader in bootstrap.php, however, helps with this. See PHPUnit Bootstrap and Autoloading Classes.
Related
I created a simple test for my new Laravel 7 application. But when I run php artisan test I get the following error.
Target [Illuminate\Contracts\View\Factory] is not instantiable.
The error doesn't appear when I go to the page in the browser.
$controller = new HomeController();
$request = Request::create('/', 'GET');
$response = $controller->home($request);
$this->assertEquals(200, $response->getStatusCode());
Although "Just write feature tests" may seem like a cop-out ("They're not unit tests!"), it is sound advice if you do not want to get bogged down by framework-specific knowledge.
You see, this is one of those problems that come from using facades, globals, or static methods. All sorts of things happen outside of your code (and thus your test code) in order for things to work.
The problem
To understand what is going on, you first need to know how Laravel utilizes Containers and Factories in order to glue things together.
Next, what happens is:
Your code (in HomeController::home() calls view() somewhere.
view() calls app() to get the factory that creates Views1
app() calls Container::make
Container::make calls Container::resolve1
Container::resolve decides the Factory needs to be built and calls Container::build to do so
Finally Container::build (using PHP's ReflectionClass figures out that \Illuminate\Contracts\View\Factory can not be Instantiated (as it is an interface) and triggers the error you see.
Or, if you're more of a visual thinker:
The reason that the error is triggered is that the framework expects the container to be configured so that a concrete class is known for abstracts (such as interfaces).
The solution
So now we know what is going on, and we want to create a unit-test, what can we do?
One solution might seem to not use view. Just inject the View class yourself! But if you try to do this, you'll quickly find yourself going down a path that will lead to basically recreating loads of framework code in userland. So not such a good idea.
A better solution would be to mock view() (Now it is really a unit!). But that will still require recreating framework code, only, within the test code. Still not that good.[3]
The easiest thing is to simply configure the Container and tell it which class to use. At this point, you could even mock the View class!
Now, purists might complain that this is not "unit" enough, as your tests will still be calling "real" code outside of the code-under-test, but I disagree...
You are using a framework, so use the framework! If your code uses glue provided by the framework, it makes sense for the test to mirror this behavior. As long as you don't call non-glue code, you'll be fine![4]
So, finally, to give you an idea of how this can be done, an example!
The example
Lets say you have a controller that looks a bit like this:
namespace App\Http\Controllers;
class HomeController extends \Illuminate\Routing\Controller
{
public function home()
{
/* ... */
return view('my.view');
}
}
Then your test[5] might look thus:
namespace Tests\Unit\app\Http\Controllers;
use App\Http\Controllers\HomeController;
use Illuminate\Contracts\View\Factory;
class HomeControllerTest extends \PHPUnit\Framework\TestCase
{
public function testHome()
{
/*/ Arange /*/
$mockFactory = $this->createMock(Factory::class);
app()->instance(Factory::class, $mockFactory);
/*/ Assert /*/
$mockFactory->expects(self::once())
->method('make')
->with('my.view')
;
/*/ Act /*/
(new HomeController())->home();
}
}
A more complex example would be to also create a mock View and have that be returned by the mock factory, but I'll leave that as an exercise to the reader.
Footnotes
app() is asked for the interface Illuminate\Contracts\View\Factory, it is not passed a concrete class name
The reason Container::make does nothing other than call another function is that the method name make is defined by PSR-11 and the Laravel container is PSR compliant.
Also, the Feature test logic provided by Laravel already does all of this for you...
Just don't forget to annotate the test with #uses for the glue that is needed, to avoid warnings when PHPUnit is set to strict mode regarding "risky" tests.
Using a variation of the "Arrange, Act, Assert" pattern
This is not how you test endpoints in Laravel. You should let Laravel instantiate the application as it is already setup in the project, the examples you can see here.
What you already wrote can be rewritten to something like this.
$response = $this->call('GET', route('home')); // insert the correct route
$response->assertOk(); // should be 200
For the test to work, you should extend the TestCase.php, that is located in your test folder.
If you're finding this in The Future and you see #Pothcera's wall of text, here's what you need to know:
The ONLY reason he's doing any of that and the ONLY reason you're seeing this in the first place in a Unit test is because he and you haven't changed from PHPUnit\Framework\TestCase to Tests\TestCase in the test file. This exception doesn't exist when you extend the test case that includes app().
My advice would be to simply extend the correct base test case and move on with your life.
In the framework we're using, there are several operations that are performed considering the class name.
So, while testing, I have to force the mock classname, doing something like this:
$this->getMock('Model', $methods, array($config), 'ModelFoobars');
Sadly I just found out that doing this will pollutes my following tests. This is very strange, since there is no internal cache (singleton pattern) in the objects I'm curently testing.
However, if I debug my test, I can find that my $model variable is an instance of PhpUnit, even if it was created using a new syntax!
$classname = 'ModelFoobars';
$model = new $classname();
I have no opcode cache, nor anything like that. This is really driving me crazy, any suggestions?
After a lot of testing and debugging (and I really mean a lot), I finally found the problem.
When you create a new mocked object, under the hood PhpUnit is dynamically creating new code, then it evals it.
By default, PhpUnit creates random class names, so you usually don't have any problems; however, if you force the class name, you can have "funny" results. Since the code is evaled, the class is pushed into the global scope!
This means that once you created the mock, you can't create another one with the same name: you are stuck using the first one.
The only solution I found is to avoid using fixed class names and always rely on the random ones.
I'm calling phpunit with the argument being a directory (bonus questions: why can't it accept a list of files?) and it's now complaining about a class being declared more than once because of a file included in the previous test!
If i run phpunit firstTest.php; phpunit secondTest.php everything works
But phpunit ./ fails with PHP Fatal error: Cannot redeclare class X
my tests are basically:
include 'class_to_be_tested.php'
class class1Test extends...
and nothing else. And i'm using the option --process-isolation. I could add require_once on my classes, but that's not what i want to be able to test them individually.
shouldn't phpunit follow best testing practices and run one test, clear whatever garbage it have, run another test on a clean state? or am i doing something wrong?
Since you have include rather than include_once and there is no other code shown in your question, the cannot redeclare error could also be that you are including the file again somewhere in the code under test.
Assuming that is not the case, there some behind the scenes things that happen with --process-isolation that can keep the global class declarations. This blog post gives more detail: http://matthewturland.com/2010/08/19/process-isolation-in-phpunit/
Basically, you will want to create your own base TestCase and override the run() method to set the preserveGlobalState to false. This should properly allow all your tests to run together.
The base class would look similar to this (taken from the blog post I referred to):
class MyTestCase extends PHPUnit_Framework_TestCase
{
public function run(PHPUnit_Framework_TestResult $result = NULL)
{
$this->setPreserveGlobalState(false);
return parent::run($result);
}
}
The way phpUnit works means that all your tests are run in the context of a single php program. phpUnit aims to isolate the tests from each other, but they are all run within the same program execution.
PHP's include statement will include the requested file regardless of whether it has been included before. This means that if you include a given class twice, you will get an error the second time. This is happening in your tests because each test is including the same file, but without any consideration to whether it's already been included by one of the other tests.
Solutions:
Wrap your include calls with a if(class_exists('classname')) so that you don't include the file if the class has already been defined.
Include the files in a phpUnit bootstrap file instead of in the tests.
Use include_once (or even require_once) instead of include.
Stop including files arbitrarily, and start using an autoloader.
Change:
include 'class_to_be_tested.php';
class class1Test extends...
to be:
include_once 'class_to_be_tested.php';
class class1Test extends...
In PHP you need to have a Really Good Reason to use the former.
Regarding why can't it accept a list of files?, I think the design decision is that you generally don't need to. However you can do it by creating a test suite in the phpunit.xml.dist file, see
http://www.phpunit.de/manual/current/en/organizing-tests.html#organizing-tests.xml-configuration
I'm trying to create a mock to satisfy a typehint with this code (Mockery):
return \Mockery::mock('\Contracts\Helpers\iFileSystemWrapper');
or this (PHPUnit):
return $this->getMock('\Contracts\Helpers\iFileSystemWrapper');
But the mock returned is called Mockery\Mock Object or Mock_iFileSystemWrapper_a5f91049. How am I supposed to type check this when it isn't an instance of what I need at all with either framework?
Why exactly is the mock framework trying to load the real class? If I wanted the real class I would include the real class.
This problem has slowed me down so many times when writing tests I'm about to just toss type hinting out the window and check class names instead, or simply use production objects as mocks are a pain to use.
I just experimented with an existing test of my own, and by changing the interface namespace name from one that exists to one that doesn't exist, I got exactly the same as what you describe (using phpunit). My mock object had the class name Mock_ViewInterface_c755461e. When I change it back to the correct interface name, it works fine.
Therefore I would say that either:
You are trying to use an interface name that doesn't exist (e.g. a typo or missing namespace component).
Your library code isn't being loaded for some reason, e.g. autoloading is not setup correctly in your unit test bootstrap.
You need use a special function to check base class.
Somthing like this:
$mock = $this->getMock('MyClass');
$this->assertInstanceOf('MyClass', $mock);
I am having problem with implementing custom session handler in php.
The code:
http://pastebin.com/9QV9f22Q
I initialize this once in my frameworks bootstrap, after I have connection with db:
require_once 'DbSession.php';
$session = new DbSession();
session_start();
But then I can't view my page. Firefox gets 302 Status with "Server not found" error. Firebug says that content is 5k long, but I can't view the page.
Log after one reload:
http://pastebin.com/JYe14nGR
I wonder why it still "loses" that created DbSession instance. Do yo have any ideas? TIA
Your code is based on a false premise: that you can return a different object from a PHP constructor. Quite the opposite: PHP entirely ignores the return value from a constructor.
Here's what's actually happening.
When your code calls:
$session = new DbSession();
the $firstTime == false check runs, meaning getInstance() gets called.
getInstance finds no existing instance, so it calls setup().
setup() calls new DbSession() again, this time passing in the argument preventing another call to getInstance(). It creates the object, registers it as the session handler, and returns it.
getInstance shoves the object in a static variable, and then returns it to the original constructor call. The original constructor call then drops the existing object on the ground and returns a brand new copy of itself.
You can fix some of this insanity by never instantiating the object outside of setup/getInstance(). Try making the constructor protected and only ever calling getInstance().
However, none of this explains why the code is malfunctioning for you. In fact, we can't explain it either. We're missing all the rest of the code, including what database adapter you're using, what ORM you're using (or even if you are using an ORM, your class and method names suggest it), what framework(s) might be involved, etc.
Try cutting all of the actual database touching from the class. Just write files on disk. Get that working first, then introduce the database layer. Chances are that your error will become obvious at that point.