API Removed custom dev/tests/ execution

Rely on standard vendor/bin/phpunit via CLI instead.
See https://github.com/silverstripe/silverstripe-framework/issues/4254

- Not disabling xdebug. That should be harmful, and is actively harming test development
- No longer able to specifically skip tests via the `SkipTests` request parameter. Use phpunit.xml groups and the `--exclude-group` CLI argument instead
- No longer able to specify multiple comma-separated module folders. use phpunit.xml groups and the `--group` CLI argument instead
- Not explicitly calling `SSViewer::flush_template_cache()` (was never the case on running `phpunit` CLI anyway, and shouldn't be required any more)
This commit is contained in:
Ingo Schommer 2015-07-16 21:32:42 +12:00
parent a16588aac3
commit d1af214ef5
18 changed files with 112 additions and 1212 deletions

View File

@ -7,11 +7,6 @@ DevelopmentAdmin:
controller: 'DevBuildController'
links:
build: 'Build/rebuild this environment. Call this whenever you have updated your project sources'
tests:
controller: 'TestRunner'
links:
tests: 'See a list of unit tests to run'
'tests/all': 'Run all tests'
tasks:
controller: 'TaskRunner'
links:

View File

@ -114,12 +114,10 @@ class CliTestReporter extends SapphireTestReporter {
protected function writeTest($test) {
if ($test['status'] != TEST_SUCCESS) {
$filteredTrace = array();
$ignoredClasses = array('TestRunner');
foreach($test['trace'] as $item) {
if(isset($item['file'])
&& strpos($item['file'], 'PHPUnit/Framework') === false
&& (!isset($item['class']) || !in_array($item['class'], $ignoredClasses))) {
&& !isset($item['class'])) {
$filteredTrace[] = $item;
}

View File

@ -1,6 +1,4 @@
<?php
require_once 'TestRunner.php';
/**
* Test case class for the Sapphire framework.
* Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier
@ -124,7 +122,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
/**
* Determines if unit tests are currently run (via {@link TestRunner}).
* Determines if unit tests are currently run, flag set during test bootstrap.
* This is used as a cheap replacement for fully mockable state
* in certain contiditions (e.g. access checks).
* Caution: When set to FALSE, certain controllers might bypass
@ -823,6 +821,36 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
return var_export($extracted, true);
}
/**
* Pushes a class and template manifest instance that include tests onto the
* top of the loader stacks.
*/
public static function use_test_manifest() {
$flush = true;
if(isset($_GET['flush']) && $_GET['flush'] === '0') {
$flush = false;
}
$classManifest = new SS_ClassManifest(
BASE_PATH, true, $flush
);
SS_ClassLoader::instance()->pushManifest($classManifest, false);
SapphireTest::set_test_class_manifest($classManifest);
SS_TemplateLoader::instance()->pushManifest(new SS_TemplateManifest(
BASE_PATH, project(), true, $flush
));
Config::inst()->pushConfigStaticManifest(new SS_ConfigStaticManifest(
BASE_PATH, true, $flush
));
// Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
// (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
DataObject::reset();
}
/**
* Returns true if we are currently using a temporary database
*/

View File

@ -1,23 +0,0 @@
<?php
/**
* Light wrapper around {@link PHPUnit_Framework_TestSuite}
* which allows to have {@link setUp()} and {@link tearDown()}
* methods which are called just once per suite, not once per
* test method in each suite/case.
*
* @package framework
* @subpackage testing
*/
class SapphireTestSuite extends PHPUnit_Framework_TestSuite {
public function setUp() {
foreach($this->groups as $group) {
if($group[0] instanceof SapphireTest) $group[0]->setUpOnce();
}
}
public function tearDown() {
foreach($this->groups as $group) {
if($group[0] instanceof SapphireTest) $group[0]->tearDownOnce();
}
}
}

View File

@ -1,444 +0,0 @@
<?php
/**
* @package framework
* @subpackage testing
*/
/**
* Controller that executes PHPUnit tests.
*
* Alternatively, you can also use the "phpunit" binary directly by
* pointing it to a file or folder containing unit tests.
* See phpunit.dist.xml in the webroot for configuration details.
*
* <h2>URL Options</h2>
* - SkipTests: A comma-separated list of test classes to skip (useful when running dev/tests/all)
*
* See {@link browse()} output for generic usage instructions.
*
* @package framework
* @subpackage testing
*/
class TestRunner extends Controller {
/** @ignore */
private static $default_reporter;
private static $url_handlers = array(
'' => 'browse',
'coverage/module/$ModuleName' => 'coverageModule',
'coverage/suite/$SuiteName!' => 'coverageSuite',
'coverage/$TestCase!' => 'coverageOnly',
'coverage' => 'coverageAll',
'cleanupdb' => 'cleanupdb',
'module/$ModuleName' => 'module',
'suite/$SuiteName!' => 'suite',
'all' => 'all',
'build' => 'build',
'$TestCase' => 'only'
);
private static $allowed_actions = array(
'index',
'browse',
'coverage',
'coverageAll',
'coverageModule',
'coverageSuite',
'coverageOnly',
'cleanupdb',
'module',
'suite',
'all',
'build',
'only'
);
/**
* @var Array Blacklist certain directories for the coverage report.
* Filepaths are relative to the webroot, without leading slash.
*
* @see http://www.phpunit.de/manual/current/en/appendixes.configuration.html
* #appendixes.configuration.blacklist-whitelist
*/
static $coverage_filter_dirs = array(
'*/thirdparty',
'*/tests',
'*/lang',
);
/**
* Override the default reporter with a custom configured subclass.
*
* @param string $reporter
*/
public static function set_reporter($reporter) {
if (is_string($reporter)) $reporter = new $reporter;
self::$default_reporter = $reporter;
}
/**
* Pushes a class and template manifest instance that include tests onto the
* top of the loader stacks.
*/
public static function use_test_manifest() {
$flush = false;
if(isset($_GET['flush']) && ($_GET['flush'] === '1' || $_GET['flush'] == 'all')) {
$flush = true;
}
$classManifest = new SS_ClassManifest(
BASE_PATH, true, $flush
);
SS_ClassLoader::instance()->pushManifest($classManifest, false);
SapphireTest::set_test_class_manifest($classManifest);
SS_TemplateLoader::instance()->pushManifest(new SS_TemplateManifest(
BASE_PATH, project(), true, $flush
));
Config::inst()->pushConfigStaticManifest(new SS_ConfigStaticManifest(
BASE_PATH, true, $flush
));
// Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
// (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
DataObject::reset();
}
public function init() {
parent::init();
$canAccess = (Director::isDev() || Director::is_cli() || Permission::check("ADMIN"));
if(!$canAccess) return Security::permissionFailure($this);
if (!self::$default_reporter) self::set_reporter(Director::is_cli() ? 'CliDebugView' : 'DebugView');
if(!PhpUnitWrapper::has_php_unit()) {
die("Please install PHPUnit using Composer");
}
}
public function Link() {
return Controller::join_links(Director::absoluteBaseURL(), 'dev/tests/');
}
/**
* Run test classes that should be run with every commit.
* Currently excludes PhpSyntaxTest
*/
public function all($request, $coverage = false) {
self::use_test_manifest();
$tests = ClassInfo::subclassesFor('SapphireTest');
array_shift($tests);
unset($tests['FunctionalTest']);
// Remove tests that don't need to be executed every time
unset($tests['PhpSyntaxTest']);
foreach($tests as $class => $v) {
$reflection = new ReflectionClass($class);
if(!$reflection->isInstantiable()) unset($tests[$class]);
}
$this->runTests($tests, $coverage);
}
/**
* Run test classes that should be run before build - i.e., everything possible.
*/
public function build() {
self::use_test_manifest();
$tests = ClassInfo::subclassesFor('SapphireTest');
array_shift($tests);
unset($tests['FunctionalTest']);
foreach($tests as $class => $v) {
$reflection = new ReflectionClass($class);
if(!$reflection->isInstantiable()) unset($tests[$class]);
}
$this->runTests($tests);
}
/**
* Browse all enabled test cases in the environment
*/
public function browse() {
self::use_test_manifest();
self::$default_reporter->writeHeader();
self::$default_reporter->writeInfo('Available Tests', false);
if(Director::is_cli()) {
$tests = ClassInfo::subclassesFor('SapphireTest');
$relativeLink = Director::makeRelative($this->Link());
echo "sake {$relativeLink}all: Run all " . count($tests) . " tests\n";
echo "sake {$relativeLink}coverage: Runs all tests and make test coverage report\n";
echo "sake {$relativeLink}module/<modulename>: Runs all tests in a module folder\n";
foreach ($tests as $test) {
echo "sake {$relativeLink}$test: Run $test\n";
}
} else {
echo '<div class="trace">';
$tests = ClassInfo::subclassesFor('SapphireTest');
asort($tests);
echo "<h3><a href=\"" . $this->Link() . "all\">Run all " . count($tests) . " tests</a></h3>";
echo "<h3><a href=\"" . $this->Link() . "coverage\">Runs all tests and make test coverage report</a></h3>";
echo "<hr />";
foreach ($tests as $test) {
echo "<h3><a href=\"" . $this->Link() . "$test\">Run $test</a></h3>";
}
echo '</div>';
}
self::$default_reporter->writeFooter();
}
/**
* Run a coverage test across all modules
*/
public function coverageAll($request) {
self::use_test_manifest();
$this->all($request, true);
}
/**
* Run only a single coverage test class or a comma-separated list of tests
*/
public function coverageOnly($request) {
$this->only($request, true);
}
/**
* Run coverage tests for one or more "modules".
* A module is generally a toplevel folder, e.g. "mysite" or "framework".
*/
public function coverageModule($request) {
$this->module($request, true);
}
public function cleanupdb() {
SapphireTest::delete_all_temp_dbs();
}
/**
* Run only a single test class or a comma-separated list of tests
*/
public function only($request, $coverage = false) {
self::use_test_manifest();
if($request->param('TestCase') == 'all') {
$this->all();
} else {
$classNames = explode(',', $request->param('TestCase'));
foreach($classNames as $className) {
if(!class_exists($className) || !is_subclass_of($className, 'SapphireTest')) {
user_error("TestRunner::only(): Invalid TestCase '$className', cannot find matching class",
E_USER_ERROR);
}
}
$this->runTests($classNames, $coverage);
}
}
/**
* Run tests for one or more "modules".
* A module is generally a toplevel folder, e.g. "mysite" or "framework".
*/
public function module($request, $coverage = false) {
self::use_test_manifest();
$classNames = array();
$moduleNames = explode(',', $request->param('ModuleName'));
$ignored = array('functionaltest', 'phpsyntaxtest');
foreach($moduleNames as $moduleName) {
$classNames = array_merge(
$classNames,
$this->getTestsInDirectory($moduleName, $ignored)
);
}
$this->runTests($classNames, $coverage);
}
/**
* Find all test classes in a directory and return an array of them.
* @param string $directory To search in
* @param array $ignore Ignore these test classes if they are found.
* @return array
*/
protected function getTestsInDirectory($directory, $ignore = array()) {
$classes = ClassInfo::classes_for_folder($directory);
return $this->filterTestClasses($classes, $ignore);
}
/**
* Find all test classes in a file and return an array of them.
* @param string $file To search in
* @param array $ignore Ignore these test classes if they are found.
* @return array
*/
protected function getTestsInFile($file, $ignore = array()) {
$classes = ClassInfo::classes_for_file($file);
return $this->filterTestClasses($classes, $ignore);
}
/**
* @param array $classes to search in
* @param array $ignore Ignore these test classes if they are found.
*/
protected function filterTestClasses($classes, $ignore) {
$testClasses = array();
if($classes) {
foreach($classes as $className) {
if(
class_exists($className) &&
is_subclass_of($className, 'SapphireTest') &&
!in_array($className, $ignore)
) {
$testClasses[] = $className;
}
}
}
return $testClasses;
}
/**
* Run tests for a test suite defined in phpunit.xml
*/
public function suite($request, $coverage = false) {
self::use_test_manifest();
$suite = $request->param('SuiteName');
$xmlFile = BASE_PATH.'/phpunit.xml';
if(!is_readable($xmlFile)) {
user_error("TestRunner::suite(): $xmlFile is not readable", E_USER_ERROR);
}
$xml = simplexml_load_file($xmlFile);
$suite = $xml->xpath("//phpunit/testsuite[@name='$suite']");
if(empty($suite)) {
user_error("TestRunner::suite(): couldn't find the $suite testsuite in phpunit.xml");
}
$suite = array_shift($suite);
$classNames = array();
if(isset($suite->directory)) {
foreach($suite->directory as $directory) {
$classNames = array_merge($classNames, $this->getTestsInDirectory($directory));
}
}
if(isset($suite->file)) {
foreach($suite->file as $file) {
$classNames = array_merge($classNames, $this->getTestsInFile($file));
}
}
$this->runTests($classNames, $coverage);
}
/**
* Give us some sweet code coverage reports for a particular suite.
*/
public function coverageSuite($request) {
return $this->suite($request, true);
}
/**
* @param array $classList
* @param boolean $coverage
*/
public function runTests($classList, $coverage = false) {
$startTime = microtime(true);
// disable xdebug, as it messes up test execution
if(function_exists('xdebug_disable')) xdebug_disable();
ini_set('max_execution_time', 0);
$this->setUp();
// Optionally skip certain tests
$skipTests = array();
if($this->getRequest()->getVar('SkipTests')) {
$skipTests = explode(',', $this->getRequest()->getVar('SkipTests'));
}
$abstractClasses = array();
foreach($classList as $className) {
// Ensure that the autoloader pulls in the test class, as PHPUnit won't know how to do this.
class_exists($className);
$reflection = new ReflectionClass($className);
if ($reflection->isAbstract()) {
array_push($abstractClasses, $className);
}
}
$classList = array_diff($classList, $skipTests, $abstractClasses);
// run tests before outputting anything to the client
$suite = new PHPUnit_Framework_TestSuite();
natcasesort($classList);
foreach($classList as $className) {
// Ensure that the autoloader pulls in the test class, as PHPUnit won't know how to do this.
class_exists($className);
$suite->addTest(new SapphireTestSuite($className));
}
// Remove the error handler so that PHPUnit can add its own
restore_error_handler();
self::$default_reporter->writeHeader("SilverStripe Test Runner");
if (count($classList) > 1) {
self::$default_reporter->writeInfo("All Tests", "Running test cases: ",implode(", ", $classList));
} elseif (count($classList) == 1) {
self::$default_reporter->writeInfo(reset($classList), '');
} else {
// border case: no tests are available.
self::$default_reporter->writeInfo('', '');
}
// perform unit tests (use PhpUnitWrapper or derived versions)
$phpunitwrapper = PhpUnitWrapper::inst();
$phpunitwrapper->setSuite($suite);
$phpunitwrapper->setCoverageStatus($coverage);
// Make sure TearDown is called (even in the case of a fatal error)
$self = $this;
register_shutdown_function(function() use ($self) {
$self->tearDown();
});
$phpunitwrapper->runTests();
// get results of the PhpUnitWrapper class
$reporter = $phpunitwrapper->getReporter();
$results = $phpunitwrapper->getFrameworkTestResults();
if(!Director::is_cli()) echo '<div class="trace">';
$reporter->writeResults();
$endTime = microtime(true);
if(Director::is_cli()) echo "\n\nTotal time: " . round($endTime-$startTime,3) . " seconds\n";
else echo "<p class=\"total-time\">Total time: " . round($endTime-$startTime,3) . " seconds</p>\n";
if(!Director::is_cli()) echo '</div>';
// Put the error handlers back
$errorHandler = Injector::inst()->get('ErrorHandler');
$errorHandler->start();
if(!Director::is_cli()) self::$default_reporter->writeFooter();
$this->tearDown();
// Todo: we should figure out how to pass this data back through Director more cleanly
if(Director::is_cli() && ($results->failureCount() + $results->errorCount()) > 0) exit(2);
}
public function setUp() {
// The first DB test will sort out the DB, we don't have to
SSViewer::flush_template_cache();
}
public function tearDown() {
SapphireTest::kill_temp_db();
}
}

View File

@ -1,272 +0,0 @@
<?php
/**
* @package framework
* @subpackage dev
*/
/**
* This method checks if a given filename exists in the include path (defined
* in php.ini.
*
* @return boolean when the file has been found in the include path.
*/
function fileExistsInIncludePath($filename) {
$paths = explode(PATH_SEPARATOR, ini_get('include_path'));
foreach($paths as $path) {
if(substr($path,-1) == DIRECTORY_SEPARATOR) $path = substr($path,0,-1);
if(@file_exists($path."/".$filename)) return true;
}
return false;
}
/**
* PHPUnit Wrapper class.
* Base class for PHPUnit wrapper classes to support different PHPUnit versions.
* The current implementation supports PHPUnit 3.4 and PHPUnit 3.5.
*/
class PhpUnitWrapper implements IPhpUnitWrapper {
/**
* Flag if coverage report shall be generated or not.
* @var boolean
*/
private $coverage = false;
/**
* PHPUnit-TestSuite class. The tests, added to this suite are performed
* in this test-run.
* @var PHPUnit_Framework_TestSuite
*/
private $suite = null;
/**
* @var PHPUnit_Framework_TestResult
*/
private $results = null;
/**
* @var PHPUnit_Framework_TestListener
*/
private $reporter = null;
/**
* Shows the version, implemented by the phpunit-wrapper class instance.
* This instance implements no phpunit, the version is null.
* @var String
*/
protected $version = null;
private static $phpunit_wrapper = null;
/**
* Getter for $coverage (@see $coverage).
* @return boolean
*/
public function getCoverageStatus() {
return $this->coverage;
}
/**
* Setter for $coverage (@see $coverage).
* @parameter $value Boolean
*/
public function setCoverageStatus($value) {
$this->coverage = $value;
}
/**
* Getter for $suite (@see $suite).
* @return PHPUnit_Framework_TestSuite
*/
public function getSuite() {
return $this->suite;
}
/**
* Setter for $suite (@see $suite).
* @param $value PHPUnit_Framework_TestSuite
*/
public function setSuite($value) {
$this->suite = $value;
}
/**
* Getter for $reporter (@see $reporter).
* @return PHPUnit_Framework_TestListener
*/
public function getReporter() {
return $this->reporter;
}
/**
* Setter for $reporter (@see $reporter).
* @param $value PHPUnit_Framework_TestListener
*/
public function setReporter($value) {
$this->reporter = $value;
}
/**
* Getter for $results (@see $results).
* @return PHPUnit_Framework_TestResult
*/
public function getFrameworkTestResults() {
return $this->results;
}
/**
* Setter for $results (@see $results).
* @param $value PHPUnit_Framework_TestResult
*/
public function setFrameworkTestResults($value) {
$this->results = $value;
}
/**
* Getter for $version (@see $version).
* @return String
*/
public function getVersion() {
return $this->version;
}
/**
* Loads and initiates phpunit, based on the available phpunit version.
*
* @return PhpUnitWrapper Instance of the php-wrapper class
*/
public static function inst() {
if (self::$phpunit_wrapper == null) {
// Loaded via autoloader, composer or other generic
if (class_exists('PHPUnit_Runner_Version')) {
self::$phpunit_wrapper = new PhpUnitWrapper_Generic();
}
// 3.5 detection
else if (fileExistsInIncludePath("/PHPUnit/Autoload.php")) {
self::$phpunit_wrapper = new PhpUnitWrapper_3_5();
}
// 3.4 detection
else if (fileExistsInIncludePath("/PHPUnit/Framework.php")) {
self::$phpunit_wrapper = new PhpUnitWrapper_3_4();
}
// No version found - will lead to an error
else {
self::$phpunit_wrapper = new PhpUnitWrapper();
}
self::$phpunit_wrapper->init();
}
return self::$phpunit_wrapper;
}
/**
* Returns true if one of the two supported PHPUNIT versions is installed.
*
* @return boolean true if PHPUnit has been installed on the environment.
*/
public static function has_php_unit() {
return (Bool) self::inst()->getVersion();
}
/**
* Implements method, defined in the interface IPhpUnitWrapper:init (@see IPhpUnitWrapper).
* This wrapper class doesn't require any initialisation.
*/
public function init() {
}
/**
* This method is called before the unittests are performed.
* This wrapper implements the non-PHPUnit version which means that unit tests
* can not be performed.
* @throws PhpUnitWrapper_Excption
*/
protected function beforeRunTests() {
throw new PhpUnitWrapper_Exception('Method \'beforeRunTests\' not implemented in PhpUnitWrapper.');
}
/**
* This method is called after the unittests are performed.
* This wrapper implements the non-PHPUnit version which means that unit tests
* can not be performed.
* @throws PhpUnitWrapper_Excption
*/
protected function afterRunTests() {
throw new PhpUnitWrapper_Exception('Method \'afterRunTests\' not implemented in PhpUnitWrapper.');
}
/**
* Perform all tests, added to the suite and initialises SilverStripe to collect
* the results of the unit tests.
*
* This method calls @see beforeRunTests and @see afterRunTests.
*/
public function runTests() {
if(Director::is_cli()) {
$this->setReporter( new CliTestReporter() );
} else {
$this->setReporter( new SapphireTestReporter() );
}
if ($this->getFrameworkTestResults() == null) {
$this->setFrameworkTestResults(new PHPUnit_Framework_TestResult());
}
$this->getFrameworkTestResults()->addListener( $this->getReporter() );
$this->beforeRunTests();
$this->getSuite()->run($this->getFrameworkTestResults());
$this->afterRunTests();
}
/**
* Returns an array containing all the module folders in the base dir.
*
* @return array
*/
protected function moduleDirectories() {
$files = scandir(BASE_PATH);
$modules = array();
foreach($files as $file) {
if(is_dir(BASE_PATH . "/$file") && file_exists(BASE_PATH . "/$file/_config.php")) {
$modules[] = $file;
}
}
return $modules;
}
}
/**
* Interface, implementing the general PHPUnit wrapper API.
*/
interface IPhpUnitWrapper {
public function init();
public function runTests();
}
/**
* PHPUnitWrapper Exception class
*/
class PhpUnitWrapper_Exception extends Exception {}
// If PHPUnit is not installed on the local environment, declare the class to
// ensure that missing class declarations are available to avoind any PHP fatal
// errors.
//
if(!PhpUnitWrapper::has_php_unit()) {
/**
* PHPUnit is a testing framework that can be installed using Composer.
* It's not bundled with SilverStripe, you will need to install it yourself.
*
* @package framework
* @subpackage testing
*/
class PHPUnit_Framework_TestCase {
}
}

View File

@ -1,76 +0,0 @@
<?php
/**
* @package framework
* @subpackage dev
*/
/**
* PHPUnit Wrapper class. Implements the correct behaviour for PHPUnit V3.4.
*/
class PhpUnitWrapper_3_4 extends PhpUnitWrapper {
public function getVersion() {
return 'PhpUnit V3.4';
}
/**
* Initialise the wrapper class.
*/
public function init() {
parent::init();
require_once 'PHPUnit/Framework.php';
require_once 'PHPUnit/Util/Report.php';
require_once 'PHPUnit/TextUI/TestRunner.php';
}
/**
* Overwrites beforeRunTests. Initiates coverage-report generation if
* $coverage has been set to true (@see setCoverageStatus).
*/
protected function beforeRunTests() {
if($this->getCoverageStatus()) {
// blacklist selected folders from coverage report
$modules = $this->moduleDirectories();
foreach(TestRunner::config()->coverage_filter_dirs as $dir) {
if($dir[0] == '*') {
$dir = substr($dir, 1);
foreach ($modules as $module) {
PHPUnit_Util_Filter::addDirectoryToFilter(BASE_PATH . '/' . $dir);
}
} else {
PHPUnit_Util_Filter::addDirectoryToFilter(BASE_PATH . '/' . $dir);
}
}
$this->getFrameworkTestResults()->collectCodeCoverageInformation(true);
}
}
/**
* Overwrites afterRunTests. Creates coverage report and clover report
* if required.
*/
protected function afterRunTests() {
if($this->getCoverageStatus()) {
if(!file_exists(ASSETS_PATH . '/coverage-report')) {
mkdir(ASSETS_PATH . '/coverage-report');
}
$ret = PHPUnit_Util_Report::render($this->getFrameworkTestResults(), ASSETS_PATH . '/coverage-report/');
$coverageApp = ASSETS_PATH . '/coverage-report/'
. preg_replace('/[^A-Za-z0-9]/','_',preg_replace('/(\/$)|(^\/)/','',Director::baseFolder())) . '.html';
$coverageTemplates = ASSETS_PATH . '/coverage-report/'
. preg_replace('/[^A-Za-z0-9]/','_',preg_replace('/(\/$)|(^\/)/','',realpath(TEMP_FOLDER))) . '.html';
echo "<p>Coverage reports available here:<ul>
<li><a href=\"$coverageApp\">Coverage report of the application</a></li>
<li><a href=\"$coverageTemplates\">Coverage report of the templates</a></li>
</ul>";
}
}
}

View File

@ -1,25 +0,0 @@
<?php
/**
* @package framework
* @subpackage dev
*/
class PhpUnitWrapper_3_5 extends PhpUnitWrapper_Generic {
public function getVersion() {
return 'PhpUnit V3.5';
}
/**
* Initialise the wrapper class.
*/
public function init() {
if(!class_exists('PHPUnit_Framework_TestCase')) {
require_once 'PHP/CodeCoverage.php';
require_once 'PHP/CodeCoverage/Report/HTML.php';
require_once 'PHPUnit/Autoload.php';
require_once 'PHP/CodeCoverage/Filter.php';
}
}
}

View File

@ -1,74 +0,0 @@
<?php
/**
* Generic PhpUnitWrapper.
* Originally intended for use with Composer based installations, but will work
* with any fully functional autoloader.
*
* @package framework
* @subpackage dev
*/
class PhpUnitWrapper_Generic extends PhpUnitWrapper {
/**
* Returns a version string, like 3.7.34 or 4.2-dev.
* @return string
*/
public function getVersion() {
return PHPUnit_Runner_Version::id();
}
protected $coverage = null;
protected static $test_name = 'SapphireTest';
public static function get_test_name() {
return static::$test_name;
}
/**
* Overwrites beforeRunTests. Initiates coverage-report generation if
* $coverage has been set to true (@see setCoverageStatus).
*/
protected function beforeRunTests() {
if($this->getCoverageStatus()) {
$this->coverage = new PHP_CodeCoverage();
$coverage = $this->coverage;
$filter = $coverage->filter();
$modules = $this->moduleDirectories();
foreach(TestRunner::config()->coverage_filter_dirs as $dir) {
if($dir[0] == '*') {
$dir = substr($dir, 1);
foreach ($modules as $module) {
$filter->addDirectoryToBlacklist(BASE_PATH . "/$module/$dir");
}
} else {
$filter->addDirectoryToBlacklist(BASE_PATH . '/' . $dir);
}
}
$filter->addFileToBlacklist(__FILE__, 'PHPUNIT');
$coverage->start(self::get_test_name());
}
}
/**
* Overwrites afterRunTests. Creates coverage report and clover report
* if required.
*/
protected function afterRunTests() {
if($this->getCoverageStatus()) {
$coverage = $this->coverage;
$coverage->stop();
$writer = new PHP_CodeCoverage_Report_HTML();
$writer->process($coverage, ASSETS_PATH.'/code-coverage-report');
}
}
}

View File

@ -51,112 +51,6 @@ the [PHPUnit](http://www.phpunit.de) documentation. It provides a lot of fundame
documentation.
</div>
## Running Tests
### PHPUnit Binary
The `phpunit` binary should be used from the root directory of your website.
:::bash
phpunit
# Runs all tests
phpunit framework/tests/
# Run all tests of a specific module
phpunit framework/tests/filesystem
# Run specific tests within a specific module
phpunit framework/tests/filesystem/FolderTest.php
# Run a specific test
phpunit framework/tests '' flush=all
# Run tests with optional `$_GET` parameters (you need an empty second argument)
<div class="alert" markdown="1">
The manifest is not flushed when running tests. Add `flush=all` to the test command to do this (see above example.)
</div>
<div class="alert" markdown="1">
If phpunit is not installed globally on your machine, you may need to replace the above usage of `phpunit` with the full
path (e.g `vendor/bin/phpunit framework/tests`)
</div>
<div class="info" markdown="1">
All command-line arguments are documented on [phpunit.de](http://www.phpunit.de/manual/current/en/textui.html).
</div>
### Via a Web Browser
Executing tests from the command line is recommended, since it most closely reflects test runs in any automated testing
environments. If for some reason you don't have access to the command line, you can also run tests through the browser.
http://yoursite.com/dev/tests
### Via the CLI
The [sake](../cli) executable that comes with SilverStripe can trigger a customized `[api:TestRunner]` class that
handles the PHPUnit configuration and output formatting. While the custom test runner a handy tool, it's also more
limited than using `phpunit` directly, particularly around formatting test output.
:::bash
sake dev/tests/all
# Run all tests
sake dev/tests/module/framework,cms
# Run all tests of a specific module (comma-separated)
sake dev/tests/FolderTest,OtherTest
# Run specific tests (comma-separated)
sake dev/tests/all "flush=all&foo=bar"
# Run tests with optional `$_GET` parameters
sake dev/tests/all SkipTests=MySkippedTest
# Skip some tests
## Making Tests Run Fast
A major impedement to testing is that by default tests are extremely slow to run. There are two things that can be done to speed them up:
### Disable xDebug
Unless executing a coverage report there is no need to have xDebug enabled.
:::bash
# Disable xdebug
sudo php5dismod xdebug
# Run tests
phpunit framework/tests/
# Enable xdebug
sudo php5enmod xdebug
### Use SQLite In Memory
SQLIte can be configured to fun in memory as opposed to disk and this makes testing an order of magnitude faster. To effect this change add the following to mysite/_config.php - this enables an optional flag to switch between MySQL and SQLite. Note also that the package silverstripe/sqlite3 will need installed, version will vary depending on which version of SilverStripe is being tested.
:::php
if(Director::isDev()) {
if(isset($_GET['db']) && ($db = $_GET['db'])) {
global $databaseConfig;
if($db == 'sqlite3') {
$databaseConfig['type'] = 'SQLite3Database';
$databaseConfig['path'] = ':memory:';
}
}
}
To use SQLite append '' db=sqlite3 after the phpunit command.
:::bash
phpunit framework/tests '' db=sqlite3
### Speed Comparison
Testing against a medium sized module with 93 tests:
* SQLite - 16.15s
* MySQL - 314s
This means using SQLite will run tests over 20 times faster.
## Test Databases and Fixtures
SilverStripe tests create their own database when the test starts. New `ss_tmp` databases are created using the same
@ -170,7 +64,7 @@ permissions to create new databases on your server.
<div class="notice" markdown="1">
The test database is rebuilt every time one of the test methods is run. Over time, you may have several hundred test
databases on your machine. To get rid of them is a call to `http://yoursite.com/dev/tests/cleanupdb`
databases on your machine. To get rid of them, run `sake dev/tasks/CleanupTestDatabasesTask`.
</div>
## Custom PHPUnit Configuration
@ -200,11 +94,6 @@ needs.
</groups>
</phpunit>
<div class="alert" markdown="1">
This configuration file doesn't apply for running tests through the "sake" wrapper
</div>
### setUp() and tearDown()
In addition to loading data through a [Fixture File](fixtures), a test case may require some additional setup work to be
@ -286,38 +175,6 @@ It's important to remember that the `parent::setUp();` functions will need to be
Config::inst()->get('ClassName', 'var_name'); // this will be 'var_value'
}
## Generating a Coverage Report
PHPUnit can generate a code coverage report ([docs](http://www.phpunit.de/manual/current/en/code-coverage-analysis.html))
by executing the following commands.
:::bash
phpunit --coverage-html assets/coverage-report
# Generate coverage report for the whole project
phpunit --coverage-html assets/coverage-report mysite/tests/
# Generate coverage report for the "mysite" module
<div class="notice" markdown="1">
These commands will output a report to the `assets/coverage-report/` folder. To view the report, open the `index.html`
file within a web browser.
</div>
Typically, only your own custom PHP code in your project should be regarded when producing these reports. To exclude
some `thirdparty/` directories add the following to the `phpunit.xml` configuration file.
:::xml
<filter>
<blacklist>
<directory suffix=".php">framework/dev/</directory>
<directory suffix=".php">framework/thirdparty/</directory>
<directory suffix=".php">cms/thirdparty/</directory>
<!-- Add your custom rules here -->
<directory suffix=".php">mysite/thirdparty/</directory>
</blacklist>
</filter>
## Related Documentation
* [How to Write a SapphireTest](how_tos/write_a_sapphiretest)
@ -326,6 +183,5 @@ some `thirdparty/` directories add the following to the `phpunit.xml` configurat
## API Documentation
* [api:TestRunner]
* [api:SapphireTest]
* [api:FunctionalTest]

View File

@ -16,30 +16,60 @@ the [Testing Glossary](testing_glossary). To get started now, follow the install
If you are familiar with PHP coding but new to unit testing then check out Mark's presentation [Getting to Grips with SilverStripe Testing](http://www.slideshare.net/maetl/getting-to-grips-with-silverstripe-testing).
You should also read over [the PHPUnit manual](http://www.phpunit.de/manual/current/en/). It provides a lot of
You should also read over the [PHPUnit manual](http://www.phpunit.de/manual/current/en/). It provides a lot of
fundamental concepts that we build on in this documentation.
Unit tests are not included in the zip/tar.gz SilverStripe [downloads](http://www.silverstripe.org/software/download/) so to get them, install SilverStripe [with composer](/getting_started/composer).
## Running Tests
## Invoking phpunit
In order to run tests, you need to install SilverStripe using [/getting-started/composer](Composer),
which will pull in the required development dependencies to run tests.
These are not included in the standard archive downloads provided from silverstripe.org.
Once you have used composer to create your project, `cd` to your project root. Composer will have installed PHPUnit alongside the required PHP classes into the `vendor/bin/` directory.
Tests are run from the commandline, in your webroot folder:
If you don't want to invoke PHPUnit through its full path (`vendor/bin/phpunit`), add `./vendor/bin` to your $PATH, or symlink phpunit into the root directory of your website:
* `vendor/bin/phpunit`: Runs all tests (as defined by `phpunit.xml`)
* `vendor/bin/phpunit framework/tests/`: Run all tests of a specific module
* `vendor/bin/phpunit framework/tests/filesystem`: Run specific tests within a specific module
* `vendor/bin/phpunit framework/tests/filesystem/FolderTest.php`: Run a specific test
* `vendor/bin/phpunit framework/tests '' flush=all`: Run tests with optional request parameters (note the empty second argument)
- `PATH=./vendor/bin:$PATH` in your shell's profile script; **or**
- `ln -s vendor/bin/phpunit phpunit` at the command prompt in your project root
Check the PHPUnit manual for all available [command line arguments](http://www.phpunit.de/manual/current/en/textui.html).
On Linux or OSX, you can avoid typing the full path on every invocation by adding `vendor/bin`
to your `$PATH` definition in the shell profile (usually `~/.profile`): `PATH=./vendor/bin:$PATH`
## Generating a Coverage Report
PHPUnit can generate a code coverage report ([docs](http://www.phpunit.de/manual/current/en/code-coverage-analysis.html))
which shows you how much of your logic is executed by your tests. This is very useful to determine gaps in tests.
:::bash
vendor/bin/phpunit --coverage-html <output-folder> <optional-tests-folder>
To view the report, open the `index.html` in `<output-folder>` in a web browser.
Typically, only your own custom PHP code in your project should be regarded when producing these reports. To exclude
some `thirdparty/` directories add the following to the `phpunit.xml` configuration file.
:::xml
<filter>
<blacklist>
<directory suffix=".php">framework/dev/</directory>
<directory suffix=".php">framework/thirdparty/</directory>
<directory suffix=".php">cms/thirdparty/</directory>
<!-- Add your custom rules here -->
<directory suffix=".php">mysite/thirdparty/</directory>
</blacklist>
</filter>
## Configuration
### phpunit.xml
The `phpunit` executable can be configured by command line arguments or through an XML file. File-based configuration has
The `phpunit` executable can be configured by [command line arguments](http://www.phpunit.de/manual/current/en/textui.html)
or through an XML file. File-based configuration has
the advantage of enforcing certain rules across test executions (e.g. excluding files from code coverage reports), and
of course this information can be version controlled and shared with other team members.
**Note: This doesn't apply for running tests through the "sake" wrapper**
SilverStripe comes with a default `phpunit.xml.dist` that you can use as a starting point. Copy the file into a new
`phpunit.xml` and customize to your needs - PHPUnit will auto-detect its existence, and prioritize it over the default
file.
@ -50,7 +80,7 @@ There's nothing stopping you from creating multiple XML files (see the `--config
### Database Permissions
SilverStripe tests create thier own database when they are run. Because of this the database user in your config file
SilverStripe tests create their own temporary database on every execution. Because of this the database user in your config file
should have the appropriate permissions to create new databases on your server, otherwise tests will not run.
## Writing Tests
@ -71,57 +101,4 @@ Tutorials and recipes for creating tests using the SilverStripe framework:
* [Creating a SilverStripe test](how_tos/write_a_sapphiretest): Writing tests to check core data objects
* [Creating a functional test](how_tos/write_a_functionaltest): An overview of functional tests and how to write a functional test
* [Testing Outgoing Email](how_tos/testing_email): An overview of the built-in email testing code
## Running Tests
### Via the "phpunit" Binary on Command Line
The `phpunit` binary should be used from the root directory of your website.
# Runs all tests defined in phpunit.xml
phpunit
# Run all tests of a specific module
phpunit framework/tests/
# Run specific tests within a specific module
phpunit framework/tests/filesystem
# Run a specific test
phpunit framework/tests/filesystem/FolderTest.php
# Run tests with optional `$_GET` parameters (you need an empty second argument)
phpunit framework/tests '' flush=all
All command-line arguments are documented on
[phpunit.de](http://www.phpunit.de/manual/current/en/textui.html).
### Via the "sake" Wrapper on Command Line
The [sake](/developer_guides/cli/) executable that comes with SilverStripe can trigger a customized
`[api:TestRunner]` class that handles the PHPUnit configuration and output formatting.
While the custom test runner a handy tool, its also more limited than using `phpunit` directly,
particularly around formatting test output.
# Run all tests
sake dev/tests/all
# Run all tests of a specific module (comma-separated)
sake dev/tests/module/framework,cms
# Run specific tests (comma-separated)
sake dev/tests/FolderTest,OtherTest
# Run tests with optional `$_GET` parameters
sake dev/tests/all flush=all
# Skip some tests
sake dev/tests/all SkipTests=MySkippedTest
### Via Web Browser
Executing tests from the command line is recommended, since it most closely reflects
test runs in any automated testing environments. However, you can also run tests through the browser:
http://localhost/dev/tests
* [Testing Outgoing Email](how_tos/testing_email): An overview of the built-in email testing code

View File

@ -80,11 +80,6 @@ Sake is particularly useful for running build tasks.
:::bash
sake dev/build "flush=1"
Or running unit tests..
:::bash
sake dev/tests/all
It can also be handy if you have a long running script..
:::bash

View File

@ -28,6 +28,7 @@
* `File` is now versioned, and should be published before they can be used on the frontend.
See section on [Migrating File DataObject from 3.x to 4.0](#migrating-file-dataobject-from-3x-to-40)
below for upgrade notes.
* Removed `dev/tests/` controller in favour of standard `vendor/bin/phpunit` command
## New API
@ -47,6 +48,11 @@
* `Configurable` Provides Config API helper methods
* `Injectable` Provides Injector API helper methods
* `Extensible` Allows extensions to be applied
* Removed ability to run tests via web requests (`http://mydomain.com/dev/tests`), use the standard CLI command instead (`vendor/bin/phpunit`)
* Removed `dev/jstests/` controller (no replacement)
* Moved test database cleanup task from `sake dev/tests/cleanupdb` to `sake dev/tasks/CleanupTestDatabasesTask`
* Removed `TestRunner` and `JSTestRunner` APIs
* Removed `PhpUnitWrapper`, `PhpUnitWrapper_3_4`, `PhpUnitWrapper_3_5`, `PhpUnitWrapper_Generic`, `SapphireTestSuite` APIs
* `HtmlEditorConfig` is now an abstract class, with a default implementation `TinyMCEConfig` for the built in
TinyMCE editor.
* `HtmlEditorField::setEditorConfig` may now take an instance of a `HtmlEditorConfig` class, as well as a

View File

@ -0,0 +1,26 @@
<?php
/**
* Cleans up leftover databases from aborted test executions (starting with ss_tmpdb)
* Task is restricted to users with administrator rights or running through CLI.
*
* @package framework
* @subpackage tasks
*/
class CleanuPTestDatabasesTask extends BuildTask {
protected $title = 'Deletes all temporary test databases';
protected $description = 'Cleans up leftover databases from aborted test executions (starting with ss_tmpdb)';
public function init() {
parent::init();
if(!Permission::check('ADMIN') && !Director::is_cli()) {
return Security::permissionFailure($this);
}
}
public function run() {
SapphireTest::delete_all_temp_dbs();
}
}

View File

@ -65,7 +65,7 @@ class EncryptAllPasswordsTask extends BuildTask {
}
/**
* @todo This should really be taken care of by TestRunner
* @todo This should really be taken care of by the testing framework
*/
protected function debugMessage($msg) {
if(class_exists('SapphireTest', false) && !SapphireTest::is_running_test()) {

View File

@ -7,12 +7,6 @@
// Make sure display_errors is on
ini_set('display_errors', 1);
// Check we're using at least PHPUnit 3.5
if(version_compare(PHPUnit_Runner_Version::id(), '3.5', '<')) {
echo 'PHPUnit 3.5 required to run tests using bootstrap.php';
die();
}
// Fake the script name and base
global $_SERVER;
if (!$_SERVER) $_SERVER = array();
@ -50,7 +44,7 @@ global $databaseConfig;
DB::connect($databaseConfig);
// Now set a fake REQUEST_URI
$_SERVER['REQUEST_URI'] = BASE_URL . '/dev';
$_SERVER['REQUEST_URI'] = BASE_URL;
// Fake a session
$_SESSION = null;
@ -58,8 +52,7 @@ $_SESSION = null;
// Prepare manifest autoloader
$controller = new FakeController();
// Get test manifest
TestRunner::use_test_manifest();
SapphireTest::use_test_manifest();
SapphireTest::set_is_running_test(true);

View File

@ -1,44 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Test of Banana javascript</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<!-- This is the minimal jasmin libraries needed -->
<link rel="stylesheet" type="text/css" href="../../../thirdparty/jasmine/lib/jasmine.css">
<script type="application/javascript" src="../../../thirdparty/jasmine/lib/jasmine.js"></script>
<script type="application/javascript" src="../../../thirdparty/jasmine/lib/jasmine-html.js"></script>
<script type="application/javascript" >
/**
* This is the javascript under test
*/
Banana = {
getAmount: function() {
return 2;
},
getColor: function() {
return 'brown';
}
}
/**
* This is the testcase and should be moved out to a individual
* javascript so other testdrivers can run them.
*/
describe("The banana", function() {
it("should have two left", function() {
expect(Banana.getAmount()).toEqual(2);
});
it("should be yellow", function() {
expect(Banana.getColor()).toEqual('yellow');
});
});
</script>
</head>
<body>
<script type="application/javascript">
jasmine.getEnv().addReporter(new jasmine.TrivialReporter());
jasmine.getEnv().execute();
</script>
</body>
</html>

View File

@ -1,16 +0,0 @@
Banana = {
getAmount: function() {
return 2;
},
getColor: function() {
return 'brown';
}
}
describe("The banana", function() {
it("should have two left", function() {
expect(Banana.getAmount()).toEqual(2);
});
it("should be yellow", function() {
expect(Banana.getColor()).toEqual('yellow');
});
});