Note:

If you want to create a new page for developers, you should create it on the Moodle Developer Resource site.

Unit test API

From MoodleDocs
Revision as of 12:47, 23 April 2009 by Frank Ralf (talk | contribs) (→‎Calls to global functions: sub-heading added)

Moodle1.7 Location: Administration > Reports > Unit tests


The purpose of Unit Tests is to evaluate the individual parts of a program (functions, and methods of classes) to make sure that each element individually does the right thing. Unit Tests can be one of the first steps in a quality control process for developing or tweaking Moodle code. The next steps will involve other forms of testing to ensure that these different parts work together properly.

The unit testing framework is based on the SimpleTest framework.

Running the unit tests in Moodle

Running the basic tests

  1. Log in with an admin account.
  2. Administration ► Development ► Unit tests (moodle >= 2.0, Administration ► Reports ► Unit tests Moodle <= 1.9)
  3. Click on the Reports link near the bottom of the page.
  4. Click the Run tests button and wait.

This finds all the tests in Moodle and runs them. You can run a subset of the tests by entering a path (for example question/type) in the 'Only run tests in' box. Similarly, if a test fails, you get some links in the failure message to make it easy to re-run just those tests.

Writing new tests

As an example, suppose we wanted to write some tests for the string_manager class in mod/quiz/editlib.php.

Where to put the tests

The unit test report finds tests by looking for folders called 'test....php' inside folders called 'simpletest'.

So, for our example, we want to create called something like mod/quiz/simpletest/testeditlib.php. The skeleton of this file should look like:

<?php /**

* Unit tests for (some of) mod/quiz/editlib.php.
*
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package question
*/

if (!defined('MOODLE_INTERNAL')) {

   die('Direct access to this script is forbidden.'); //  It must be included from a Moodle page

}

// Make sure the code being tested is accessible. require_once($CFG->dirroot . '/mod/quiz/editlib.php'); // Include the code to test

/** This class contains the test cases for the functions in editlib.php. */ class quiz_editlib_test extends UnitTestCase {

   function test_something() {
       // Do the test here.
   }
   // ... more test methods.

} ?>

That is, you have a class called something_test, and in that class you have lots of methods called test_something. Normally, you have one test method for each particular thing you want to test, and you should try to name the function to describe what is being tested - without making the name too ridiculously long!

A test function

The a test function typically looks like

function test_move_question_up() {

   // Setup fixture
   // Exercise SUT
   $newlayout = quiz_move_question_up('1,2,0', 2);
   // Validate outcome
   $this->assertEqual($newlayout, '2,1,0');
   // Teardown fixture

}

This is the four phase test pattern. Those comments use a lot of testing jargon. The fixture is is the background situation that needs to be set up before the test runs. SUT is short for 'situation under test'. This is where you call the function or method that you want to test. Then you check to see if the function did the right thing. Finally, you have to clean up the fixture you created. With luck there is nothing to do here

In this simple example, there is no setup or teardown to do. We just call the function we are testing with some sample input, and check that the return value is what we expect.

Shared setUp and tearDown methods

If all your test cases relate to the same area of code, then they may all need to same bit of fixture set up. For example, all the tests in lib/simpletest/teststringmanager.php need an instance of the string_manager class to test.

To avoid duplicating code, you can override a method called setUp() that sets up the test data. If present, this method will be called before each test method. You can write a matching tearDown() method if there is any clean-up that needs to be done after each test case has run. For example, in lib/simpletest/teststringmanager.php there are setUp and tearDown methods that do something like: public function setUp() {

   // ...
   $this->stringmanager = new string_manager(...);

}

public function tearDown() {

   $this->stringmanager = null;

} Then, each test can use $this->stringmanager without having to worry about the details of how it is set up.

Further information

The SimpleTest documentation is at: http://www.simpletest.org/.

Changes to your existing code to make it work with unit testing

The whole point of unit testing is to test each piece of functionality separately. You can only do this is only possible to isolate that function and call it individually, perhaps after setting up a few other things.

Therefore, it is good if you can write your code to depend on as few other things as possible.

Include paths

Includes like

require_once('../../config.php'); // Won't work.

won't work. Instead, the more robust option is

require_once(dirname(__FILE__) . '/../../config.php'); // Do this.

Access to global variables

Because your code was included from within a function, you can't access global variables until you have done a global statement.

require_once(dirname(__FILE__) . '/../../config.php'); require_once($CFG->libdir . '/moodlelib.php'); // Won't work.

require_once(dirname(__FILE__) . '/../../config.php');

global $CFG; // You need this. require_once($CFG->libdir . '/moodlelib.php'); // Will work now.

Calls to global functions

Testing a class method that calls global functions can be problematic. At least, it's always complex, because we can't control what goes on in the global functions. We can't override the global functions or mock them in our unit tests. If the global functions themselves are well tested, this may not be a big problem, but most global functions are not well tested.

Bridge Pattern

If your code needs to rely extensively on some public API, you could use the bridge pattern to decouple your code from that API. This way, when you write unit tests, you can override the bridging class or mock it, and control its outputs while you focus exclusively on testing your code.

An basic example follows: Imagine that I do not trust the get_string() global function, but my code needs to use it. Initially my code has strong coupling with get_string():

class myclass {

   public function print_stuff($stuff) {
       echo get_string($stuff);
   }

}

Now let's write a bridging class to solve this coupling issue and use it instead of get_string():

class languageBridge {

   public function get_string($stuff,$module='moodle') {
       echo get_string($stuff, $module);
   }

}

class myclass {

   public $lang_bridge;
   public function __construct() {
       $this->lang_bridge = new languageBridge();
   }
   public function print_stuff($stuff) {
       echo $this->lang_bridge->get_string($stuff);
   }

}

Unit testing in 2.0

Moodle 2.0


With the Objectification of the Database libraries in Moodle 2.0, new and better approaches to Unit testing can be used. Here is a sample of a simple test case: (in course/simpletest)

require_once($CFG->dirroot . '/course/lib.php');

global $DB;
Mock::generate(get_class($DB), 'mockDB');

class courselib_test extends UnitTestCase {
    var $realDB;

    function setUp() {
        global $DB;
        $this->realDB = $DB;
        $DB           = new mockDB();
    }

    function tearDown() {
        global $DB;
        $DB = $this->realDB;
    }

    function testMoveSection() {
        global $DB;
        $course = new stdClass();
        $course->id = 1;

        $sections = array();
        for ($i = 1; $i < 11; $i++) {
            $sections[$i]          = new stdClass();
            $sections[$i]->id      = $i;
            $sections[$i]->section = $i - 1;
        }

        $DB->expectOnce('get_records', array('course_sections', array('course' => $course->id)));
        $DB->setReturnValue('get_records', $sections);
        $this->assertFalse(move_section($course, 2, 3));
    }
}

See also UnitTestCaseUsingDatabase in lib/simpletestlib.php.

A warning

In 'xUnit Test Patterns' there is a scale of testing difficulty that goes from 1. to 6. Moodle is definitely at number 6. on that scale 'Non-object-oriented legacy software'. It then goes recommend that you don't start to learn about unit testing with that sort of software :-(

Further reading about unit testing

PHP in Action has an excellent chapter explaining unit testing in PHP with simpletest. (Although the rest of that book advocates a style of programming that is very different from the style used in Moodle.)

Pragmatic Unit Testing in Java with JUnit is also a very good introduction, despite being in the wrong programming language. JUnit and Simpletest are very similar.

xUnit Test Patterns is the ultimate unit test book. I think it teaches you everything you could learn about unit testing by reading a book. The only way to learn more would be years of experience. It has really great advice for dealing with the kind of messy problems you get in a big, real project like Moodle.

Template:CategoryDeveloper