We have moved our forum to GitHub Discussions. For questions about Phalcon v3/v4/v5 you can visit here and for Phalcon v6 here.

phpunit: Phalcon\DI\Exception: Service 'db' was not found in the dependency injection container

Hi,

In my phpunit tests I have a test helper where I load in my models.

// use the application autoloader to autoload the classes
// autoload the dependencies found in composer
$loader = new \Phalcon\Loader();

$loader->registerDirs(array(
    ROOT_PATH,
    PATH_CONFIG,
    PATH_MODELS
));

$loader->registerNamespaces(array(
    'Phalcon' => PATH_INCUBATOR.'Library/Phalcon/'
));

$loader->register();

$config = include PATH_CONFIG;

$di = new FactoryDefault();

DI::reset();

// add any needed services to the DI here
/**
     * Database connection is created based in the parameters defined in the configuration file
     */
    $di->set('db', function() use ($config) {
        return new \Phalcon\Db\Adapter\Pdo\Mysql(array(
            "host" => $config->database->host,
            "username" => $config->database->username,
            "password" => $config->database->password,
            "dbname" => $config->database->dbname
        ));
    });

DI::setDefault($di);

I know this works as I can create a new model on which my other tests depend:

public function testCreateUser() {
        $user = new \Users();
        return $user;
    }

My problem comes in when I attempt this:

$all_users = \Users::find();

I get the following error:

Phalcon\DI\Exception: Service 'db' was not found in the dependency injection container

Can anyone help?



16.3k

Why you add DI::reset(); and whats that?



47.7k
edited Apr '14

I added DI::reset(); as it is given in the unit testing example in phalcon documentation here

I'm not sure what you are referring to when you say 'and whats that?'.

My guess is that DI::reset() nulls all values from the dependency injector container from memory before adding new stuff to the dependency injector to ensure the dependency injector is fresh.

The di is reset and set by class methods so I don't understand the issue.



47.7k

Ok so I could get access to the database doing the following:

    $di = $this->getDI();
    $connection = $di->getDb();
    $result = $connection->fetchOne("SELECT * FROM UNIVERSE");

But only in the same method where a newly created object model had been created.

To get the di in another test method I had to pass model object to the test method and get the db service from the model object.

    $di = $that->getDI();
    $connection = $di->getDb();
    $result = $connection->fetchOne("SELECT * FROM UNIVERSE");

I might be missing the point but this is keeps things thread safe.

try to set your db as shared service :

$di->set('db', function() use ($config) {
        return new \Phalcon\Db\Adapter\Pdo\Mysql(array(
            "host" => $config->database->host,
            "username" => $config->database->username,
            "password" => $config->database->password,
            "dbname" => $config->database->dbname
        ));
    }, true);


47.7k
edited Apr '14

Ok so to be able to user $all_users = \Users::find() in tests that are after the first test (after the first tear down has executed after the di has been reset)

In the UnitTestCase class:

...
abstract class UnitTestCase extends PhalconTestCase...

I have added:

use Phalcon\DI\FactoryDefault,
...
...
$di = new FactoryDefault();        

$di->set('db', function() use ($config) {
    return new \Phalcon\Db\Adapter\Pdo\Mysql(array(
    "host" => $config->database->host,
    "username" => $config->database->username,
    "password" => $config->database->password,
    "dbname" => $config->database->dbname
));
...    

The parent class setUp in incubator will see that the $di is not null and not attempt to create a new $di.

Hi baychae Can you post the whole code?

I'm struggling with exactly the same problem, and can't find a solution. The database service is not registred in the dependency container....



47.7k
edited Apr '14

Apologies. I am following the phalcon phpunit testing docs here:

https://docs.phalcon.io/en/latest/reference/unit-testing.html

So assuming you have installed phalcon incubator (mine is in my project)....

Here is my TestHelper.php as in the example:

I have loaded the config included it and used it to set the db service in the dependency injector.

<?php
use Phalcon\DI,
    Phalcon\DI\FactoryDefault;

ini_set('display_errors',1);
error_reporting(E_ALL);

define('ROOT_PATH', __DIR__);
define('PATH_INCUBATOR', __DIR__ . '/../vendor/incubator/');
define('PATH_CONFIG', __DIR__ . '/../app/config/config.php');
define('PATH_MODELS', __DIR__ . '/../app/models/');

set_include_path(
    ROOT_PATH . PATH_SEPARATOR . get_include_path()
);

// use the application autoloader to autoload the classes
// autoload the dependencies found in composer
$loader = new \Phalcon\Loader();

$loader->registerDirs(array(
    ROOT_PATH,
    PATH_CONFIG,
    PATH_MODELS
));

$loader->registerNamespaces(array(
    'Phalcon' => PATH_INCUBATOR.'Library/Phalcon/'
));

$loader->register();

$config = include PATH_CONFIG;

$di = new FactoryDefault();

DI::reset();

// add any needed services to the DI here
/**
* Database connection is created based in the parameters defined in the configuration file
*/
$di->set('db', function() use ($config) {
       return new \Phalcon\Db\Adapter\Pdo\Mysql(array(
               "host" => $config->database->host,
               "username" => $config->database->username,
               "password" => $config->database->password,
               "dbname" => $config->database->dbname
       ));
},true);

DI::setDefault($di);

Then in the UnitTestCase which extends the PhalconTestCase from incubator I have accessed the global config which I use to set the dependency injector with the db service (this is so we can have the db service available in each and every test).

<?php
    use Phalcon\DI,
    Phalcon\DI\FactoryDefault,
    \Phalcon\Test\UnitTestCase as PhalconTestCase;

abstract class UnitTestCase extends PhalconTestCase {

  /**
  * @var \Voice\Cache
  */
  protected $_cache;

  /**
  * @var \Phalcon\Config
  */
  protected $_config;

  /**
  * @var bool
  */
  private $_loaded = false;

  public function setUp(Phalcon\DiInterface $di = NULL, Phalcon\Config $config = NULL) {

    global $config;

    // Load any additional services that might be required during testing
    $di = DI::getDefault();

    $di = new FactoryDefault();        

    $di->set('db', function() use ($config) {
          return new \Phalcon\Db\Adapter\Pdo\Mysql(array(
          "host" => $config->database->host,
          "username" => $config->database->username,
          "password" => $config->database->password,
          "dbname" => $config->database->dbname
                ));
    });

    // get any DI components here. If you have a config, be sure to pass it to the parent
    parent::setUp($di,$config);

    $this->_loaded = true;
  }

  /**
  * Check if the test case is setup properly
  * @throws \PHPUnit_Framework_IncompleteTestError;
  */
  public function __destruct() {
      if(!$this->_loaded) {
      throw new \PHPUnit_Framework_IncompleteTestError('Please run parent::setUp().');
      }
    }
}

My unit tests extend \UnitTestCase as defined above.

I can't recommend doing this. It might be better to setup a database adapter in the phpunit test itself.

Thank you very much for your code! It really helped me get on with the my tests. Your code works just fine.

You can simplify it by removing the "$di->set('db', function() use ($config) {..." part from TestHelper.php.

I can't see any differences when I remove the code, but maybe I'm missing something?

Again, thanks alot!



47.7k

You're welcome.

Yes removing the whole dependency injector setup worked for me. The $di must not be present in the TestHelper.php at all for my first test to run.

I now setup the $di wholly in the UnitTestCase.php:

This is what I removed in my test helper to get it to work:

$di = new FactoryDefault();

DI::reset();

// add any needed services to the DI here
/**
* Database connection is created based in the parameters defined in the configuration file
*/
$di->set('db', function() use ($config) {
       return new \Phalcon\Db\Adapter\Pdo\Mysql(array(
               "host" => $config->database->host,
               "username" => $config->database->username,
               "password" => $config->database->password,
               "dbname" => $config->database->dbname
       ));
});

DI::setDefault($di);

This is what I have in my UnitTestCase.php:

    public function setUp(Phalcon\DiInterface $di = NULL, Phalcon\Config $config = NULL) {

        global $config;

        // Load any additional services that might be required during testing
        $di = DI::getDefault();

        $di = new FactoryDefault();        

        $di->setShared('db', function() use ($config) {
        return new \Phalcon\Db\Adapter\Pdo\Mysql(array(
            "host" => $config->database->host,
            "username" => $config->database->username,
            "password" => $config->database->password,
            "dbname" => $config->database->dbname
                        ));
        });

        // get any DI components here. If you have a config, be sure to pass it to the parent
        parent::setUp($di,$config);

        $this->_loaded = true;
    }

Yes, putting the Dependency Injector in the Boostraper seems to cause a bit of a headache. The methods aren't accessible for some reason within the tests and the abstract UnitTestCase.php - well for me that is. Phalcon\DI:getDefault() should return your configured services from the bootstrap after you set the default because its in a global scope, however I'm thinking maybe it is the way PHPUnit deals with things between each test - who knows, someone out there does. Anyhow, after looking at the UnitTestCase file in the incubator it is a better approach to keep it within the abstract setUp($di, $config) function. Why? Well I find it a better design when it comes to inheritance.

Here is my setup:

TestHelper.php


<?php

ini_set('display_errors',1);
error_reporting(E_ALL);

define('ROOT_PATH', __DIR__);
define('MODEL_PATH', __DIR__ . '/../app/models/');

set_include_path(
    ROOT_PATH . PATH_SEPARATOR . get_include_path()
);
// Using the incubator
require __DIR__ . "/../../vendor/autoload.php";

$loader = new \Phalcon\Loader();
$loader->registerDirs(array(
    ROOT_PATH,
    MODEL_PATH
));
$loader->registerNamespaces(array(
    'App\Models' => '../models',
));
$loader->register();

As you can see I've removed the Dependency Injector out of the bootstrap.

UnitTestCase.php


<?php
use Phalcon\DI,
       \Phalcon\Test\UnitTestCase as PhalconTestCase;

abstract class UnitTestCase extends PhalconTestCase {

    /**
     * @var \Voice\Cache
     */
    protected $_cache;

    /**
     * @var \Phalcon\Config
     */
    protected $_config;

    /**
     * @var bool
     */
    private $_loaded = false;

    /**
     * Default fixture for each test
     */
    public function setUp(Phalcon\DiInterface $di = NULL, Phalcon\Config $config = NULL) {
        if(is_null($di)) {
            $di = new Phalcon\DI\FactoryDefault();
        }
        // Load your additional dependencies and services here

        // We should check the config state also
        // We can put it into the DI also if we wanted to
        if(is_null($config)) {
            $this->_config = new Phalcon\Config\Adapter\Ini('../config.ini');
        } else {
            $this->_config = $config
        }

        // Set it as default if anything else uses the DI::getDefault() static method
        DI::setDefault($di);

        // Pass it up the chain
        parent::setUp($di, $this->_config);

        $this->_loaded = true;
    }

    /**
     * Check if the test case is setup properly
     * @throws \PHPUnit_Framework_IncompleteTestError;
     */
    public function __destruct() {
        if(!$this->_loaded) {
            throw new \PHPUnit_Framework_IncompleteTestError('Please run parent::setUp().');
        }
    }
}

Here is a example test to see how you would use it

ExampleTest.php

<?php

namespace App\Test;

use App\Models\TestModel;

class ExampleTest extends \UnitTestCase {

    public function setUp(\Phalcon\DiInterface $di = NULL, \Phalcon\Config $config = NULL) {
        // You now have the option. You can leave out this setUp to use the defaults
        // in the abstract class UnitTestCase.php call, however if this test has
        // specific requirements you can set a new DI with those services. Then when it gets 
        // passed up the inheritance chain the defaults will be applied. 
        $di = new \Phalcon\DI\FactoryDefault();
        $di->set('simple', function() {
            $simpleService = new stdClass();
            $simpleService->name = "Simple";
            return $simpleService;
        }

        // Here is where you take advantage of inheritance the use of the abstract object
        parent::($di, $config);
    }

    public function testTestCase() {
        $this->assertEquals('works', 'works', 'This is OK');
    }

    public function testHello() {
        $newTest = new TestModel();
        $newTest->hello('jim');
        $this->expectOutputString('Hello jim');

        // Let's test the DI with the new structure
        echo $this->di->get('config')->database->adapter;
        $this->expectOutputString('Mysql');

        echo $this->di->get('simple')->name;
        $this->expectOutputString('Simple');

    }
}

This works perfect for me and allows me to change my fixtures to the way I wan instead of throwing everthing in the abstract class. Hope this helps someone!

edited Oct '14

I'd like to expand on Rhys' answer above in case it helps others.

Phalcon's UnitTestCase seems to remove all DI services in the setUp() function, so you have to recreate them in your own setUp() function. But you don't have to fetch a new FactoryDefault DI as well, Phalcon's UnitTestCase does that for you and you can access the DI via $this->di.

My own setup is like this:

A basic TestCase which only pulls in the config file:

abstract class TestCase extends \Phalcon\Test\UnitTestCase {
    public function setUp(\Phalcon\DiInterface $di = NULL, \Phalcon\Config $config = NULL) {
        parent::setUp($di, $config);

        if(is_null($config))
        {
            $config = new \Phalcon\Config\Adapter\Ini('../config.ini');
        }
        $this->di->set('config', $config);
    }
}

A basic TestCaseDb which also injects a database connector:

abstract class TestCaseDb extends TestCase {

    /**
     * Default fixture for each test
     */
    public function setUp(\Phalcon\DiInterface $di = NULL, \Phalcon\Config $config = NULL) {
        parent::setUp($di, $config);

        // db
        $dbConfig = $this->di->get('config')->database;
        $this->di->set('db', function() use ($dbConfig) {
              return new \Phalcon\Db\Adapter\Pdo\Mysql(array(
              'host' => $dbConfig->host,
              'username' => $dbConfig->username,
              'password' => $dbConfig->password,
              'dbname' => $dbConfig->name,
              'options' => array(
                \PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
                \PDO::ATTR_EMULATE_PREPARES  => false,
                \PDO::ATTR_STRINGIFY_FETCHES => false,
              )
            ));
          }, true);
        $this->di->set('modelsManager', function() {
          return new \Phalcon\Mvc\Model\Manager();
        });

        $this->di->set('modelsMetadata', function() {
          return new \Phalcon\Mvc\Model\Metadata\Memory();
        });
    }
}

The config file looks like this:

[database]
host     = localhost
username = test
password = test
name     = testdb

And then you simply extend either TestCase or TestCaseDb, depending on whether you need database access in your tests or not.

edited Nov '14

Hi, today I have same problem. Tests were working perfectly on earlier versions. I moved project to new server, updated Phalcon and incubator and here I come.

Currently, DI is reset on every test method finish.

protected function tearDown()
{
    $di = $this->getDI();
    $di::reset();
    parent::tearDown();
}

Sorce taken from github.

If you want to have shared DI in all tests, you can use my workaround. Just redeclare tearDown in your UnitTestCase class. Call second parent and ignore above code:

protected function tearDown()
{
   \PHPUnit_Framework_TestCase::tearDown();
}

I am having the exact same issue with the Model::findFirst function but just using it inside a controller?

Any help?