Code hacking is a mechanism that modifies PHP code files while they are loaded. It's intended to ease unit testing code that would otherwise be very difficult or impossible to test (and **only** for this - see [An important note](#an-important-note) about that).
This class has two bits that are difficult to unit test: the call to `WC_Some_Legacy_Service::do_something` (let's assume that we can't refactor that one) and the `get_option` invocation. Let's see how the code hacker can help us with that.
Now, modify `tests/legacy/mockable-functions.php` so that the returned array contains `'get_option'` (if it doesn't already), and modify `tests/legacy/classes-with-mockable-static-methods.php` so that the returned array contains `WC_Some_Legacy_Service`.
For a function to be mockable its name needs to be included in the array returned by `tests/legacy/mockable-functions.php`, so if you need to mock a function that is not included in the array, just go and add it.
Function mocks can be defined by using `FunctionsMockerHack::add_function_mocks`. This method accepts an associative array where keys are function names and values are callbacks with the same signature as the functions they are replacing.
If you ever need to remove the configured function mocks from inside a test, you can do so by executing `FunctionsMockerHack::get_hack_instance()->reset()`. This is done automatically before each test via PHPUnit's `BeforeTestHook`, so normally you won't need to do that.
Note that the code hacker is configured so that only the production code files are modified, the tests code itself is **not** modified. This means that you can use the original functions within your tests even if you have mocked them, for example the following would work:
For a public static method to be mockable the name of the class that defines it needs to be included in the array returned by `tests/legacy/classes-with-mockable-static-methods.php`, so if you need to mock a static method for a class that is not included in the array, just go and add it.
Static method mocks can be defined by using `StaticMockerHack::add_method_mocks`. This method accepts an associative array where keys are class names and values are in turn associative arrays, those having method names as keys and callbacks with the same signature as the methods they are replacing as values.
If you ever need to remove the configured static method mocks from inside a test, you can do so by executing `StaticMockerHack::get_hack_instance()->reset()`. This is done automatically before each test via PHPUnit's `BeforeTestHook`, so normally you won't need to do that.
Note that the code hacker is configured so that only the production code files are modified, the tests code itself is **not** modified. This means that you can use the original static methods within your tests even if you have mocked them, for example the following would work:
Inside your test files you can create classes that extend classes marked as `final` thanks to the `BypassFinalsHack` that is registered at bootstrap time. No extra configuration is needed.
If you want to try it out, mark the `WC_Admin_Foobar` in the previos example as `final`, then add the following to the tests file: `class WC_Admin_Foobar_Subclass extends WC_Admin_Foobar {}`. Without the hack you would get a `Class WC_Admin_Foobar_Subclass may not inherit from final class (WC_Admin_Foobar)` error when trying to run the tests.
The core of the code hacker is the `CodeHacker` class, which is based on [the Bypass Finals project](https://github.com/dg/bypass-finals) by David Grudl. This class is actually [a streamWrapper class](https://www.php.net/manual/en/class.streamwrapper.php) for the regular filesystem, most of its methods are just short-circuited to the regular PHP filesystem handling functions but the `stream_open` method contains some code that allows the magic to happen. What it does (for PHP files only) is to read the file contents and apply all the necessary modifications to it, then if the code has been modified it is stored in a temporary file which is then the one that receives any further filesystem operations instead of the original file. That way, for all practical purposes the content of the file is the "hacked" content.
The files inside `tests/Tools/CodeHacking/Hacks` implement the "hacks" (code file modifications) that are registered via `CodeHacker::add_hack` within `tests/legacy/bootstrap.php`.
The functions mocker works by replacing all instances of `the_function(...)` with `FunctionsMockerHack::the_function(...)`, then `FunctionsMockerHack::__call_static` is implemented in a way that invokes the appropriate callback if defined for the invoked function, or reverts to executing the original function if not. The static methods mocker works similarly, but replacing instances of `TheClass::the_method(...)` with `StaticMockerHack::invoke__the_method__for__TheClass(...)`.
First, implement the hack as a class that contains a `public function hack($code, $path)` method and a `public function reset()` method. The former takes in `$code` a string with the contents of the file pointed by `$path` and returns the modified code, the later reverts the hack to its original state (e.g. for `FunctionsMockerHack` it unregisters all the previously registered function mocks). For convenience you can make your hack a subclass of `CodeHack` but that's not mandatory.
Second, configure the hack as required inside the `initialize_code_hacker` method in `tests/legacy/bootstrap.php`, and register it using `CodeHacker::add_hack`.
In a few rare cases the code hacker will cause problems with tests that do write operations on the local filesystem. In these cases it is possible to temporarily disable the code hacker using `self::disable_code_hacker()` and `self::reenable_code_hacker()` in the test (these methods are defined in `WC_Unit_Test_Case`). These methods are carefully written so that they won't enable the code hacker if it wasn't enabled when the test started, and there's a disabling requests count in place to ensure that the code hacker isn't enabled before it should.
One of these cases is the usage of the `copy` command to copy files. Since this function is used in a few tests, a convenience `file_copy` method is defined in `WC_Unit_Test_Case`; it just temporarily disables the hacker, does the copy, and reenables the hacker.
The code hacker is intended to be a **last resort** mechanism to test stuff that it's **really** difficult or impossible to test otherwise - the mechanisms already in place to help testing (e.g. the PHPUnit's mocks or the Woo helpers) should still be used whenever possible. And of course, the code hacker should not be an excuse to write code that's difficult to test.