Commit f2743e8a authored by Dries's avatar Dries

Issue #1901670 by msonnabaum, kim.pepper, effulgentsia: Start using PHPUnit for unit tests.

parent d96e6a54
......@@ -6,14 +6,16 @@
namespace Drupal\breakpoint\Tests;
use Drupal\simpletest\UnitTestBase;
use Drupal\Tests\UnitTestCase;
use Drupal\breakpoint\Plugin\Core\Entity\Breakpoint;
use Drupal\breakpoint\InvalidBreakpointMediaQueryException;
/**
* Tests for media queries in a breakpoint.
*
* @group Breakpoint
*/
class BreakpointMediaQueryTest extends UnitTestBase {
class BreakpointMediaQueryTest extends UnitTestCase {
public static function getInfo() {
return array(
......@@ -116,7 +118,7 @@ public function testInvalidMediaQueries() {
$this->assertFalse(Breakpoint::isValidMediaQuery($media_query), $media_query . ' is not valid.');
}
catch (InvalidBreakpointMediaQueryException $e) {
$this->assertTrue(TRUE, format_string('%media_query is not valid.', array('%media_query' => $media_query)));
$this->assertTrue(TRUE, sprintf('%s is not valid.', $media_query));
}
}
}
......
......@@ -131,6 +131,18 @@ function simpletest_run_tests($test_list, $reporter = 'drupal') {
->useDefaults(array('test_id'))
->execute();
$phpunit_tests = isset($test_list['UnitTest']) ? $test_list['UnitTest'] : array();
$phpunit_results = simpletest_run_phpunit_tests($test_id, $phpunit_tests);
simpletest_process_phpunit_results($phpunit_results);
if (!array_key_exists('WebTest', $test_list) || empty($test_list['WebTest'])) {
// Early return if there are no WebTests to run.
return $test_id;
}
// Contine with SimpleTests only.
$test_list = $test_list['WebTest'];
// Clear out the previous verbose files.
file_unmanaged_delete_recursive('public://simpletest/verbose');
......@@ -157,6 +169,104 @@ function simpletest_run_tests($test_list, $reporter = 'drupal') {
return $test_id;
}
/**
* Executes phpunit tests and returns the results of the run.
*
* @param $test_id
* The current test ID.
* @param $unescaped_test_classnames
* An array of test class names, including full namespaces, to be passed as
* a regular expression to phpunit's --filter option.
*
* @return array
* The parsed results of phpunit's junit xml output, in the format of the
* simpletest table's schema.
*/
function simpletest_run_phpunit_tests($test_id, array $unescaped_test_classnames) {
$phpunit_file = simpletest_phpunit_xml_filepath($test_id);
simpletest_phpunit_run_command($unescaped_test_classnames, $phpunit_file);
return simpletest_phpunit_xml_to_rows($test_id, $phpunit_file);
}
/**
* Inserts the parsed phpunit results into the simpletest table.
*
* @param array $phpunit_results
* An array of test results returned from simpletest_phpunit_xml_to_rows.
*/
function simpletest_process_phpunit_results($phpunit_results) {
// Insert the results of the phpunit test run into the db so the results are
// displayed along with simpletest's results.
if (!empty($phpunit_results)) {
$query = db_insert('simpletest')->fields(array_keys($phpunit_results[0]));
foreach ($phpunit_results as $result) {
$query->values($result);
}
$query->execute();
}
}
/**
* Returns the path to use for phpunit's --log-junit option.
*
* @param $test_id
* The current test ID.
* @return string
* Path to the phpunit xml file to use for the current test_id.
*/
function simpletest_phpunit_xml_filepath($test_id) {
return drupal_realpath('public://simpletest') . '/phpunit-' . $test_id . '.xml';
}
/**
* Returns the path to core's phpunit.xml.dist configuration file.
*
* @return string
* Path to core's phpunit.xml.dist configuration file.
*/
function simpletest_phpunit_configuration_filepath() {
return DRUPAL_ROOT . '/core/phpunit.xml.dist';
}
/**
* Executes the phpunit command.
*
* @param array $unescaped_test_classnames
* An array of test class names, including full namespaces, to be passed as
* a regular expression to phpunit's --filter option.
* @param string $phpunit_file
* A filepath to use for phpunit's --log-junit option.
*/
function simpletest_phpunit_run_command(array $unescaped_test_classnames, $phpunit_file) {
$phpunit_bin = DRUPAL_ROOT . "/core/vendor/bin/phpunit";
// Double escape namespaces so they'll work in a regexp.
$escaped_test_classnames = array_map(function($class) {
return addslashes($class);
}, $unescaped_test_classnames);
$filter_string = implode("|", $escaped_test_classnames);
$command = array(
$phpunit_bin,
'--filter',
escapeshellarg($filter_string),
'--log-junit',
escapeshellarg($phpunit_file),
);
// Need to change directories before running the command so that we can use
// relative paths in the configuration file's exclusions.
$old_cwd = getcwd();
chdir(DRUPAL_ROOT . "/core");
// exec in a subshell so that the environment is isolated when running tests
// via the simpletest UI.
$ret = exec(join($command, " "));
chdir($old_cwd);
return $ret;
}
/**
* Batch operation callback.
*/
......@@ -398,10 +508,23 @@ function simpletest_classloader_register() {
$matches = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.' . $info['extension'] . '$/', $info['dir']);
foreach ($matches as $name => $file) {
drupal_classloader_register($name, dirname($file->uri));
drupal_classloader()->registerNamespace('Drupal\\' . $name . '\\Tests', DRUPAL_ROOT . '/' . dirname($file->uri) . '/tests');
// While being there, prime drupal_get_filename().
drupal_get_filename($type, $name, $file->uri);
}
}
// Register the core test directory so we can find Drupal\UnitTestCase.
drupal_classloader()->registerNamespace('Drupal\\Tests', DRUPAL_ROOT . '/core/tests');
// Manually register phpunit prefixes because they use a classmap instead of a
// prefix. This can be safely removed if we move to using composer's
// autoloader with a classmap.
drupal_classloader()->registerPrefixes(array(
'PHPUnit' => DRUPAL_ROOT . '/core/vendor/phpunit/phpunit',
'File_Iterator' => DRUPAL_ROOT . '/core/vendor/phpunit/php-file-iterator/',
'PHP_Timer' => DRUPAL_ROOT . '/core/vendor/phpunit/php-timer/',
));
}
/**
......@@ -452,7 +575,8 @@ function simpletest_clean_environment() {
}
/**
* Removed prefixed tables from the database that are left over from crashed tests.
* Removed prefixed tables from the database that are left over from crashed
* tests.
*/
function simpletest_clean_database() {
$tables = db_find_tables(Database::getConnection()->prefixTables('{simpletest}') . '%');
......@@ -569,3 +693,67 @@ function simpletest_library_info() {
return $libraries;
}
/**
* Get PHPUnit Classes
*
* @param bool $name_only
* If TRUE, returns a flat array of class names only.
*/
function simpletest_phpunit_get_available_tests() {
// Load the PHPUnit configuration file, which tells us where to find the
// tests.
$phpunit_config = simpletest_phpunit_configuration_filepath();
$configuration = PHPUnit_Util_Configuration::getInstance($phpunit_config);
// Find all the tests and get a list of unique class names.
$test_suite = $configuration->getTestSuiteConfiguration(NULL);
$test_classes = array();
foreach ($test_suite AS $test) {
$name = get_class($test);
if (!array_key_exists($name, $test_classes)) {
$test_classes[$name] = $test->getInfo();
}
}
return $test_classes;
}
/**
* Converts phpunit's junit xml output to an array.
*
* The returned array of rows is in a format that can be inserted into the
* simpletest results table.
*
* @param $test_id
* The current test ID.
* @param $phpunit_xml_file
* Path to the phpunit xml file.
*/
function simpletest_phpunit_xml_to_rows($test_id, $phpunit_xml_file) {
$contents = file_get_contents($phpunit_xml_file);
$xml = new SimpleXMLElement($contents);
$records = array();
foreach ($xml->testsuite as $testsuite) {
foreach ($testsuite as $suite) {
foreach ($suite as $testcase) {
$message = '';
if ($testcase->failure) {
$lines = explode("\n", $testcase->failure);
$message = $lines[2];
}
$attributes = $testcase->attributes();
$records[] = array(
'test_id' => $test_id,
'test_class' => (string)$attributes->class,
'status' => empty($testcase->failure) ? 'pass' : 'fail',
'message' => $message,
'message_group' => 'Other', // TODO: Check on the proper values for this.
'function' => $attributes->class . '->' . $attributes->name . '()',
'line' => (string)$attributes->line,
'file' => (string)$attributes->file,
);
}
}
}
return $records;
}
......@@ -8,7 +8,7 @@
/**
* List tests arranged in groups that can be selected and run.
*/
function simpletest_test_form($form) {
function simpletest_test_form($form, &$form_state) {
$form['tests'] = array(
'#type' => 'details',
'#title' => t('Tests'),
......@@ -21,6 +21,9 @@ function simpletest_test_form($form) {
// Generate the list of tests arranged by group.
$groups = simpletest_test_get_all();
$groups['PHPUnit'] = simpletest_phpunit_get_available_tests();
$form_state['storage']['PHPUnit'] = $groups['PHPUnit'];
foreach ($groups as $group => $tests) {
$form['tests']['table'][$group] = array(
'#collapsed' => TRUE,
......@@ -180,11 +183,15 @@ function simpletest_test_form_submit($form, &$form_state) {
// Get list of tests.
$tests_list = array();
simpletest_classloader_register();
$phpunit_all = array_keys($form_state['storage']['PHPUnit']);
foreach ($form_state['values'] as $class_name => $value) {
// Since class_exists() will likely trigger an autoload lookup,
// we do the fast check first.
if ($value === 1 && class_exists($class_name)) {
$tests_list[] = $class_name;
$test_type = in_array($class_name, $phpunit_all) ? 'UnitTest' : 'WebTest';
$tests_list[$test_type][] = $class_name;
}
}
if (count($tests_list) > 0 ) {
......
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap.php" colors="true">
<testsuites>
<testsuite name="Drupal Unit Test Suite">
<directory>./tests/*</directory>
<directory>./modules/*/tests/*</directory>
<directory>../modules/*/tests/*</directory>
<directory>../sites/*/modules/*/tests/*</directory>
<!-- Exclude files that end in Test.php that aren't actually phpunit tests. -->
<exclude>./modules/config/tests/config_test/lib/Drupal/config_test</exclude>
<exclude>./modules/views/tests/views_test_data/lib/Drupal/views_test_data</exclude>
</testsuite>
</testsuites>
</phpunit>
......@@ -51,7 +51,7 @@
// Display all available tests.
echo "\nAvailable test groups & classes\n";
echo "-------------------------------\n\n";
$groups = simpletest_test_get_all();
$groups = simpletest_script_get_all_tests();
foreach ($groups as $group => $tests) {
echo $group . "\n";
foreach ($tests as $class => $info) {
......@@ -315,23 +315,68 @@ function simpletest_script_init($server_software) {
require_once DRUPAL_ROOT . '/core/includes/bootstrap.inc';
}
/**
* Get all available tests from simpletest and PHPUnit.
*
* @return
* An array of tests keyed with the groups specified in each of the tests
* getInfo() method and then keyed by the test class. An example of the array
* structure is provided below.
*
* @code
* $groups['Block'] => array(
* 'BlockTestCase' => array(
* 'name' => 'Block functionality',
* 'description' => 'Add, edit and delete custom block...',
* 'group' => 'Block',
* ),
* );
* @endcode
*/
function simpletest_script_get_all_tests() {
$tests = simpletest_test_get_all();
$tests['PHPUnit'] = simpletest_phpunit_get_available_tests();
return $tests;
}
/**
* Execute a batch of tests.
*/
function simpletest_script_execute_batch($test_classes) {
function simpletest_script_execute_batch($test_groups) {
global $args, $test_ids;
// Separate PHPUnit tests from simpletest.
if (isset($test_groups['PHPUnit'])) {
$phpunit_tests = $test_groups['PHPUnit'];
unset($test_groups['PHPUnit']);
}
// Flatten the simpletest tests into an array of classnames.
$test_classes = array();
foreach ($test_groups as $group) {
$test_classes = array_merge($test_classes, array_values($group));
}
// Multi-process execution.
$children = array();
while (!empty($test_classes) || !empty($children)) {
while (!empty($test_classes) || !empty($children) || isset($phpunit_tests)) {
while (count($children) < $args['concurrency']) {
if (empty($test_classes)) {
if (empty($test_classes) && !isset($phpunit_tests)) {
break;
}
// Fork a child process.
$test_id = db_insert('simpletest_test_id')->useDefaults(array('test_id'))->execute();
$test_ids[] = $test_id;
// Process phpunit tests immediately since they are fast and we don't need
// to fork for them.
if (isset($phpunit_tests)) {
simpletest_script_run_phpunit($test_id, $phpunit_tests);
unset($phpunit_tests);
continue;
}
// Fork a child process.
$test_class = array_shift($test_classes);
$command = simpletest_script_command($test_id, $test_class);
$process = proc_open($command, array(), $pipes, NULL, NULL, array('bypass_shell' => TRUE));
......@@ -383,6 +428,51 @@ function simpletest_script_execute_batch($test_classes) {
}
}
/**
* Run a group of phpunit tests.
*/
function simpletest_script_run_phpunit($test_id, $phpunit_tests) {
$results = simpletest_run_phpunit_tests($test_id, $phpunit_tests);
simpletest_process_phpunit_results($results);
// Map phpunit results to a data structure we can pass to
// _simpletest_format_summary_line.
$summaries = array();
foreach ($results as $result) {
if (!isset($summaries[$result['test_class']])) {
$summaries[$result['test_class']] = array(
'#pass' => 0,
'#fail' => 0,
'#exception' => 0,
'#debug' => 0,
);
}
switch ($result['status']) {
case 'pass':
$summaries[$result['test_class']]['#pass']++;
break;
case 'fail':
$summaries[$result['test_class']]['#fail']++;
break;
case 'exception':
$summaries[$result['test_class']]['#exception']++;
break;
case 'debug':
$summary['#exception']++;
break;
}
}
foreach ($summaries as $class => $summary) {
$had_fails = $summary['#fail'] > 0;
$had_exceptions = $summary['#exception'] > 0;
$status = ($had_fails || $had_exceptions ? 'fail' : 'pass');
$info = call_user_func(array($class, 'getInfo'));
simpletest_script_print($info['name'] . ' ' . _simpletest_format_summary_line($summary) . "\n", simpletest_script_color_code($status));
}
}
/**
* Bootstrap Drupal and run a single test.
*/
......@@ -535,10 +625,10 @@ function simpletest_script_get_test_list() {
$test_list = array();
if ($args['all']) {
$groups = simpletest_test_get_all();
$groups = simpletest_script_get_all_tests();
$all_tests = array();
foreach ($groups as $group => $tests) {
$all_tests = array_merge($all_tests, array_keys($tests));
$all_tests[$group] = array_keys($tests);
}
$test_list = $all_tests;
}
......@@ -578,20 +668,28 @@ function simpletest_script_get_test_list() {
// Extract all class names.
// Abstract classes are excluded on purpose.
preg_match_all('@^class ([^ ]+)@m', $content, $matches);
require $file;
if (!$namespace) {
$test_list = array_merge($test_list, $matches[1]);
$info = $matches[1][0]::getInfo();
$test_list[$info[$group]] = $matches[1][0];
}
else {
$class = '\\' . $namespace . '\\' . $matches[1][0];
$info = $class::getInfo();
foreach ($matches[1] as $class_name) {
$test_list[] = $namespace . '\\' . $class_name;
if (!isset($test_list[$info['group']])) {
$test_list[$info['group']] = array();
}
$test_list[$info['group']][] = $namespace . '\\' . $class_name;
}
}
}
}
else {
$groups = simpletest_test_get_all();
$groups = simpletest_script_get_all_tests();
foreach ($args['test_names'] as $group_name) {
$test_list = array_merge($test_list, array_keys($groups[$group_name]));
$test_list[$group_name] = array_keys($groups[$group_name]);
}
}
}
......@@ -626,9 +724,11 @@ function simpletest_script_reporter_init() {
}
else {
echo "Tests to be run:\n";
foreach ($test_list as $class_name) {
$info = call_user_func(array($class_name, 'getInfo'));
echo " - " . $info['name'] . ' (' . $class_name . ')' . "\n";
foreach ($test_list as $group) {
foreach ($group as $class_name) {
$info = call_user_func(array($class_name, 'getInfo'));
echo " - " . $info['name'] . ' (' . $class_name . ')' . "\n";
}
}
echo "\n";
}
......
......@@ -2,18 +2,20 @@
/**
* @file
* Definition of Drupal\system\Tests\Cache\NullBackendTest.
* Definition of Drupal\Tests\Core\Cache\NullBackendTest.
*/
namespace Drupal\system\Tests\Cache;
namespace Drupal\Tests\Core\Cache;
use Drupal\Core\Cache\NullBackend;
use Drupal\simpletest\UnitTestBase;
use Drupal\Tests\UnitTestCase;
/**
* Tests the cache NullBackend.
*
* @group Cache
*/
class NullBackendTest extends UnitTestBase {
class NullBackendTest extends UnitTestCase {
public static function getInfo() {
return array(
......
......@@ -2,18 +2,20 @@
/**
* @file
* Contains \Drupal\system\Tests\Common\NestedArrayUnitTest.
* Contains \Drupal\Core\NestedArrayUnitTest.
*/
namespace Drupal\system\Tests\Common;
namespace Drupal\Tests\Core;
use Drupal\Component\Utility\NestedArray;
use Drupal\simpletest\UnitTestBase;
use Drupal\Tests\UnitTestCase;
/**
* Tests the NestedArray helper class.
*
* @group System
*/
class NestedArrayUnitTest extends UnitTestBase {
class NestedArrayUnitTest extends UnitTestCase {
/**
* Form array to check.
......@@ -51,26 +53,26 @@ function setUp() {
function testGetValue() {
// Verify getting a value of a nested element.
$value = NestedArray::getValue($this->form, $this->parents);
$this->assertEqual($value['#value'], 'Nested element', 'Nested element value found.');
$this->assertEquals($value['#value'], 'Nested element', 'Nested element value found.');
// Verify changing a value of a nested element by reference.
$value = &NestedArray::getValue($this->form, $this->parents);
$value['#value'] = 'New value';
$value = NestedArray::getValue($this->form, $this->parents);
$this->assertEqual($value['#value'], 'New value', 'Nested element value was changed by reference.');
$this->assertEqual($this->form['details']['element']['#value'], 'New value', 'Nested element value was changed by reference.');
$this->assertEquals($value['#value'], 'New value', 'Nested element value was changed by reference.');
$this->assertEquals($this->form['details']['element']['#value'], 'New value', 'Nested element value was changed by reference.');
// Verify that an existing key is reported back.
$key_exists = NULL;
NestedArray::getValue($this->form, $this->parents, $key_exists);
$this->assertIdentical($key_exists, TRUE, 'Existing key found.');
$this->assertSame($key_exists, TRUE, 'Existing key found.');
// Verify that a non-existing key is reported back and throws no errors.
$key_exists = NULL;
$parents = $this->parents;
$parents[] = 'foo';
NestedArray::getValue($this->form, $parents, $key_exists);
$this->assertIdentical($key_exists, FALSE, 'Non-existing key not found.');
$this->assertSame($key_exists, FALSE, 'Non-existing key not found.');
}
/**
......@@ -84,8 +86,8 @@ function testSetValue() {
// Verify setting the value of a nested element.
NestedArray::setValue($this->form, $this->parents, $new_value);
$this->assertEqual($this->form['details']['element']['#value'], 'New value', 'Changed nested element value found.');
$this->assertIdentical($this->form['details']['element']['#required'], TRUE, 'New nested element value found.');
$this->assertEquals($this->form['details']['element']['#value'], 'New value', 'Changed nested element value found.');
$this->assertSame($this->form['details']['element']['#required'], TRUE, 'New nested element value found.');
}
/**
......@@ -99,13 +101,13 @@ function testUnsetValue() {
$parents[] = 'foo';
NestedArray::unsetValue($this->form, $parents, $key_existed);
$this->assertTrue(isset($this->form['details']['element']['#value']), 'Outermost nested element key still exists.');
$this->assertIdentical($key_existed, FALSE, 'Non-existing key not found.');
$this->assertSame($key_existed, FALSE, 'Non-existing key not found.');
// Verify unsetting a nested element.
$key_existed = NULL;
NestedArray::unsetValue($this->form, $this->parents, $key_existed);
$this->assertFalse(isset($this->form['details']['element']), 'Removed nested element not found.');
$this->assertIdentical($key_existed, TRUE, 'Existing key was found.');
$this->assertSame($key_existed, TRUE, 'Existing key was found.');
}
/**
......@@ -113,12 +115,12 @@ function testUnsetValue() {
*/
function testKeyExists() {
// Verify that existing key is found.
$this->assertIdentical(NestedArray::keyExists($this->form, $this->parents), TRUE, 'Nested key found.');
$this->assertSame(NestedArray::keyExists($this->form, $this->parents), TRUE, 'Nested key found.');
// Verify that non-existing keys are not found.
$parents = $this->parents;
$parents[] = 'foo';
$this->assertIdentical(NestedArray::keyExists($this->form, $parents), FALSE, 'Non-existing nested key not found.');
$this->assertSame(NestedArray::keyExists($this->form, $parents), FALSE, 'Non-existing nested key not found.');
}
/**
......@@ -141,6 +143,6 @@ function testMergeDeepArray() {
'language' => 'en',
'html' => TRUE,
);
$this->assertIdentical(NestedArray::mergeDeepArray(array($link_options_1, $link_options_2)), $expected, 'NestedArray::mergeDeepArray() returned a properly merged array.');
$this->assertSame(NestedArray::mergeDeepArray(array($link_options_1, $link_options_2)), $expected, 'NestedArray::mergeDeepArray() returned a properly merged array.');
}
}
<?php
/**
* @file
* Contains \Drupal\Tests\UnitTestCase.
*/
namespace Drupal\Tests;
class UnitTestCase extends \PHPUnit_Framework_TestCase {
/**
* This method exists to support the simpletest UI runner.
*
* It should eventually be replaced with something native to phpunit.
*
* Also, this method is empty because you can't have an abstract static
* method. Sub-classes should always override it.
*
* @return array
* An array describing the test like so:
* array(
* 'name' => 'Something Test',
* 'description' => 'Tests Something',
* 'group' => 'Something',
* )
*/
public static function getInfo() {
throw new \RuntimeException("Sub-class must implement the getInfo method!");
}
/**
* Generates a random string containing letters and numbers.
*
* The string will always start with a letter. The letters may be upper or
* lower case. This method is better for restricted inputs that do not accept
* certain characters. For example, when testing input fields that require
* machine readable values (i.e. without spaces and non-standard characters)
* this method is best.
*
* Do not use this method when testing unvalidated user input. Instead, use
* Drupal\simpletest\TestBase::randomString().
*
* @param int $length
* Length of random string to generate.
*
* @return string
* Randomly generated string.
*
* @see Drupal\simpletest\TestBase::randomString()
*/
public static function randomName($length = 8) {
$values = array_merge(range(65, 90), range(97, 122), range(48, 57));
$max = count($values) - 1;
$str = chr(mt_rand(97, 122));
for ($i = 1; $i < $length; $i++) {
$str .= chr($values[mt_rand(0, $max)]);
}
return $str;
}
}
<?php
// Register the namespaces we'll need to autoload from.
$loader = require __DIR__ . "/../vendor/autoload.php";
$loader->add('Drupal\\', __DIR__);
$loader->add('Drupal\Core', __DIR__ . "/../../core/lib");
$loader->add('Drupal\Component', __DIR__ . "/../../core/lib");
foreach (scandir(__DIR__ . "/../modules") as $module) {
$loader->add('Drupal\\' . $module, __DIR__ . "/../modules/" . $module . "/lib");
}
// Look into removing this later.
define('REQUEST_TIME', (int) $_SERVER['REQUEST_TIME']);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment