Issue #2232861 by grom358, daffie, alexpott, larowlan, pfrenssen, hussainweb, pcambra, jibran, phenaproxima, moshe weitzman, nick_schuch: Create BrowserTestBase for web-testing on top of Mink

8.0.x
Alex Pott 2015-04-12 12:08:53 +01:00
parent 418497eef1
commit 300f14e860
13 changed files with 1672 additions and 55 deletions

View File

@ -725,9 +725,12 @@ function drupal_valid_test_ua($new_prefix = NULL) {
// a test environment.
$test_prefix = FALSE;
// Perform a basic check on the User-Agent HTTP request header first. Any
// inbound request that uses the simpletest UA header needs to be validated.
if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^(simpletest\d+);(.+);(.+);(.+)$/", $_SERVER['HTTP_USER_AGENT'], $matches)) {
// A valid Simpletest request will contain a hashed and salted authentication
// code. Check if this code is present in a cookie or custom user agent
// string.
$http_user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : NULL;
$user_agent = isset($_COOKIE['SIMPLETEST_USER_AGENT']) ? $_COOKIE['SIMPLETEST_USER_AGENT'] : $http_user_agent;
if (isset($user_agent) && preg_match("/^(simpletest\d+);(.+);(.+);(.+)$/", $user_agent, $matches)) {
list(, $prefix, $time, $salt, $hmac) = $matches;
$check_string = $prefix . ';' . $time . ';' . $salt;
// Read the hash salt prepared by drupal_generate_test_ua().

View File

@ -303,7 +303,8 @@ function install_begin_request($class_loader, &$install_state) {
// The user agent header is used to pass a database prefix in the request when
// running tests. However, for security reasons, it is imperative that no
// installation be permitted using such a prefix.
if ($install_state['interactive'] && strpos($request->server->get('HTTP_USER_AGENT'), 'simpletest') !== FALSE && !drupal_valid_test_ua()) {
$user_agent = $request->cookies->get('SIMPLETEST_USER_AGENT') ?: $request->server->get('HTTP_USER_AGENT');
if ($install_state['interactive'] && strpos($user_agent, 'simpletest') !== FALSE && !drupal_valid_test_ua()) {
header($request->server->get('SERVER_PROTOCOL') . ' 403 Forbidden');
exit;
}

View File

@ -443,4 +443,89 @@ abstract class Database {
public static function ignoreTarget($key, $target) {
self::$ignoreTargets[$key][$target] = TRUE;
}
/**
* Converts a URL to a database connection info array.
*
* @param string $url
* The URL.
* @param string $root
* The root directory of the Drupal installation.
*
* @return array
* The database connection info.
*
* @throws \InvalidArgumentException
* Exception thrown when the provided URL does not meet the minimum
* requirements.
*/
public static function convertDbUrlToConnectionInfo($url, $root) {
$info = parse_url($url);
if (!isset($info['scheme'], $info['host'], $info['path'])) {
throw new \InvalidArgumentException('Minimum requirement: driver://host/database');
}
$info += array(
'user' => '',
'pass' => '',
'fragment' => '',
);
// A SQLite database path with two leading slashes indicates a system path.
// Otherwise the path is relative to the Drupal root.
if ($info['path'][0] === '/') {
$info['path'] = substr($info['path'], 1);
}
if ($info['scheme'] === 'sqlite' && $info['path'][0] !== '/') {
$info['path'] = $root . '/' . $info['path'];
}
$database = array(
'driver' => $info['scheme'],
'username' => $info['user'],
'password' => $info['pass'],
'host' => $info['host'],
'database' => $info['path'],
);
if (isset($info['port'])) {
$database['port'] = $info['port'];
}
return $database;
}
/**
* Gets database connection info as a URL.
*
* @param string $key
* (Optional) The database connection key.
*
* @return string
* The connection info as a URL.
*/
public static function getConnectionInfoAsUrl($key = 'default') {
$db_info = static::getConnectionInfo($key);
if ($db_info['default']['driver'] == 'sqlite') {
$db_url = 'sqlite://localhost/' . $db_info['default']['database'];
}
else {
$user = '';
if ($db_info['default']['username']) {
$user = $db_info['default']['username'];
if ($db_info['default']['password']) {
$user .= ':' . $db_info['default']['password'];
}
$user .= '@';
}
$db_url = $db_info['default']['driver'] . '://' . $user . $db_info['default']['host'];
if (isset($db_info['default']['port'])) {
$db_url .= ':' . $db_info['default']['port'];
}
$db_url .= '/' . $db_info['default']['database'];
}
if ($db_info['default']['prefix']['default']) {
$db_url .= '#' . $db_info['default']['prefix']['default'];
}
return $db_url;
}
}

View File

@ -238,6 +238,9 @@ function simpletest_phpunit_configuration_filepath() {
* The results as returned by exec().
*/
function simpletest_phpunit_run_command(array $unescaped_test_classnames, $phpunit_file) {
// Setup an environment variable containing the database connection so that
// functional tests can connect to the database.
putenv('SIMPLETEST_DB=' . Database::getConnectionInfoAsUrl());
$phpunit_bin = simpletest_phpunit_command();
$command = array(
@ -273,6 +276,7 @@ function simpletest_phpunit_run_command(array $unescaped_test_classnames, $phpun
// via the simpletest UI.
$ret = exec(join($command, " "));
chdir($old_cwd);
putenv('SIMPLETEST_DB=');
return $ret;
}

File diff suppressed because it is too large Load Diff

View File

@ -179,6 +179,7 @@ class SimpletestTestForm extends FormBase {
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
global $base_url;
// Test discovery does not run upon form submission.
simpletest_classloader_register();
@ -209,6 +210,7 @@ class SimpletestTestForm extends FormBase {
}
}
if (!empty($tests_list)) {
putenv('SIMPLETEST_BASE_URL=' . $base_url);
$test_id = simpletest_run_tests($tests_list, 'drupal');
$form_state->setRedirect(
'simpletest.result_form',

View File

@ -79,8 +79,9 @@ class TestDiscovery {
$existing = $this->classLoader->getPrefixesPsr4();
// Add PHPUnit test namespace of Drupal core.
// Add PHPUnit test namespaces of Drupal core.
$this->testNamespaces['Drupal\\Tests\\'] = [DRUPAL_ROOT . '/core/tests/Drupal/Tests'];
$this->testNamespaces['Drupal\\FunctionalTests\\'] = [DRUPAL_ROOT . '/core/tests/Drupal/FunctionalTests'];
$this->availableExtensions = array();
foreach ($this->getExtensions() as $name => $extension) {
@ -95,8 +96,9 @@ class TestDiscovery {
// Add Simpletest test namespace.
$this->testNamespaces["Drupal\\$name\\Tests\\"][] = "$base_path/src/Tests";
// Add PHPUnit test namespace.
$this->testNamespaces["Drupal\\Tests\\$name\\"][] = "$base_path/tests/src";
// Add PHPUnit test namespaces.
$this->testNamespaces["Drupal\\Tests\\$name\\Unit\\"][] = "$base_path/tests/src/Unit";
$this->testNamespaces["Drupal\\Tests\\$name\\Functional\\"][] = "$base_path/tests/src/Functional";
}
foreach ($this->testNamespaces as $prefix => $paths) {
@ -322,7 +324,7 @@ class TestDiscovery {
throw new MissingGroupException(sprintf('Missing @group annotation in %s', $classname));
}
// Force all PHPUnit tests into the same group.
if (strpos($classname, 'Drupal\\Tests\\') === 0) {
if (static::isUnitTest($classname)) {
$info['group'] = 'PHPUnit';
}
else {
@ -407,6 +409,31 @@ class TestDiscovery {
return $annotations;
}
/**
* Determines if the provided classname is a unit test.
*
* @param $classname
* The test classname.
*
* @return bool
* TRUE if the class is a unit test. FALSE if not.
*/
public static function isUnitTest($classname) {
if (strpos($classname, 'Drupal\\Tests\\') === 0) {
$namespace = explode('\\', $classname);
$first_letter = Unicode::substr($namespace[2], 0, 1);
if (Unicode::strtoupper($first_letter) === $first_letter) {
// A core unit test.
return TRUE;
}
elseif ($namespace[3] == 'Unit') {
// A module unit test.
return TRUE;
}
}
return FALSE;
}
/**
* Returns all available extensions.
*

View File

@ -128,21 +128,23 @@ class SimpleTestBrowserTest extends WebTestBase {
// to be created. However this scenario is covered by the testception of
// \Drupal\simpletest\Tests\SimpleTestTest.
$this->drupalGet('admin/config/development/testing');
$edit = array(
$tests = array(
// A KernelTestBase test.
'tests[Drupal\field\Tests\String\StringFormatterTest]' => TRUE,
'Drupal\field\Tests\String\StringFormatterTest',
// A PHPUnit unit test.
'Drupal\Tests\action\Unit\Menu\ActionLocalTasksTest',
// A PHPUnit functional test.
'Drupal\Tests\simpletest\Functional\BrowserTestBaseTest',
);
$this->drupalPostForm(NULL, $edit, t('Run tests'));
$this->assertText('0 fails, 0 exceptions');
$this->drupalGet('admin/config/development/testing');
$edit = array(
// A PHPUnit test.
'tests[Drupal\Tests\action\Unit\Menu\ActionLocalTasksTest]' => TRUE,
);
$this->drupalPostForm(NULL, $edit, t('Run tests'));
$this->assertText('0 fails, 0 exceptions');
foreach ($tests as $test) {
$this->drupalGet('admin/config/development/testing');
$edit = array(
"tests[$test]" => TRUE,
);
$this->drupalPostForm(NULL, $edit, t('Run tests'));
$this->assertText('0 fails, 0 exceptions');
}
}
}

View File

@ -0,0 +1,72 @@
<?php
/**
* @file
* Contains \Drupal\simpletest\WebAssert.
*/
namespace Drupal\simpletest;
use Behat\Mink\WebAssert as MinkWebAssert;
use Behat\Mink\Element\TraversableElement;
use Behat\Mink\Exception\ElementNotFoundException;
/**
* Defines a class with methods for asserting presence of elements during tests.
*/
class WebAssert extends MinkWebAssert {
/**
* Checks that specific button exists on the current page.
*
* @param string $button
* One of id|name|label|value for the button.
* @param \Behat\Mink\Element\TraversableElement $container
* (optional) The document to check against. Defaults to the current page.
*
* @return \Behat\Mink\Element\NodeElement
* The matching element.
*
* @throws \Behat\Mink\Exception\ElementNotFoundException
* When the element doesn't exist.
*/
public function buttonExists($button, TraversableElement $container = NULL) {
$container = $container ?: $this->session->getPage();
$node = $container->findButton($button);
if ($node === NULL) {
throw new ElementNotFoundException($this->session, 'button', 'id|name|label|value', $button);
}
return $node;
}
/**
* Checks that specific select field exists on the current page.
*
* @param string $select
* One of id|name|label|value for the select field.
* @param \Behat\Mink\Element\TraversableElement $container
* (optional) The document to check against. Defaults to the current page.
*
* @return \Behat\Mink\Element\NodeElement
* The matching element
*
* @throws \Behat\Mink\Exception\ElementNotFoundException
* When the element doesn't exist.
*/
public function selectExists($select, TraversableElement $container = NULL) {
$container = $container ?: $this->session->getPage();
$node = $container->find('named', array(
'select',
$this->session->getSelectorsHandler()->xpathLiteral($select),
));
if ($node === NULL) {
throw new ElementNotFoundException($this->session, 'select', 'id|name|label|value', $select);
}
return $node;
}
}

View File

@ -0,0 +1,64 @@
<?php
/**
* @file
* Contains \Drupal\Tests\simpletest\Functional\BrowserTestBaseTest.
*/
namespace Drupal\Tests\simpletest\Functional;
use Drupal\simpletest\BrowserTestBase;
/**
* Tests BrowserTestBase functionality.
*
* @group simpletest
*
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
class BrowserTestBaseTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('test_page_test', 'form_test');
/**
* Tests basic page test.
*/
public function testGoTo() {
$account = $this->drupalCreateUser();
$this->drupalLogin($account);
// Visit a Drupal page that requires login.
$this->drupalGet('/test-page');
$this->assertSession()->statusCodeEquals(200);
// Test page contains some text.
$this->assertSession()->pageTextContains('Test page text.');
}
/**
* Tests basic form functionality.
*/
public function testForm() {
// Ensure the proper response code for a _form route.
$this->drupalGet('/form-test/object-builder');
$this->assertSession()->statusCodeEquals(200);
// Ensure the form and text field exist.
$this->assertSession()->elementExists('css', 'form#form-test-form-test-object');
$this->assertSession()->fieldExists('bananas');
$edit = ['bananas' => 'green'];
$this->submitForm($edit, 'Save', 'form-test-form-test-object');
$config_factory = $this->container->get('config.factory');
$value = $config_factory->get('form_test.object')->get('bananas');
$this->assertSame('green', $value);
}
}

View File

@ -20,6 +20,7 @@ class TestInfoParsingTest extends UnitTestCase {
}
public function infoParserProvider() {
// A module provided unit test.
$tests[] = [
// Expected result.
[
@ -31,6 +32,32 @@ class TestInfoParsingTest extends UnitTestCase {
'Drupal\Tests\simpletest\Unit\TestInfoParsingTest',
];
// A core unit test.
$tests[] = [
// Expected result.
[
'name' => 'Drupal\Tests\Core\DrupalTest',
'group' => 'PHPUnit',
'description' => 'Tests \Drupal.',
],
// Classname.
'Drupal\Tests\Core\DrupalTest',
];
// Functional PHPUnit test.
$tests[] = [
// Expected result.
[
'name' => 'Drupal\Tests\simpletest\Functional\BrowserTestBaseTest',
'group' => 'simpletest',
'description' => 'Tests BrowserTestBase functionality.',
],
// Classname.
'Drupal\Tests\simpletest\Functional\BrowserTestBaseTest',
];
// Simpletest classes can not be autoloaded in a PHPUnit test, therefore
// provide a docblock.
$tests[] = [
// Expected result.
[

View File

@ -6,19 +6,31 @@
<ini name="error_reporting" value="32767"/>
<!-- Do not limit the amount of memory tests take to run. -->
<ini name="memory_limit" value="-1"/>
<env name="SIMPLETEST_BASE_URL" value=""/>
<!-- Example SIMPLETEST_BASE_URL value: http://localhost -->
<env name="SIMPLETEST_DB" value=""/>
<!-- Example SIMPLETEST_DB value: mysql://username:password@localhost/databasename#table_prefix -->
</php>
<testsuites>
<testsuite name="Drupal Unit Test Suite">
<directory>./tests</directory>
<directory>./modules/*/tests</directory>
<directory>../modules</directory>
<directory>../sites/*/modules</directory>
<testsuite name="unit">
<directory>./tests/Drupal/Tests</directory>
<directory>./modules/*/tests/src/Unit</directory>
<directory>../modules/*/tests/src/Unit</directory>
<directory>../sites/*/modules/*/tests/src/Unit</directory>
<!-- Exclude Composer's vendor directory so we don't run tests there. -->
<exclude>./vendor</exclude>
<!-- Exclude Drush tests. -->
<exclude>./drush/tests</exclude>
</testsuite>
<testsuite name="functional">
<directory>./tests/Drupal/FunctionalTests</directory>
<directory>./modules/*/tests/src/Functional</directory>
<directory>../modules/*/tests/src/Functional</directory>
<directory>../sites/*/modules/*/tests/src/Functional</directory>
<!-- Exclude Composer's vendor directory so we don't run tests there. -->
<exclude>./vendor</exclude>
<!-- Exclude Drush tests. -->
<exclude>./drush/tests</exclude>
<!-- Exclude special-case files from config's test modules. -->
<exclude>./modules/config/tests/config_test/src</exclude>
</testsuite>
</testsuites>
<listeners>

View File

@ -356,6 +356,17 @@ function simpletest_script_init() {
}
}
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
$base_url = 'https://';
}
else {
$base_url = 'http://';
}
$base_url .= $host;
if ($path !== '') {
$base_url .= $path;
}
putenv('SIMPLETEST_BASE_URL=' . $base_url);
$_SERVER['HTTP_HOST'] = $host;
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_ADDR'] = '127.0.0.1';
@ -419,36 +430,13 @@ function simpletest_script_setup_database($new = FALSE) {
if (!empty($args['dburl'])) {
// Remove a possibly existing default connection (from settings.php).
Database::removeConnection('default');
$info = parse_url($args['dburl']);
if (!isset($info['scheme'], $info['host'], $info['path'])) {
simpletest_script_print_error('Invalid --dburl. Minimum requirement: driver://host/database');
try {
$databases['default']['default'] = Database::convertDbUrlToConnectionInfo($args['dburl'], DRUPAL_ROOT);
}
catch (\InvalidArgumentException $e) {
simpletest_script_print_error('Invalid --dburl. Reason: ' . $e->getMessage());
exit(1);
}
$info += array(
'user' => '',
'pass' => '',
'fragment' => '',
);
if ($info['path'][0] === '/') {
$info['path'] = substr($info['path'], 1);
}
if ($info['scheme'] === 'sqlite' && $info['path'][0] !== '/') {
$info['path'] = DRUPAL_ROOT . '/' . $info['path'];
}
$databases['default']['default'] = array(
'driver' => $info['scheme'],
'username' => $info['user'],
'password' => $info['pass'],
'host' => $info['host'],
'database' => $info['path'],
'prefix' => array(
'default' => $info['fragment'],
),
);
if (isset($info['port'])) {
$databases['default']['default']['port'] = $info['port'];
}
}
// Otherwise, use the default database connection from settings.php.
else {