We launched new forums in March 2019—join us there. In a hurry for help with your website? Get Help Now!
  • Hoping someone can help explain this before my mind melts. I'm trying to write unit tests for my Snippets using the PHPUnit framework

    I can reproduce the problem using a very simple Snippet:

    MyTestSnippet:
    $x = $modx->getOption('x',$scriptProperties,'default value');
    return $x;


    I can easily add this to a page and then view the page and see the expected result (e.g. "default value");

    But when I set this up in a unit test, I keep getting fatal errors because the $modx object is apparently not initialized, e.g.

    PHP Fatal error:  Call to a member function getOption() on a non-object in 
    /path/to/core/cache/includes/elements/modsnippet/556.include.cache.php on line 8


    My testing code is pretty simple, but the testMySnippet function always fails.

    <?php
    class snippetTest extends \PHPUnit_Framework_TestCase {
    
        // Must be static because we set it up inside a static function
        public static $modx;
        
        /**
         * Load up MODX for our tests.
         * Create sample data for testing.
         */
        public static function setUpBeforeClass() {        
            $docroot = '/set/up/magically/elsewhere';
            include_once $docroot . '/config.core.php';
            
            if (!defined('MODX_API_MODE')) {
                define('MODX_API_MODE', true);
            }
            
            include_once MODX_CORE_PATH . 'model/modx/modx.class.php';
            
            self::$modx = new \modX();
            self::$modx->initialize('mgr');
        }
        
        public function testMySnippet() {
            $props = array();
            $actual = self::$modx->runSnippet('MyTestSnippet', $props);
            $expected = 'default value';
            $this->assertEquals($expected, $actual);
        }
    
    


    Has anyone done this successfully? I had no idea I would be bashing my brains out all weekend for what I thought would take a few minutes.


    Using Revo 2.2.14-pl
    • Ach... the solution is simple: variable scoping. $modx must be declared globally. If I adjust my testing function to this, it works:

      public function testMySnippet() {
              global $modx;
              $modx = self::$modx;
      
              $props = array();
              $actual = self::$modx->runSnippet('MyTestSnippet', $props);
              $expected = 'default value';
              $this->assertEquals($expected, $actual);
      }
        • 3749
        • 24,544 Posts
        Since you're in a class, you can also do this in setUp after initializing MODX:

        $this->modx =& $modx;

        Then you can use $this->modx anywhere without using 'global'.

        MyComponent has extensive unit tests that might give you some ideas: https://github.com/BobRay/MyComponent/tree/master/_build/test
          Did I help you? Buy me a beer
          Get my Book: MODX:The Official Guide
          MODX info for everyone: http://bobsguides.com/modx.html
          My MODX Extras
          Bob's Guides is now hosted at A2 MODX Hosting
        • Interestingly, you can't use "$this->modx" in this particular scenario, and that's part of the rub that ensnared me. PHPUnit uses a *static* function to do its set-up fixtures: see the public static function "setUpBeforeClass()". So I had to use self::$modx instead of the more common $this->modx.

          Then I had to dissect exactly what the cached Snippet function was referencing. Take a look at the actual files that get created when a Snippet is cached: they get rewritten as a simple function with "global $modx" line added. That refers to the $modx variable from the index.php file (set when the mgr or web context are initialized). So the unit test must use "$modx" verbatim: no other variable name will work.

          Secondly, I should point out that passing by reference did not work:

          // Did not work 
          global $modx;
          $modx =& self::$modx;
          
          // Did work
          global $modx;
          $modx = self::$modx;


          I can't explain that last one -- that's some subtlety that's beyond me.
            • 40045
            • 534 Posts
            You could also have a peek at the _build/test/ folder of the github repo to see how it's done for the core, they wrote their own test classes for that as far as I can tell!
            • Another snag: it seems that you can't test a snippet until it's been cached (?)... runSnippet returns nothing until I've added the snippet tag to a page and viewed the page (i.e. forced it to be cached).

              Hmm... "runSnippet" does not get used anywhere in the _build/test/ directory, so they're not testing this the same way.
                • 3749
                • 24,544 Posts
                Interesting, I've been doing unit tests on MyComponent for a couple of years and have never run into those issues (though I have had problems with running processors that are in subdirectories). I use $this->modx all the time. I don't think I use runSnippet() anywhere, though, as all my methods are in classes.

                You might try a more extensive initialization of MODX. Here's an example:

                /**
                     * Sets up the fixture, for example, opens a network connection.
                     * This method is called before each test is executed.
                     */
                    protected function setUp()
                    {
                        // echo "\n---------------- SETUP --------------------";
                        require_once dirname(__FILE__) . '/build.config.php';
                        require_once dirname(__FILE__) . '/uthelpers.class.php';
                        require_once MODX_CORE_PATH . 'model/modx/modx.class.php';
                        $this->utHelpers = new UtHelpers();
                
                        $modx = new modX();
                        $modx->initialize('mgr');
                        $modx->getService('error', 'error.modError', '', '');
                        $modx->getService('lexicon', 'modLexicon');
                        $modx->getRequest();
                        $homeId = $modx->getOption('site_start');
                        $homeResource = $modx->getObject('modResource', $homeId);
                
                        if ($homeResource instanceof modResource) {
                            $modx->resource = $homeResource;
                        } else {
                            echo "\nNo Resource\n";
                        }
                
                        $modx->setLogLevel(modX::LOG_LEVEL_ERROR);
                        $modx->setLogTarget('ECHO');
                
                        require_once MODX_ASSETS_PATH . 'mycomponents/mycomponent/core/components/mycomponent/model/mycomponent/mycomponentproject.class.php';
                
                        /* @var $categoryObj modCategory */
                        $this->mc = new MyComponentProject($modx);
                        $this->mc->init(array(), 'unittest');
                        $this->modx =& $modx;
                  Did I help you? Buy me a beer
                  Get my Book: MODX:The Official Guide
                  MODX info for everyone: http://bobsguides.com/modx.html
                  My MODX Extras
                  Bob's Guides is now hosted at A2 MODX Hosting
                • Ah, you're using the setUp() fixture, whereas I opted for setUpBeforeClass(). setUp() runs before each test, whereas setUpBeforeClass() runs once before any tests in the class are loaded. I learned something more about PHPUnit today. setUpBeforeClass() is more appropriate in some cases -- e.g. some of my tests are populating the database with seed data for the tests, but I could certainly restructure some of the others.

                  Re my Snippet caching problem: I'm developing on MAMP, so the problem arises when I run tests as a non-admin user (because MAMP seems to inexplicably require that PHP and Apache run as the admin user), so running tests that require cache files to be written will fail because the limited user does not have the correct permissions to create directories and files in the core/cache/ folder. Clearly, I need to write a test for that... not sure where to write tests that must be passed *before* any other tests are run...
                    • 3749
                    • 24,544 Posts
                    Ah. I'm running my tests on XAMPP in Windows, so permissions are pretty much a non-issue.

                    I use setUp() because I want each test function to be completely independent of the others, though it does mean it takes longer to run them.

                    In one set of tests, I do have a testSetup() function as the first one, which makes sure everything went well in setup().
                      Did I help you? Buy me a beer
                      Get my Book: MODX:The Official Guide
                      MODX info for everyone: http://bobsguides.com/modx.html
                      My MODX Extras
                      Bob's Guides is now hosted at A2 MODX Hosting