woocommerce/tests/legacy/framework/class-wc-unit-test-case.php

334 lines
11 KiB
PHP
Raw Normal View History

2014-09-01 06:04:02 +00:00
<?php
/**
* Base test case for all WooCommerce tests.
*
* @package WooCommerce\Tests
*/
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Testing\Tools\CodeHacking\CodeHacker;
use PHPUnit\Framework\Constraint\IsType;
/**
* WC Unit Test Case.
2014-09-01 06:04:02 +00:00
*
* Provides WooCommerce-specific setup/tear down/assert methods, custom factories,
2015-11-03 13:31:20 +00:00
* and helper functions.
2014-09-01 06:04:02 +00:00
*
* @since 2.2
*/
class WC_Unit_Test_Case extends WP_HTTP_TestCase {
2014-09-01 06:04:02 +00:00
/**
* Holds the WC_Unit_Test_Factory instance.
*
* @var WC_Unit_Test_Factory
*/
2014-09-01 06:04:02 +00:00
protected $factory;
/**
* @var int Keeps the count of how many times disable_code_hacker has been invoked.
*/
private static $code_hacker_temporary_disables_requested = 0;
/**
* Increase the count of Code Hacker disable requests, and effectively disable it if the count was zero.
* Does nothing if the code hacker wasn't enabled when the test suite started running.
*/
protected static function disable_code_hacker() {
if ( CodeHacker::is_enabled() ) {
CodeHacker::disable();
self::$code_hacker_temporary_disables_requested = 1;
} elseif ( self::$code_hacker_temporary_disables_requested > 0 ) {
self::$code_hacker_temporary_disables_requested++;
}
}
/**
* Decrease the count of Code Hacker disable requests, and effectively re-enable it if the count reaches zero.
* Does nothing if the count is already zero.
*/
protected static function reenable_code_hacker() {
if ( self::$code_hacker_temporary_disables_requested > 0 ) {
self::$code_hacker_temporary_disables_requested--;
if ( 0 === self::$code_hacker_temporary_disables_requested ) {
CodeHacker::enable();
}
}
}
2014-09-01 06:04:02 +00:00
/**
2015-11-03 13:31:20 +00:00
* Setup test case.
2014-09-01 06:04:02 +00:00
*
* @since 2.2
*/
public function setUp() {
parent::setUp();
// Add custom factories.
2014-09-01 06:04:02 +00:00
$this->factory = new WC_Unit_Test_Factory();
2014-09-05 06:35:53 +00:00
// Setup mock WC session handler.
2014-09-05 06:35:53 +00:00
add_filter( 'woocommerce_session_handler', array( $this, 'set_mock_session_handler' ) );
$this->setOutputCallback( array( $this, 'filter_output' ) );
// Register post types before each test.
WC_Post_types::register_post_types();
WC_Post_types::register_taxonomies();
CodeHacker::reset_hacks();
// Reset the instance of MockableLegacyProxy that was registered during bootstrap,
// in order to start the test in a clean state (without anything mocked).
wc_get_container()->get( LegacyProxy::class )->reset();
2014-09-05 06:35:53 +00:00
}
/**
* Set up class unit test.
*
* @since 3.5.0
*/
public static function setUpBeforeClass() {
parent::setUpBeforeClass();
// Terms are deleted in WP_UnitTestCase::tearDownAfterClass, then e.g. Uncategorized product_cat is missing.
WC_Install::create_terms();
}
2014-09-05 06:35:53 +00:00
/**
2015-11-03 13:31:20 +00:00
* Mock the WC session using the abstract class as cookies are not available.
* during tests.
2014-09-05 06:35:53 +00:00
*
2015-02-04 16:22:52 +00:00
* @since 2.2
* @return string The $output string, sans newlines and tabs.
2014-09-05 06:35:53 +00:00
*/
public function set_mock_session_handler() {
return 'WC_Mock_Session_Handler';
}
2014-09-05 06:36:46 +00:00
/**
2015-11-03 13:31:20 +00:00
* Strip newlines and tabs when using expectedOutputString() as otherwise.
* the most template-related tests will fail due to indentation/alignment in.
* the template not matching the sample strings set in the tests.
2014-09-05 06:36:46 +00:00
*
* @since 2.2
*
* @param string $output The captured output.
* @return string The $output string, sans newlines and tabs.
2014-09-05 06:36:46 +00:00
*/
public function filter_output( $output ) {
$output = preg_replace( '/[\n]+/S', '', $output );
$output = preg_replace( '/[\t]+/S', '', $output );
return $output;
2014-09-01 06:04:02 +00:00
}
/**
* Throws an exception with an optional message and code.
*
* Note: can't use `throwException` as that's reserved.
*
* @since 3.3-dev
* @param string $message Optional. The exception message. Default is empty.
* @param int $code Optional. The exception code. Default is empty.
* @throws Exception Containing the given message and code.
*/
public function throwAnException( $message = null, $code = null ) {
$message = $message ? $message : "We're all doomed!";
throw new Exception( $message, $code );
}
/**
* Copies a file, temporarily disabling the code hacker.
* Use this instead of "copy" in tests for compatibility with the code hacker.
*
* TODO: Investigate why invoking "copy" within a test with the code hacker active causes the test to fail.
*
* @param string $source Path to the source file.
* @param string $dest The destination path.
* @return bool true on success or false on failure.
*/
public static function file_copy( $source, $dest ) {
self::disable_code_hacker();
$result = copy( $source, $dest );
self::reenable_code_hacker();
return $result;
}
/**
* Create a new user in a given role and set it as the current user.
*
* @param string $role The role for the user to be created.
2020-07-24 20:01:42 +00:00
* @return int The id of the user created.
*/
public function login_as_role( $role ) {
$user_id = $this->factory->user->create( array( 'role' => $role ) );
wp_set_current_user( $user_id );
return $user_id;
}
/**
* Create a new administrator user and set it as the current user.
*
2020-07-24 20:01:42 +00:00
* @return int The id of the user created.
*/
public function login_as_administrator() {
2020-07-24 20:04:28 +00:00
return $this->login_as_role( 'administrator' );
}
2020-07-24 20:04:28 +00:00
/**
* Get an instance of a class that has been registered in the dependency injection container.
* To get an instance of a legacy class (such as the ones in the 'íncludes' directory) use
* 'get_legacy_instance_of' instead.
*
* @param string $class_name The class name to get an instance of.
*
* @return mixed The instance.
*/
public function get_instance_of( string $class_name ) {
return wc_get_container()->get( $class_name );
}
/**
* Get an instance of legacy class (such as the ones in the 'íncludes' directory).
* To get an instance of a class registered in the dependency injection container use 'get_instance_of' instead.
*
* @param string $class_name The class name to get an instance of.
*
* @return mixed The instance.
*/
public function get_legacy_instance_of( string $class_name ) {
return wc_get_container()->get( LegacyProxy::class )->get_instance_of( $class_name );
}
/**
* Reset all the cached resolutions in the dependency injection container, so any further "get"
* for shared definitions will generate the instance again.
* This may be needed when registering mocks for already resolved shared classes.
*/
public function reset_container_resolutions() {
wc_get_container()->reset_all_resolved();
}
/**
* Reset the mock legacy proxy class so that all the registered mocks are unregistered.
*/
public function reset_legacy_proxy_mocks() {
wc_get_container()->get( LegacyProxy::class )->reset();
}
/**
* Register the function mocks to use in the mockable LegacyProxy.
*
* @param array $mocks An associative array where keys are function names and values are function replacement callbacks.
*
* @throws \Exception Invalid parameter.
*/
public function register_legacy_proxy_function_mocks( array $mocks ) {
wc_get_container()->get( LegacyProxy::class )->register_function_mocks( $mocks );
}
/**
* Register the static method mocks to use in the mockable LegacyProxy.
*
* @param array $mocks An associative array where keys are class names and values are associative arrays, in which keys are method names and values are method replacement callbacks.
*
* @throws \Exception Invalid parameter.
*/
public function register_legacy_proxy_static_mocks( array $mocks ) {
wc_get_container()->get( LegacyProxy::class )->register_static_mocks( $mocks );
}
/**
* Register the class mocks to use in the mockable LegacyProxy.
*
* @param array $mocks An associative array where keys are class names and values are either factory callbacks (optionally with a $class_name argument) or objects.
*
* @throws \Exception Invalid parameter.
*/
public function register_legacy_proxy_class_mocks( array $mocks ) {
wc_get_container()->get( LegacyProxy::class )->register_class_mocks( $mocks );
}
Refactor the settings pages, and add unit tests for them. This commit fixes some inconsistencies in the settings pages, and makes all the existing pages extensible by adding new sections (that was possible in some pages, but not in others). Main changes: 1. Modify the 'get_sections' method so that it invokes a new protected 'get_own_sections' method and then triggers the 'woocommerce_get_sections_' . id filter. This way the filter is triggered only in the base class and not in each of the derived classes too. 2. Change the get_settings() method so that it has its signature changed to get_settings( $current_section = '' ) in the base class and in all the derived class. Some derived classes were already using this signature, but others (those not having multiple sections natively) weren't, making then effectively impossible to define multiple sections for these pages via filters. With this change all the section pages act consistently and allow both adding new settings to the default "General" section and creating new sections via filters. 3. Change the implementation of 'get_settings' in the base class so that it searches for a 'get_settings_for_{section_id}_section' method in the class and executes it, otherwise it executes the new protected method get_settings_for_section( $current_section ); then it triggers the 'woocommerce_get_settings_' . id filter. This makes it easier to separate the code that returns the list of filters in multiple methods, one per section, instead of using one big if-else-else... block. So now instead of overriding get_settings($current_section='') derived classes need to implement get_settings_for_{$current_section}_section for each section, or override get_settings_for_section($current_section) or both. 'get_settings_for_section' returns an empty array by default. Also, 'woocommerce_get_settings_' . id is triggered in one single place too. Other improvements: * Remove duplicated code from 'output' in 'WC_Settings_Page' children. Some classes inherited from 'WC_Settings_Page' override the 'output' method with custom code, which in all cases ended up repeating the code of the original method as a fallback. These repetitions have been replaced with 'parent::output()'. * Fix inconsistencies for 'save' and 'output' in WC_Settings_Tax/Emails The 'WC_Settings_Tax' and 'WC_Settings_Emails' classes had some inconsistencies in their 'save' and 'output' methods that prevented the proper creation new sections and the addition of new settings via the 'woocommerce_get_sections_' and 'woocommerce_get_settings_' filters. Now they work as expected. * Deduplicate parts of 'save' in 'WC_Settings_Page' and children. Two methods have been added to 'WC_Settings_Page' class: 'save_settings_for_current_section' and 'do_update_options_action'. These are intended to be invoked by derived classes in their 'save' methods, in order to remove code repetition. * Add some helper methods to WC_Unit_Test_Case. Methods added: - assertOutputsHTML - assertEqualsHTML - normalize_html - capture_output_from
2020-09-16 10:27:05 +00:00
/**
* Asserts that a certain callable output is equivalent to a given piece of HTML.
*
* "Equivalent" means that the string representations of the HTML pieces are equal
* except for line breaks, tabs and redundant whitespace.
*
* @param string $expected The expected HTML.
* @param callable $callable The callable that is supposed to output the expected HTML.
* @param string $message Optional error message to display if the assertion fails.
*/
protected function assertOutputsHTML( $expected, $callable, $message = '' ) {
$actual = $this->capture_output_from( $callable );
$this->assertEqualsHTML( $expected, $actual, $message );
}
/**
* Asserts that two pieces of HTML are equivalent.
*
* "Equivalent" means that the string representations of the HTML pieces are equal
* except for line breaks, tabs and redundant whitespace.
*
* @param string $expected The expected HTML.
* @param string $actual The HTML that is supposed to be equivalent to the expected one.
* @param string $message Optional error message to display if the assertion fails.
*/
protected function assertEqualsHTML( $expected, $actual, $message = '' ) {
$this->assertEquals( $this->normalize_html( $expected ), $this->normalize_html( $actual ), $message );
}
/**
* Normalizes a block of HTML.
* Line breaks, tabs and redundand whitespaces are removed.
*
* @param string $html The block of HTML to normalize.
*
* @return string The normalized block.
*/
protected function normalize_html( $html ) {
$html = $this->filter_output( $html );
$html = str_replace( '&', '&amp;', $html );
$html = preg_replace( '/> +</', '><', $html );
$doc = new DomDocument();
$doc->preserveWhiteSpace = false; //phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$doc->loadHTML( $html );
return $doc->saveHTML();
}
/**
* Executes a callable, captures its output and returns it.
*
* @param callable $callable Callable to execute.
* @param mixed ...$params Parameters to pass to the callable as arguments.
*
* @return false|string The output generated by the callable, or false if there is an error.
*/
protected function capture_output_from( $callable, ...$params ) {
ob_start();
call_user_func( $callable, ...$params );
$output = ob_get_contents();
ob_end_clean();
return $output;
}
/**
* Asserts that a variable is of type int.
* TODO: After upgrading to PHPUnit 8 or newer, remove this method and replace calls with PHPUnit's built-in 'assertIsInt'.
*
* @param mixed $actual The value to check.
* @param mixed $message Error message to use if the assertion fails.
* @return bool mixed True if the value is of integer type, false otherwise.
*/
public static function assertIsInteger( $actual, $message = '' ) {
return self::assertInternalType( 'int', $actual, $message );
}
2014-09-01 06:04:02 +00:00
}