Skip to content

Commit

Permalink
Merge pull request #88 from Yoast/JRF/sniff-check-placement-testdoubles
Browse files Browse the repository at this point in the history
New sniff to check that test double/mock classes are in the correct directory
  • Loading branch information
Martijn Groeneveldt committed Aug 8, 2018
2 parents f880c53 + b8227b6 commit f80e431
Show file tree
Hide file tree
Showing 14 changed files with 321 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
<!-- Conflicts with PHPCS autoloading of sniffs. -->
<exclude name="Yoast.Files.FileName"/>

<!-- This sniff is irrelevant for a PHPCS standard which follows the PHPCS directory structure. -->
<exclude name="Yoast.Files.TestDoubles"/>

<!-- Conflicts with variable names coming from PHPCS itself. -->
<exclude name="WordPress.NamingConventions.ValidVariableName"/>
</rule>
Expand Down
159 changes: 159 additions & 0 deletions Yoast/Sniffs/Files/TestDoublesSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<?php
/**
* YoastCS\Yoast\Sniffs\Files\TestDoublesSniff.
*
* @package Yoast\YoastCS
* @author Juliette Reinders Folmer
* @license https://opensource.org/licenses/MIT MIT
*/

namespace YoastCS\Yoast\Sniffs\Files;

use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;

/**
* Check that all mock/doubles classes are in their own file and in a `doubles` directory.
*
* @package Yoast\YoastCS
*
* @since 1.0.0
*/
class TestDoublesSniff implements Sniff {

/**
* Relative path to the directory where the test doubles/mocks should be placed.
*
* The path should be relative to the root/basepath of the project and can be
* customized from within a custom ruleset.
*
* @var string
*/
public $doubles_path = '/tests/doubles';

/**
* Target path for test double/mock classes or false if the intended
* target directory doesn't exist.
*
* @var string|bool
*/
protected $target_path;

/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return array(
T_CLASS,
T_INTERFACE,
T_TRAIT,
);
}

/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current
* in the stack passed in $tokens.
*
* @return void|int Void or StackPtr to the end of the file if no basepath was set.
*/
public function process( File $phpcsFile, $stackPtr ) {
// Stripping potential quotes to ensure `stdin_path` passed by IDEs does not include quotes.
$file = preg_replace( '`^([\'"])(.*)\1$`Ds', '$2', $phpcsFile->getFileName() );

if ( 'STDIN' === $file ) {
return;
}

$object_name = $phpcsFile->getDeclarationName( $stackPtr );
if ( empty( $object_name ) ) {
return;
}

if ( stripos( $object_name, 'mock' ) === false && stripos( $object_name, 'double' ) === false ) {
return;
}

if ( ! isset( $phpcsFile->config->basepath ) ) {
$phpcsFile->addWarning(
'For the TestDoubles sniff to be able to function, the --basepath needs to be set.',
0,
'MissingBasePath'
);

return ( $phpcsFile->numTokens + 1 );
}

$base_path = $this->normalize_directory_separators( $phpcsFile->config->basepath );
if ( ! isset( $this->target_path ) || defined( 'PHP_CODESNIFFER_IN_TESTS' ) ) {
$target_path = $base_path . '/';
$target_path .= ltrim( $this->normalize_directory_separators( $this->doubles_path ), '/' );

$this->target_path = false;
if ( file_exists( $target_path ) && is_dir( $target_path ) ) {
$this->target_path = strtolower( $target_path );
}
}

if ( false === $this->target_path ) {
// Non-existent target path.
$phpcsFile->addError(
'Double/Mock test helper class detected, but no "%s" sub-directory found in "%s". Please create the sub-directory.',
$stackPtr,
'NoDoublesDirectory',
array(
$this->doubles_path,
$base_path,
)
);
}

$path_info = pathinfo( $file );
if ( empty( $path_info['dirname'] ) ) {
return;
}

$tokens = $phpcsFile->getTokens();
$dirname = $this->normalize_directory_separators( $path_info['dirname'] );
if ( false === $this->target_path || stripos( $dirname, $this->target_path ) === false ) {
$phpcsFile->addError(
'Double/Mock test helper classes should be placed in the "%s" sub-directory. Found %s: %s',
$stackPtr,
'WrongDirectory',
array(
$this->doubles_path,
$tokens[ $stackPtr ]['content'],
$object_name,
)
);
}

$more_objects_in_file = $phpcsFile->findNext( $this->register(), ( $stackPtr + 1 ) );
if ( false !== $more_objects_in_file ) {
$phpcsFile->addError(
'Double/Mock test helper classes should be in their own file. Found %s: %s',
$stackPtr,
'OneObjectPerFile',
array(
$tokens[ $stackPtr ]['content'],
$object_name,
)
);
}
}

/**
* Normalize all directory separators to be a forward slash.
*
* @param string $path Path to normalize.
*
* @return string
*/
private function normalize_directory_separators( $path ) {
return strtr( $path, '\\', '/' );
}
}
1 change: 1 addition & 0 deletions Yoast/Tests/Files/TestDoublesUnitTest.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?php
118 changes: 118 additions & 0 deletions Yoast/Tests/Files/TestDoublesUnitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php
/**
* Unit test class for Yoast Coding Standard.
*
* @package Yoast\YoastCS
* @author Juliette Reinders Folmer
* @license https://opensource.org/licenses/MIT MIT
*/

namespace YoastCS\Yoast\Tests\Files;

use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest;

/**
* Unit test class for the TestDoubles sniff.
*
* @package Yoast\YoastCS
*
* @since 1.0.0
*/
class TestDoublesUnitTest extends AbstractSniffUnitTest {

/**
* Set CLI values before the file is tested.
*
* @param string $testFile The name of the file being tested.
* @param \PHP_CodeSniffer\Config $config The config data for the test run.
*
* @return void
*/
public function setCliValues( $testFile, $config ) {
if ( $testFile === 'no-basepath.inc' ) {
return;
}

$config->basepath = __DIR__ . DIRECTORY_SEPARATOR . 'TestDoublesUnitTests';
}

/**
* Get a list of all test files to check.
*
* @param string $testFileBase The base path that the unit tests files will have.
*
* @return string[]
*/
protected function getTestFiles( $testFileBase ) {
$sep = DIRECTORY_SEPARATOR;
$test_files = glob( dirname( $testFileBase ) . $sep . 'TestDoublesUnitTests{' . $sep . ',' . $sep . '*' . $sep . '}*.inc', GLOB_BRACE );

if ( ! empty( $test_files ) ) {
return $test_files;
}

return array( $testFileBase . '.inc' );
}

/**
* Returns the lines where errors should occur.
*
* @param string $testFile The name of the file being tested.
*
* @return array <int line number> => <int number of errors>
*/
public function getErrorList( $testFile = '' ) {

switch ( $testFile ) {
// In tests/.
case 'mock-not-in-correct-dir.inc':
return array(
3 => 1,
);

case 'multiple-objects-in-file.inc':
return array(
5 => 2,
);

case 'not-in-correct-custom-dir.inc':
return array(
4 => 2,
);

case 'not-in-correct-dir-double.inc':
return array(
3 => 1,
);

case 'not-in-correct-dir-mock.inc':
return array(
3 => 1,
);

case 'not-double-or-mock.inc': // In tests.
case 'correct-dir-double.inc': // In tests/doubles.
case 'correct-dir-mock.inc': // In tests/doubles.
case 'correct-custom-dir.inc': // In tests/mocks.
default:
return array();
}
}

/**
* Returns the lines where warnings should occur.
*
* @param string $testFile The name of the file being tested.
*
* @return array <int line number> => <int number of warnings>
*/
public function getWarningList( $testFile = '' ) {
if ( $testFile === 'no-basepath.inc' ) {
return array(
1 => 1,
);
}

return array();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

class Prefix_ClassName_Double {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

class Prefix_ClassName_Mock {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

class Mock_Prefix_ClassName {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@codingStandardsChangeSetting Yoast.Files.TestDoubles doubles_path /tests/mocks
<?php

class Prefix_ClassName_Double {}

// @codingStandardsChangeSetting Yoast.Files.TestDoubles doubles_path /tests/doubles
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

use PHPUnit\Framework\TestCase;

class Prefix_ClassName_Double {}

class Test_ClassName extends extends TestCase {}
3 changes: 3 additions & 0 deletions Yoast/Tests/Files/TestDoublesUnitTests/tests/no-basepath.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

class Prefix_ClassName_Double {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

class Prefix_ClassName {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@codingStandardsChangeSetting Yoast.Files.TestDoubles doubles_path /tests/assets
<?php

class Prefix_ClassName_Double {}

// @codingStandardsChangeSetting Yoast.Files.TestDoubles doubles_path /tests/doubles
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

class Prefix_ClassName_Double {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

class Prefix_ClassName_Mock {}

0 comments on commit f80e431

Please sign in to comment.