diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c376259 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: Testing DealNews\GetConfig + +on: [push] + +jobs: + test: + + runs-on: ubuntu-latest + + strategy: + matrix: + php-versions: ['8.0', '8.1', '8.2'] + include: + - operating-system: 'ubuntu-latest' + php-versions: '8.0' + phpunit-versions: 9 + steps: + + - name: Checkout + uses: actions/checkout@v3 + + - name: Composer Install + uses: php-actions/composer@v6 + with: + php_version: ${{ matrix.php-versions }} + + - name: PHPUnit tests + uses: php-actions/phpunit@v3 + with: + php_extensions: "pcov yaml" + version: "9.6" + php_version: ${{ matrix.php-versions }} + + - name: Run Phan + uses: k1LoW/phan-action@v0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7594d84 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Temporary files +.DS_Store +*~ +*.sw[aop] +.php-cs-fixer.cache +.phpunit.result.cache + +# Deployment dependencies +vendor +composer.lock +phpunit.xml + +# IDE project settings +.project +.settings +.idea +*.sublime-workspace +*.sublime-project + +# test artifacts +tests/fixtures/test_copy.db diff --git a/.phan/config.php b/.phan/config.php new file mode 100644 index 0000000..0349851 --- /dev/null +++ b/.phan/config.php @@ -0,0 +1,116 @@ + true, + + // Allow null to be cast as any type and for any + // type to be cast to null. + "null_casts_as_any_type" => true, + + // If this has entries, scalars (int, float, bool, string, null) + // are allowed to perform the casts listed. + // E.g. ['int' => ['float', 'string'], 'float' => ['int'], 'string' => ['int'], 'null' => ['string']] + // allows casting null to a string, but not vice versa. + // (subset of scalar_implicit_cast) + 'scalar_implicit_partial' => [ + 'int' => ['float', 'string'], + 'float' => ['int'], + 'string' => ['int'], + 'null' => ['string', 'bool'], + 'bool' => ['null'], + ], + + // Backwards Compatibility Checking + 'backward_compatibility_checks' => false, + + // Run a quick version of checks that takes less + // time + "quick_mode" => true, + + // Only emit critical issues + "minimum_severity" => 0, + + // A set of fully qualified class-names for which + // a call to parent::__construct() is required + 'parent_constructor_required' => [ + ], + + // Add any issue types (such as 'PhanUndeclaredMethod') + // here to inhibit them from being reported + 'suppress_issue_types' => [ + // These report false positives in libraries due + // to them not being used by any of the other + // library code. + 'PhanUnreferencedPublicClassConstant', + 'PhanWriteOnlyProtectedProperty', + 'PhanUnreferencedPublicMethod', + 'PhanUnreferencedUseNormal', + 'PhanUnreferencedProtectedMethod', + 'PhanUnreferencedProtectedProperty', + + ], + + // A list of directories that should be parsed for class and + // method information. After excluding the directories + // defined in exclude_analysis_directory_list, the remaining + // files will be statically analyzed for errors. + // + // Thus, both first-party and third-party code being used by + // your application should be included in this list. + 'directory_list' => [ + 'src', + 'vendor', + 'tests', + ], + + // A list of directories holding code that we want + // to parse, but not analyze + "exclude_analysis_directory_list" => [ + "vendor", + "tests", + ], + + // A file list that defines files that will be excluded + // from parsing and analysis and will not be read at all. + // + // This is useful for excluding hopelessly unanalyzable + // files that can't be removed for whatever reason. + 'exclude_file_list' => [ + ], + + // Set to true in order to attempt to detect dead + // (unreferenced) code. Keep in mind that the + // results will only be a guess given that classes, + // properties, constants and methods can be referenced + // as variables (like `$class->$property` or + // `$class->$method()`) in ways that we're unable + // to make sense of. + 'dead_code_detection' => true, +]; diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..33de634 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,68 @@ +in(__DIR__) +; + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PSR2' => true, + 'array_syntax' => [ + 'syntax' => 'short', + ], + 'binary_operator_spaces' => [ + 'default' => 'align_single_space', + ], + 'blank_line_after_opening_tag' => true, + 'blank_line_before_statement' => ['statements' => ['return']], + 'braces_position' => [ + 'allow_single_line_anonymous_functions' => true, + 'allow_single_line_empty_anonymous_classes' => true, + 'anonymous_classes_opening_brace' => 'same_line', + 'anonymous_functions_opening_brace' => 'same_line', + 'classes_opening_brace' => 'same_line', + 'control_structures_opening_brace' => 'same_line', + 'functions_opening_brace' => 'same_line', + ], + 'combine_consecutive_unsets' => true, + 'concat_space' => [ + 'spacing' => 'one', + ], + 'declare_equal_normalize' => true, + 'escape_implicit_backslashes' => [ + 'single_quoted' => true, + 'double_quoted' => true, + ], + 'function_typehint_space' => true, + 'include' => true, + 'lowercase_cast' => true, +// 'class_attributes_separation' => ['elements' => ['method']], + 'native_function_casing' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_empty_comment' => true, + 'no_empty_statement' => true, + 'no_mixed_echo_print' => [ + 'use' => 'echo', + ], + 'no_multiline_whitespace_around_double_arrow' => true, + 'multiline_whitespace_before_semicolons' => false, + 'no_short_bool_cast' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'no_spaces_around_offset' => true, + 'no_unused_imports' => true, + 'no_whitespace_before_comma_in_array' => true, + 'no_whitespace_in_blank_line' => true, + 'object_operator_without_whitespace' => true, + 'ordered_imports' => true, + 'short_scalar_cast' => true, + 'single_blank_line_before_namespace' => true, + 'single_quote' => true, + 'space_after_semicolon' => true, + 'ternary_operator_spaces' => true, + 'trailing_comma_in_multiline' => ['elements' => ['arrays']], + 'trim_array_spaces' => true, + 'unary_operator_spaces' => true, + 'whitespace_after_comma_in_array' => true, + ]) + ->setFinder($finder) +; diff --git a/README.md b/README.md new file mode 100644 index 0000000..e382825 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Test Helpers + +A collection of traits that are useful when writing PHPUnit tests. \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c18c5ec --- /dev/null +++ b/composer.json @@ -0,0 +1,45 @@ +{ + "name": "dealnews/test-helpers", + "type": "library", + "license": "BSD-3-Clause", + "description": "A PHP library of traits for use in PHPUnit test cases.", + "config": { + "optimize-autoloader": true, + "discard-changes": true, + "sort-packages": true + }, + "require": { + "php": "^8.0", + "guzzlehttp/guzzle": "^7.8", + "phpunit/phpunit": "^9.6" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.38", + "php-parallel-lint/php-parallel-lint": "^1.3" + }, + "autoload": { + "psr-4" : { + "DealNews\\TestHelpers\\" : "src" + } + }, + "autoload-dev": { + "psr-4": { + "DealNews\\TestHelpers\\Tests\\": "tests/" + } + }, + "scripts": { + "phan": [ + "docker run --rm -e PHAN_DISABLE_XDEBUG_WARN=1 -v `pwd`:/mnt/src -w /mnt/src phanphp/phan:5 -p" + ], + "test": [ + "parallel-lint src/ tests/", + "phpunit --colors=never" + ], + "lint": [ + "parallel-lint src/ tests/" + ], + "fix": [ + "php-cs-fixer fix --config .php-cs-fixer.dist.php src tests" + ] + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..984f514 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + ./src + + + ./tests + ./vendor + + + + + + + + ./tests + + + + + functional + + + + + + + + + diff --git a/src/AssertionStack.php b/src/AssertionStack.php new file mode 100644 index 0000000..3e484ae --- /dev/null +++ b/src/AssertionStack.php @@ -0,0 +1,100 @@ + + * @copyright 1997-Present DealNews.com, Inc + * @package \DealNews\TestHelpers + */ +trait AssertionStack { + + /** + * @var \PHPUnit\Framework\TestCase + */ + protected static $testcase; + + /** + * Stack of assertEqual calls for a method to check the method's parameters that were passed in + * + * @var array + */ + protected static $assert_equal_stacks = []; + + /** + * Sets the currently used testcase object so that this mock class can reference and use + * the assertEquals method + * + * @param \PHPUnit\Framework\TestCase $testCase + */ + public function setTestCaseForMock(\PHPUnit\Framework\TestCase $testCase) : void { + self::$testcase = $testCase; + } + + /** + * Resets the assert equal stack + */ + public function resetAssertEqualStack() { + self::$assert_equal_stacks = []; + } + + /** + * Adds multiple return values for a method + * + * @param string $func The method name + * @param array $expected_param_values A list of arrays. Each array contains a list of param values that are expected + */ + public function setAssertEqualStack(string $func, array $expected_param_values) : void { + foreach ($expected_param_values as $expected_param_value_array) { + $this->addAssertEqualStack($func, $expected_param_value_array); + } + } + + /** + * Adds a single set of expected parameters to be checked with assertEqual + * + * @param string $func The function + * @param array $expected_param_values An array of expected param values + */ + public function addAssertEqualStack(string $func, array $expected_param_values) : void { + self::$assert_equal_stacks[$func][] = $expected_param_values; + } + + /** + * Takes one set of expected param values off of the top of the stack for a particular func/method + * and calls assertEqual for each parameter + * + * @param string $func The function + * @param array $params The parameter values that were passed when the method was called. + * + */ + protected function callAssertEqualFromStack(string $func, array $params) : void { + $classname = get_parent_class($this); + + if (empty(self::$testcase)) { + trigger_error('The testcase object was never set. See: setTestCaseForMock() method that should exist in the mock class of ' . $classname, E_USER_ERROR); + } + + if (!empty(self::$assert_equal_stacks[$func])) { + $expected_values = array_shift(self::$assert_equal_stacks[$func]); + + self::$testcase->assertEquals(count($expected_values), count($params), 'The number of expected parameters and passed-in parameters does not match for the method that is a mock of ' . $classname . '::' . $func . '()'); + + $reflect = new \ReflectionMethod($this, $func); + $param_definitions = $reflect->getParameters(); + + foreach ($params as $parameter_number => $parameter_value) { + $parameter_name = '[Unknown parameter name]'; + if (!empty($param_definitions[$parameter_number])) { + $parameter_name = $param_definitions[$parameter_number]->getName(); + } + + self::$testcase->assertEquals(array_shift($expected_values), $parameter_value, $parameter_name . ' parameter does not have the expected value for the method that is a mock of ' . $classname . '::' . $func . '()'); + } + } + } +} diff --git a/src/CatchErrors.php b/src/CatchErrors.php new file mode 100644 index 0000000..465842f --- /dev/null +++ b/src/CatchErrors.php @@ -0,0 +1,28 @@ + + * @copyright 1997-Present DealNews.com, Inc + * @package DealNews\TestHelpers + */ +trait CatchErrors { + + public function setUp(): void { + parent::setUp(); + set_error_handler( + static function ($errno, $errstr) { + throw new \Exception($errstr, $errno); + }, + E_ALL + ); + } + + public function tearDown(): void { + parent::tearDown(); + restore_error_handler(); + } +} diff --git a/src/Fixtures.php b/src/Fixtures.php new file mode 100644 index 0000000..b4118e0 --- /dev/null +++ b/src/Fixtures.php @@ -0,0 +1,163 @@ + + * @author Jeremy Earle + * @copyright 1997-Present DealNews.com, Inc + * @package \DealNews\TestHelpers + */ +trait Fixtures { + + /** + * Directory where test are stored + */ + public static $test_directory; + + /** + * Directory where fixtures are stored + */ + public static $fixture_directory; + + /** + * Set test dir and fixture dir + */ + public static function setUpBeforeClass(): void { + if (!defined('TEST_DIR')) { + define('TEST_DIR', null); + } + + if (!defined('FIXTURE_DIR')) { + define('FIXTURE_DIR', null); + } + + self::$test_directory = realpath(self::$test_directory ?? TEST_DIR ?? __DIR__ . '/../../../../tests'); + self::$fixture_directory = realpath(self::$fixture_directory ?? FIXTURE_DIR ?? self::$test_directory . '/fixtures'); + + if (empty(self::$test_directory)) { + throw new \RuntimeException('Unable to find test directory.'); + } + } + + /** + * Wrapper for assertSame that sorts array keys before comparing the + * arrays. If the values are not arrays, there is no difference with + * this method and assertSame + * + * @param mixed $expected The expected value + * @param mixed $actual The actual value + * @param string $message The message + */ + public function assertSameData(mixed $expected, mixed $actual, string $message = ''): void { + if (is_array($expected) && is_array($actual)) { + $expected = $this->sortKeysRecursive($expected); + $actual = $this->sortKeysRecursive($actual); + } + + $this->assertSame($expected, $actual, $message); + } + + /** + * Returns the path to a fixture file + * + * @param string $fixture The fixture name + * + * @return string The fixture filename. + */ + public function getFixtureFile(string $fixture): string { + + // data providers are loaded before setUpBeforeClass is run + // so make sure it has been called before using the variables + if (self::$fixture_directory === null) { + self::setUpBeforeClass(); + } + + $file = realpath(self::$fixture_directory . "/$fixture"); + $this->assertTrue(!empty($file) && file_exists($file), "Fixture $fixture does not exist"); + + return $file; + } + + /** + * Returns the contents of a fixture file + * + * @param string $fixture The fixture name + * + * @return string The fixture data. + */ + public function getFixtureData(string $fixture): string { + return file_get_contents($this->getFixtureFile($fixture)); + } + + /** + * Returns fixture data JSON decoded + * + * @param string $fixture The fixture name + * + * @return array|object The fixture data. + */ + public function getFixtureJson(string $fixture, bool $as_array = true) { + return json_decode($this->getFixtureData($fixture), $as_array); + } + + /** + * Returns fixture data as an array of JSON decoded lines + * + * @param string $fixture The fixture name + * + * @return array The fixture data. + */ + public function getFixtureJsonLines(string $fixture, bool $as_array = true): array { + $data = []; + + $fp = fopen($this->getFixtureFile($fixture), 'r'); + + while (!feof($fp)) { + $line = trim(fgets($fp)); + if (!empty($line)) { + $record = json_decode($line, $as_array); + if (is_array($record) || is_object($record)) { + $data[] = $record; + } + } + } + + return $data; + } + + /** + * Checks if a fixture string is a fixture file + * + * @param string $fixture The fixture + * + * @return bool True if the specified fixture is fixture file, False otherwise. + */ + protected function isFixtureFile(string $fixture): bool { + if (!empty($fixture)) { + $file = realpath(self::$fixture_directory . "/$fixture"); + } + + return !empty($file) && file_exists($file); + } + + /** + * Sorts arrays by key recursively + * + * @param array $arr The arr + * + * @return array + */ + protected function sortKeysRecursive(array $arr): array { + ksort($arr); + foreach ($arr as $k => $v) { + if (is_array($v)) { + $arr[$k] = $this->sortKeysRecursive($v); + } + } + + return $arr; + } +} diff --git a/src/Guzzle.php b/src/Guzzle.php new file mode 100644 index 0000000..653ca03 --- /dev/null +++ b/src/Guzzle.php @@ -0,0 +1,58 @@ + + * @author Jeremy Earle + * @copyright 1997-Present DealNews.com, Inc + * @package \DealNews\TestHelpers + */ +trait Guzzle { + + use Fixtures; + + /** + * Create a guzzle mock. + * + * @param integer|array $codes + * @param array $fixtures + * @param array $container + * + * @return GuzzleClient + */ + public function makeGuzzleMock($codes, array $fixtures, array &$container): GuzzleClient { + $responses = []; + + if (is_array($codes)) { + $this->assertEquals(count($codes), count($fixtures), 'When using an array of codes, the number of codes must match the number of fixtures'); + } + + foreach ($fixtures as $fixture) { + if (is_array($fixture)) { + $data = json_encode($fixture); + } elseif (is_string($fixture) && $this->isFixtureFile($fixture)) { + $data = $this->getFixtureData($fixture); + } else { + $data = $fixture; + } + $code = is_array($codes) ? array_shift($codes) : $codes; + $responses[] = new Response($code, ['Content-Type' => 'application/json'], $data); + } + + $history = Middleware::history($container); + $mock = new MockHandler($responses); + $handler_stack = HandlerStack::create($mock); + $handler_stack->push($history); + + return new GuzzleClient(['handler' => $handler_stack]); + } +} diff --git a/src/Methods.php b/src/Methods.php new file mode 100644 index 0000000..401440d --- /dev/null +++ b/src/Methods.php @@ -0,0 +1,238 @@ + + * @author Jeremy Earle + * @copyright 1997-Present DealNews.com, Inc + * @package \DealNews\TestHelpers + */ +trait Methods { + public array $method_counts = []; + public static array $static_method_counts = []; + + protected array $stacks = []; + protected static array $static_stacks = []; + + protected array $return_values = []; + protected static array $static_return_values = []; + + /** + * Resets the stacks and method counts, then adds a stack of return values for each method mentioned + * in the provided array + * + * @param array $stacks + */ + public function _addMultipleMethodResponses(array $stacks) { + $this->stacks = []; + $this->method_counts = []; + foreach ($stacks as $func => $stack) { + $this->_addMultipleResponses($func, $stack); + } + } + + /** + * Sets the stack/list of return values for the provided method + * + * @param string $func The name of the method + * @param array $return A list/stack of return values you want the method to return + */ + public function _addMultipleResponses(string $func, array $return) { + self::_checkMethodExists($func); + $this->stacks[$func] = $return; + } + + /** + * Adds a specific value that should be returned when the provided param values are given to + * the provided method name. + * + * This is not a stack of return values like _addMultipleResponses or _addMultipleMethodResponses uses. And once the + * method is called with the expected parameters, it will return your provided return value for that method call and + * each subsequent call with those parameter values. + * + * @param string $func The name of the method + * @param array $params A list of the parameter values that are expected that should yield this return value + * @param mixed $return The return value + */ + public function _addReturnValueWithParams(string $func, array $params, $return) { + self::_checkMethodExists($func); + $this->return_values[$func][serialize($params)] = $return; + } + + /** + * Resets the stacks, counts, etc... associated with static method calls. Then adds a stack of return values for each + * static method mentioned in the provided array + * + * @param array $stacks + */ + public static function _addMultipleStaticMethodResponses(array $stacks) { + self::_resetStaticResponses(); + foreach ($stacks as $func => $stack) { + self::_addMultipleStaticResponses($func, $stack); + } + } + + /** + * Sets the stack/list of return values for the provided static method + * + * @param string $func The name of the method + * @param array $return A list/stack of return values you want the method to return + */ + public static function _addMultipleStaticResponses(string $func, array $return) { + self::_checkMethodExists($func); + self::$static_stacks[$func] = $return; + } + + /** + * Resets static properties associated with static method stacks, counts, etc... + * + * Should be called when calling static methods at the top of each test. + */ + public static function _resetStaticResponses() { + self::$static_stacks = []; + self::$static_method_counts = []; + self::$static_return_values = []; + } + + /** + * Adds a specific value that should be returned when the provided param values are given to + * the provided static method name. + * + * This is not a stack of return values like _addMultipleStaticResponses or _addMultipleStaticMethodResponses uses. And once the + * static method is called with the expected parameters, it will return your provided return value for that method call and + * each subsequent call with those parameter values. + * + * @param string $func The name of the method + * @param array $params A list of the parameter values that are expected that should yield this return value + * @param mixed $return The return value + */ + public static function _addStaticReturnValueWithParams(string $func, array $params, $return) { + self::_checkMethodExists($func); + self::$static_return_values[$func][serialize($params)] = $return; + } + + /** + * Checks to make sure that the provided method name exists in the parent class. This assumes that your mock class extends + * a class (the parent). If not, the class itself will be checked that it contains the method. + * + * @param string $func The method name + */ + protected static function _checkMethodExists(string $func) { + $called_class = get_called_class(); + $parent = get_parent_class($called_class); + + // Sometimes a mock class will implement and interface instead + // of extending a class. In that case, + if (empty($parent)) { + $interfaces = class_implements($called_class); + if (empty($interfaces)) { + throw new \LogicException("$called_class must extend another class or implement an interface which defines $func to add a mock response with the Mock\\Methods trait."); + } + foreach ($interfaces as $interface) { + if (method_exists($interface, $func)) { + $parent = $interface; + break; + } + } + } + + if (empty($parent) || !method_exists($parent, $func)) { + if (empty($parent)) { + $parent = $called_class; + } + throw new \InvalidArgumentException("Method $func not found for $parent when attempting to add a mock response with the Mock\\Methods trait."); + } + } + + /** + * Pulls the next static method response/return value off of the stack and returns it for the provided static method + * name. It also increments the static method count. + * + * @param string $func The name of the method + * @param mixed $default A default value to use if there are no return values on the stack + * + * @return mixed|null + */ + protected static function _getNextStaticResponse(string $func, $default) { + self::_incrementCount($func, self::$static_method_counts); + + return empty(self::$static_stacks[$func]) ? $default : array_shift(self::$static_stacks[$func]); + } + + /** + * Pulls the next method response/return value off of the stack and returns it for the provided method name. It also + * increments the method count. + * + * @param string $func The name of the method + * @param mixed $default A default value to use of there are no return values on the stack + * + * @return mixed|null + */ + protected function _getNextResponse(string $func, $default) { + self::_incrementCount($func, $this->method_counts); + + return empty($this->stacks[$func]) ? $default : array_shift($this->stacks[$func]); + } + + /** + * Checks to see if there is a return value set that matched the provided parameter values. If there is, it returns it. + * Otherwise, it returns the default value provided. + * + * Also, increments the method counts. + * + * @param string $func The name of the method + * @param array $params A list of parameter values that were passed to the method name provided + * @param mixed|null $default A default value to use if there is no matched return value for this set of parameter values + * + * @return mixed|null + */ + protected function _getResponseWithParams(string $func, array $params, $default = null) { + self::_incrementCount($func, $this->method_counts); + $ser = serialize($params); + + return ( + array_key_exists($func, $this->return_values) && + array_key_exists($ser, $this->return_values[$func]) + ) ? $this->return_values[$func][$ser] : $default; + } + + /** + * Checks to see if there is a return value set that matched the provided parameter values. If there is, it returns it. + * Otherwise, it returns the default value provided. + * + * Also, increments the static method counts. + * + * @param string $func The name of the method + * @param array $params A list of parameter values that were passed to the method name provided + * @param mixed|null $default A default value to use if there is no matched return value for this set of parameter values + * + * @return mixed|null + */ + protected static function _getStaticResponseWithParams(string $func, array $params, $default = null) { + self::_incrementCount($func, self::$static_method_counts); + + $ser = serialize($params); + + return ( + array_key_exists($func, self::$static_return_values) && + array_key_exists($ser, self::$static_return_values[$func]) + ) ? self::$static_return_values[$func][$ser] : $default; + } + + /** + * Increments the count for the provided method stored in the provided variable + * + * @param string $func The name of the method + * @param array $stack_count The array that the method count is stored in + */ + protected static function _incrementCount(string $func, array &$stack_count) { + if (empty($stack_count[$func])) { + $stack_count[$func] = 0; + } + + $stack_count[$func]++; + } +} diff --git a/src/ReflectionHelper.php b/src/ReflectionHelper.php new file mode 100644 index 0000000..c82b773 --- /dev/null +++ b/src/ReflectionHelper.php @@ -0,0 +1,68 @@ +getMethod($method_name); + $method->setAccessible(true); + if (empty($arguments)) { + return $method->invoke($object); + } else { + return $method->invokeArgs($object, $arguments); + } + } + + /** + * Returns the value of a property on the object that is not a public property. + * + * If the property has not been initialized, then this method will return NULL + * + * @param object $object + * @param string $property_name + * + * @return mixed|null + * + * @throws \ReflectionException + */ + protected function getNonPublicPropertyValue(object $object, string $property_name) { + $class = new \ReflectionClass($object); + $property = $class->getProperty($property_name); + $property->setAccessible(true); + if ($property->isInitialized($object)) { + return $property->getValue($object); + } else { + return null; + } + } + + /** + * Sets the value of a property on the object that is not a public property + * + * @param object $object + * @param string $property_name + * @param mixed $property_value + * + * @return void + * + * @throws \ReflectionException + */ + protected function setNonPublicPropertyValue(object $object, string $property_name, $property_value) { + $class = new \ReflectionClass($object); + $property = $class->getProperty($property_name); + $property->setAccessible(true); + $property->setValue($object, $property_value); + } +} diff --git a/tests.old/bootstrap.php b/tests.old/bootstrap.php new file mode 100644 index 0000000..489116a --- /dev/null +++ b/tests.old/bootstrap.php @@ -0,0 +1,25 @@ + $t) { + if (!isset($t['file'])) { + break; + } + fwrite(STDERR, "#$pos {$t['file']} on line {$t['line']}\n"); + } + fwrite(STDERR, "###########\n"); + $args = func_get_args(); + foreach ($args as $arg) { + fwrite(STDERR, trim(var_export($arg, true)) . "\n"); + } + fwrite(STDERR, "###########\n"); + fwrite(STDERR, "END DEBUG\n\n"); +} diff --git a/tests.old/etc/config.d/01-config.ini b/tests.old/etc/config.d/01-config.ini new file mode 100644 index 0000000..0f5119f --- /dev/null +++ b/tests.old/etc/config.d/01-config.ini @@ -0,0 +1,3 @@ +[test] +test.string = example-config-ini-1 +test.override.me = nope \ No newline at end of file diff --git a/tests.old/etc/config.d/02-config.env b/tests.old/etc/config.d/02-config.env new file mode 100644 index 0000000..ba077db --- /dev/null +++ b/tests.old/etc/config.d/02-config.env @@ -0,0 +1,2 @@ +test_string2=example-config-env-2 +test_override_me=yep diff --git a/tests.old/etc/config.d/03-config.yaml b/tests.old/etc/config.d/03-config.yaml new file mode 100644 index 0000000..6e58ce7 --- /dev/null +++ b/tests.old/etc/config.d/03-config.yaml @@ -0,0 +1,4 @@ +--- +test: + string3: example-config-yaml-3 +... diff --git a/tests.old/etc/config.d/04-config.json b/tests.old/etc/config.d/04-config.json new file mode 100644 index 0000000..a8cdaf3 --- /dev/null +++ b/tests.old/etc/config.d/04-config.json @@ -0,0 +1,5 @@ +{ + "test": { + "string4": "example-config-json-4" + } +} \ No newline at end of file diff --git a/tests.old/etc/config.env b/tests.old/etc/config.env new file mode 100644 index 0000000..b31017b --- /dev/null +++ b/tests.old/etc/config.env @@ -0,0 +1,3 @@ +test_string=example-env +test_empty= +test_zero=0 diff --git a/tests.old/etc/config.ini b/tests.old/etc/config.ini new file mode 100644 index 0000000..30d0554 --- /dev/null +++ b/tests.old/etc/config.ini @@ -0,0 +1,4 @@ +[test] +test.string = example-ini +test.empty = +test.zero = 0 \ No newline at end of file diff --git a/tests.old/etc/config.json b/tests.old/etc/config.json new file mode 100644 index 0000000..b61a67f --- /dev/null +++ b/tests.old/etc/config.json @@ -0,0 +1,5 @@ +{ + "test": { + "string": "example-json" + } +} \ No newline at end of file diff --git a/tests.old/etc/config.yaml b/tests.old/etc/config.yaml new file mode 100644 index 0000000..5c2205b --- /dev/null +++ b/tests.old/etc/config.yaml @@ -0,0 +1,4 @@ +--- +test: + string: example-yaml +... diff --git a/tests.old/etc/config_env_file.ini b/tests.old/etc/config_env_file.ini new file mode 100644 index 0000000..5a5eb70 --- /dev/null +++ b/tests.old/etc/config_env_file.ini @@ -0,0 +1,3 @@ +[test] +test.string = example-env +dealnews.test.env.var = something diff --git a/tests.old/etc/get_config_bad.ini b/tests.old/etc/get_config_bad.ini new file mode 100644 index 0000000..18bd7e6 --- /dev/null +++ b/tests.old/etc/get_config_bad.ini @@ -0,0 +1,3 @@ +{ + "some": "json" +} \ No newline at end of file diff --git a/tests.old/etc/get_config_bad.json b/tests.old/etc/get_config_bad.json new file mode 100644 index 0000000..bb7e7aa --- /dev/null +++ b/tests.old/etc/get_config_bad.json @@ -0,0 +1 @@ +some = json diff --git a/tests.old/etc/get_config_bad.txt b/tests.old/etc/get_config_bad.txt new file mode 100644 index 0000000..bb7e7aa --- /dev/null +++ b/tests.old/etc/get_config_bad.txt @@ -0,0 +1 @@ +some = json diff --git a/tests.old/etc/get_config_bad.yaml b/tests.old/etc/get_config_bad.yaml new file mode 100644 index 0000000..bb7e7aa --- /dev/null +++ b/tests.old/etc/get_config_bad.yaml @@ -0,0 +1 @@ +some = json diff --git a/tests.old/etc/get_config_test.ini b/tests.old/etc/get_config_test.ini new file mode 100644 index 0000000..fc5966a --- /dev/null +++ b/tests.old/etc/get_config_test.ini @@ -0,0 +1,2 @@ +[test] +test.string = example diff --git a/tests/AssertionStackTest.php b/tests/AssertionStackTest.php new file mode 100644 index 0000000..8573c20 --- /dev/null +++ b/tests/AssertionStackTest.php @@ -0,0 +1,78 @@ +callAssertEqualFromStack(__FUNCTION__, func_get_args()); + } + }; + + $this->expectException('\\PHPUnit\\Framework\\ExpectationFailedException'); + $this->expectExceptionMessage('The number of expected parameters and passed-in parameters does not match for the method that is a mock of stdClass::test()'); + + $mock->setTestCaseForMock($this); + $mock->addAssertEqualStack('test', [ + 'var1' => 'foo', + 'var2' => 'bar', + 'var3' => 'baz', + ]); + + $mock->test('foo', 'bar', 'baz', 'hat'); + } + + /** + * Tests to mak sure we're correctly failing on an assertion that one of the parameter values that was passed + * to the mock method is not correct + */ + public function testParameterValueFailure() { + $mock = new class extends \StdClass { + use AssertionStack; + + public function test($var1, $var2, $var3) { + $this->callAssertEqualFromStack(__FUNCTION__, func_get_args()); + } + }; + + $this->expectException('\\PHPUnit\\Framework\\ExpectationFailedException'); + $this->expectExceptionMessage('var3 parameter does not have the expected value for the method that is a mock of stdClass::test()'); + + $mock->setTestCaseForMock($this); + $mock->addAssertEqualStack('test', [ + 'var1' => 'foo', + 'var2' => 'bar', + 'var3' => 'baz', + ]); + + $mock->test('foo', 'bar', 'hat'); + } + + public function testSuccess() { + $mock = new class extends \StdClass { + use AssertionStack; + + public function test($var1, $var2, $var3) { + $this->callAssertEqualFromStack(__FUNCTION__, func_get_args()); + } + }; + + $mock->setTestCaseForMock($this); + $mock->addAssertEqualStack('test', [ + 'var1' => 'foo', + 'var2' => 'bar', + 'var3' => 'baz', + ]); + + $mock->test('foo', 'bar', 'baz'); + } +} diff --git a/tests/Classes/StaticTestNoParams.php b/tests/Classes/StaticTestNoParams.php new file mode 100644 index 0000000..61a1b5d --- /dev/null +++ b/tests/Classes/StaticTestNoParams.php @@ -0,0 +1,12 @@ +assertEquals( + __DIR__, + self::$instance::$test_directory + ); + + $this->assertEquals( + __DIR__ . '/fixtures', + self::$instance::$fixture_directory + ); + } + + public function testGetFixtureFile() { + self::$instance::setUpBeforeClass(); + $result = self::$instance->getFixtureFile('foo.json'); + $this->assertEquals( + realpath(__DIR__ . '/fixtures/foo.json'), + $result + ); + } + + public function testGetFixtureData() { + self::$instance::setUpBeforeClass(); + $result = self::$instance->getFixtureData('foo.json'); + $this->assertEquals( + '{"foo":true}', + $result + ); + } + + public function testGetFixtureJson() { + self::$instance::setUpBeforeClass(); + $result = self::$instance->getFixtureJson('foo.json'); + $this->assertEquals( + ['foo' => true], + $result + ); + } + + public function testGetFixtureJsonLines() { + self::$instance::setUpBeforeClass(); + $result = self::$instance->getFixtureJsonLines('foo.jsonl'); + $this->assertEquals( + [ + ['foo' => true], + ['foo' => false], + ], + $result + ); + } +} diff --git a/tests/GuzzleTest.php b/tests/GuzzleTest.php new file mode 100644 index 0000000..6109fb3 --- /dev/null +++ b/tests/GuzzleTest.php @@ -0,0 +1,36 @@ +makeGuzzleMock( + [ + 200, + 404, + 200, + ], + [ + 'foo.json', + ['bar' => 2], + null, + ], + $container + ); + + $this->assertTrue($client instanceof \GuzzleHttp\Client); + } +} diff --git a/tests/MethodsTest.php b/tests/MethodsTest.php new file mode 100644 index 0000000..4108ca9 --- /dev/null +++ b/tests/MethodsTest.php @@ -0,0 +1,264 @@ +_getNextResponse(__FUNCTION__, 'default'); + } + }; + + $child->_addMultipleResponses('test', ['foo']); + + $this->assertTrue(true); + + $bare_mock = new class implements TestInterface { + use Methods; + + public function test(): string { + return $this->_getNextResponse(__FUNCTION__, 'default'); + } + }; + + $bare_mock->_addMultipleResponses('test', ['foo']); + + $this->assertTrue(true); + } + + public function testCheckMethodExistsExceptionChild() { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Method test2 not found for DealNews\\TestHelpers\\Tests\\Classes\\TestNoParams when attempting to add a mock response with the Mock\\Methods trait.'); + + $child = new class extends TestNoParams { + use Methods; + + public function test(): string { + return $this->_getNextResponse(__FUNCTION__, 'default'); + } + }; + + $child->_addMultipleResponses('test2', ['foo']); + } + + public function testCheckMethodExistsExceptionBare() { + $this->expectException(\InvalidArgumentException::class); + // expectExceptionMessage checks the string is contained, not an exact match + $this->expectExceptionMessage('Method test2 not found'); + + $bare_mock = new class implements TestInterface { + use Methods; + + public function test(): string { + return $this->_getNextResponse(__FUNCTION__, 'default'); + } + }; + + $bare_mock->_addMultipleResponses('test2', ['foo']); + } + + public function testCheckMethodExistsExceptionBareNoInterface() { + $this->expectException(\LogicException::class); + // expectExceptionMessage checks the string is contained, not an exact match + $this->expectExceptionMessage('class@anonymous'); + + $bare_mock = new class { + use Methods; + + public function test(): string { + return $this->_getNextResponse(__FUNCTION__, 'default'); + } + }; + + $bare_mock->_addMultipleResponses('test', ['foo']); + } + + public function testGetNextResponse() { + $object = new class extends TestNoParams { + use Methods; + + public function test(): string { + return $this->_getNextResponse(__FUNCTION__, 'default'); + } + }; + + $object->_addMultipleResponses( + 'test', + [ + 'true', + 'false', + 'false', + 'true', + 'true', + ] + ); + + $this->assertEquals('true', $object->test()); + $this->assertEquals('false', $object->test()); + $this->assertEquals('false', $object->test()); + $this->assertEquals('true', $object->test()); + $this->assertEquals('true', $object->test()); + + $this->assertEquals(5, $object->method_counts['test']); + + // test default value + $this->assertEquals('default', $object->test()); + $this->assertEquals(6, $object->method_counts['test']); + + // test _addMultipleMethodResponses + $object->_addMultipleMethodResponses([ + 'test' => [ + 'some', + 'different', + 'results', + 'than', + 'before', + ], + ]); + + $this->assertEquals('some', $object->test()); + $this->assertEquals('different', $object->test()); + $this->assertEquals('results', $object->test()); + $this->assertEquals('than', $object->test()); + $this->assertEquals('before', $object->test()); + + $this->assertEquals(5, $object->method_counts['test']); + } + + public function testGetResponseWithParams() { + $object = new class extends TestOneParam { + use Methods; + + public function test(string $foo): string { + return $this->_getResponseWithParams(__FUNCTION__, func_get_args(), 'default'); + } + }; + + $object->_addReturnValueWithParams( + 'test', + ['true_test'], + 'true' + ); + + $object->_addReturnValueWithParams( + 'test', + ['false_test'], + 'false' + ); + + $this->assertEquals('false', $object->test('false_test')); + $this->assertEquals('true', $object->test('true_test')); + $this->assertEquals('default', $object->test('default_test')); + + $this->assertEquals(3, $object->method_counts['test']); + } + + public function testGetNextStaticResponse() { + $object = new class extends StaticTestNoParams { + use Methods; + + public static function test(): string { + return self::_getNextStaticResponse(__FUNCTION__, 'default'); + } + }; + + $object::_addMultipleStaticResponses( + 'test', + [ + 'true', + 'false', + 'false', + 'true', + 'true', + 'false', // sixth one should be used due to reset below + ] + ); + + $this->assertEquals('true', $object::test()); + $this->assertEquals('false', $object::test()); + $this->assertEquals('false', $object::test()); + $this->assertEquals('true', $object::test()); + $this->assertEquals('true', $object::test()); + + $this->assertEquals(5, $object::$static_method_counts['test']); + + // test reset + $object::_resetStaticResponses(); + $this->assertEquals('default', $object::test()); + $this->assertEquals(1, $object::$static_method_counts['test']); + + // test default value + $this->assertEquals('default', $object::test()); + $this->assertEquals(2, $object::$static_method_counts['test']); + + // test _addMultipleStaticMethodResponses + $object::_addMultipleStaticMethodResponses([ + 'test' => [ + 'some', + 'different', + 'results', + 'than', + 'before', + ], + ]); + + $this->assertEquals('some', $object::test()); + $this->assertEquals('different', $object::test()); + $this->assertEquals('results', $object::test()); + $this->assertEquals('than', $object::test()); + $this->assertEquals('before', $object::test()); + + $this->assertEquals(5, $object::$static_method_counts['test']); + } + + public function testGetStaticResponseWithParams() { + $object = new class extends StaticTestOneParam { + use Methods; + + public static function test(string $foo): string { + return self::_getStaticResponseWithParams(__FUNCTION__, func_get_args(), 'default'); + } + }; + + $object::_addStaticReturnValueWithParams( + 'test', + ['true_test'], + 'true' + ); + + $object::_addStaticReturnValueWithParams( + 'test', + ['false_test'], + 'false' + ); + + $object::_addStaticReturnValueWithParams( + 'test', + ['reset_test'], + 'reset' + ); + + $this->assertEquals('false', $object::test('false_test')); + $this->assertEquals('true', $object::test('true_test')); + $this->assertEquals(2, $object::$static_method_counts['test']); + + //test reset + $object::_resetStaticResponses(); + $this->assertEquals('default', $object::test('reset_test')); + + //test default + $this->assertEquals('default', $object::test('default_test')); + + $this->assertEquals(2, $object::$static_method_counts['test']); + } +} diff --git a/tests/ReflectionHelperTest.php b/tests/ReflectionHelperTest.php new file mode 100644 index 0000000..3504a36 --- /dev/null +++ b/tests/ReflectionHelperTest.php @@ -0,0 +1,92 @@ +executeNonPublicMethod($test, 'aProtectedMethod'); + $this->assertEquals(null, $result, 'aProtectedMethod with no arguments passed did not return the expected value'); + + // protected method, with a string argument passed + $result = $this->executeNonPublicMethod($test, 'aProtectedMethod', ['hello world']); + $this->assertEquals('hello world', $result, 'aProtectedMethod with a string argument passed did not return the expected value'); + + // private method, no arguments passed + $result = $this->executeNonPublicMethod($test, 'aPrivateMethod'); + $this->assertEquals(null, $result, 'aPrivateMethod with no arguments passed did not return the expected value'); + + // private method, with a string argument passed + $result = $this->executeNonPublicMethod($test, 'aPrivateMethod', ['hello world']); + $this->assertEquals('hello world', $result, 'aPrivateMethod with a string argument passed did not return the expected value'); + } + + public function testGetNonPublicPropertyValue() { + $test = new class { + protected string $a_protected_property = 'hello world, protected'; + + private string $a_private_property = 'hello world, private'; + + protected string $uninitialized; + }; + + $result = $this->getNonPublicPropertyValue($test, 'a_protected_property'); + $this->assertEquals('hello world, protected', $result, 'Did not get the expected value for a_protected_property'); + + $result = $this->getNonPublicPropertyValue($test, 'a_private_property'); + $this->assertEquals('hello world, private', $result, 'Did not get the expected value for a_private_property'); + + $result = $this->getNonPublicPropertyValue($test, 'uninitialized'); + $this->assertEquals(null, $result, 'Did not get the expected value for uninitialized'); + } + + public function testSetNonPublicPropertyValue() { + $test = new class { + protected string $a_protected_property = 'hello world, protected'; + + private string $a_private_property = 'hello world, private'; + + protected string $uninitialized; + + public function getAProtectedPropertyValue() { + return $this->a_protected_property; + } + + public function getAPrivatePropertyValue() { + return $this->a_private_property; + } + + public function getUninitializedValue() { + if (!isset($this->uninitialized)) { + // not initialized, yet + return null; + } else { + return $this->uninitialized; + } + } + }; + + $this->setNonPublicPropertyValue($test, 'a_protected_property', 'different value, protected'); + $this->assertEquals('different value, protected', $test->getAProtectedPropertyValue(), 'Did not get the expected value for a_protected_property'); + + $this->setNonPublicPropertyValue($test, 'a_private_property', 'different value, private'); + $this->assertEquals('different value, private', $test->getAPrivatePropertyValue(), 'Did not get the expected value for a_private_property'); + + $this->setNonPublicPropertyValue($test, 'uninitialized', 'different value, uninitialized'); + $this->assertEquals('different value, uninitialized', $test->getUninitializedValue(), 'Did not get the expected value for uninitialized'); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..a36d5bc --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,31 @@ + $t) { + if (!isset($t['file'])) { + break; + } + fwrite(STDERR, "#$pos {$t['file']} on line {$t['line']}\n"); + } + fwrite(STDERR, "###########\n"); + $args = func_get_args(); + foreach ($args as $arg) { + fwrite(STDERR, trim(var_export($arg, true)) . "\n"); + } + fwrite(STDERR, "###########\n"); + fwrite(STDERR, "END DEBUG\n\n"); +} diff --git a/tests/fixtures/foo.json b/tests/fixtures/foo.json new file mode 100644 index 0000000..b435d57 --- /dev/null +++ b/tests/fixtures/foo.json @@ -0,0 +1 @@ +{"foo":true} \ No newline at end of file diff --git a/tests/fixtures/foo.jsonl b/tests/fixtures/foo.jsonl new file mode 100644 index 0000000..e644e02 --- /dev/null +++ b/tests/fixtures/foo.jsonl @@ -0,0 +1,2 @@ +{"foo":true} +{"foo":false}