diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist
index 528e67a8..7e86aa36 100644
--- a/.phpcs.xml.dist
+++ b/.phpcs.xml.dist
@@ -22,6 +22,9 @@
+
+
+
diff --git a/Yoast/Sniffs/Files/TestDoublesSniff.php b/Yoast/Sniffs/Files/TestDoublesSniff.php
new file mode 100644
index 00000000..cb65af1d
--- /dev/null
+++ b/Yoast/Sniffs/Files/TestDoublesSniff.php
@@ -0,0 +1,159 @@
+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, '\\', '/' );
+ }
+}
diff --git a/Yoast/Tests/Files/TestDoublesUnitTest.inc b/Yoast/Tests/Files/TestDoublesUnitTest.inc
new file mode 100644
index 00000000..b3d9bbc7
--- /dev/null
+++ b/Yoast/Tests/Files/TestDoublesUnitTest.inc
@@ -0,0 +1 @@
+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 =>
+ */
+ 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 =>
+ */
+ public function getWarningList( $testFile = '' ) {
+ if ( $testFile === 'no-basepath.inc' ) {
+ return array(
+ 1 => 1,
+ );
+ }
+
+ return array();
+ }
+}
diff --git a/Yoast/Tests/Files/TestDoublesUnitTests/tests/doubles/correct-dir-double.inc b/Yoast/Tests/Files/TestDoublesUnitTests/tests/doubles/correct-dir-double.inc
new file mode 100644
index 00000000..0ef27d1c
--- /dev/null
+++ b/Yoast/Tests/Files/TestDoublesUnitTests/tests/doubles/correct-dir-double.inc
@@ -0,0 +1,3 @@
+