Core points
This article was reviewed by Younes Rafie. Thanks to all SitePoint peer reviewers for getting SitePoint content to its best!
I didn't write tests for my code at the beginning. Like many people, my "test" is writing code and refreshing the page. "Does it look right?" I will ask myself. If I think it's right, I'll keep going.
In fact, most of the work I have done is to companies that don’t care much about other forms of testing. After years of experience, and wise advice from people like Chris Hartjes, I saw the value of testing. And I'm still learning what good tests look like.
I recently started working on some JavaScript projects that include bundled test observers.
This is a wonderful advanced video tutorial on testing-driven NodeJS development!
In the world of JavaScript, it is not uncommon to preprocess source code. In the JavaScript world, developers write code using unsupported syntax and then convert the code into widely supported syntax, often using a tool called Babel.
To reduce the burden of calling conversion scripts, the boilerplate project has begun to include scripts that automatically monitor file changes; then call these scripts.
The projects I worked on took a similar approach to rerun unit tests. When I change the JavaScript files, the files are converted and the unit tests are rerun. This way, I can see immediately whether anything is broken.
The code for this tutorial can be found on Github. I've tested it with PHP 7.1.
I've started setting up something similar for PHPUnit since I started working on these projects. In fact, the first project I set up the PHPUnit Observer script is a PHP project that also preprocesses files.
After I added a preprocessing script to my project, it all started:
composer require pre/short-closures
These specific preprocessing scripts allow me to rename PSR-4's automatically loaded classes (from path/to/file.php ⇒ path/to/file.pre) to opt-in to the functionality they provide. So I added the following to my composer.json file:
"autoload": { "psr-4": { "App\": "src" } }, "autoload-dev": { "psr-4": { "App\Tests\": "tests" } }
This is from composer.json
Then I added a class to generate a function containing the details of the current user session:
namespace App; use Closure; class Session { private $user; public function __construct(array $user) { $this->user = $user; } public function closureWithUser(Closure $closure) { return () => { $closure($this->user); }; } }
This comes from src/Session.pre
To check if this works, I set up a small sample script:
require_once __DIR__ . "/vendor/autoload.php"; $session = new App\Session(["id" => 1]); $closure = ($user) => { print "user: " . $user["id"] . PHP_EOL; }; $closureWithUser = $session->closureWithUser($closure); $closureWithUser();
This comes from example.pre
…and because I want to use short closures in non-PSR-4 classes, I also need to set up a loader:
require_once __DIR__ . "/vendor/autoload.php"; Pre\Plugin\process(__DIR__ . "/example.pre");
This comes from loader.php
This section of code is a lot to illustrate a small point. The Session class has a closureWithUser method that accepts one closure and returns another. When called, this new closure will call the original closure, providing the user session array as a parameter.
To run all of this, type in the terminal:
php loader.php
As a side note, these preprocessors generate effective PHP syntax that is pretty beautiful. It looks like this:
$closure = function ($user) { print "user: " . $user["id"] . PHP_EOL; };
…and
public function closureWithUser(Closure $closure) { return [$closure = $closure ?? null, "fn" => function () use (&$closure) { $closure($this->user); }]["fn"]; }
You may not want to submit both php and pre files to the repository. To do this, I have added app/**/*.php and examples.php to .gitignore .
So how do we test this? Let's start with installing PHPUnit:
composer require --dev phpunit/phpunit
Then, we should create a configuration file:
<?xml version="1.0" encoding="UTF-8"?> <phpunit backupGlobals="false" backupStaticAttributes="false" bootstrap="vendor/autoload.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="false" processIsolation="false" stopOnFailure="false" syntaxCheck="false" > <testsuites> <testsuite> <directory suffix="Test.php">tests</directory> </testsuite> </testsuites> <filter> <whitelist addUncoveredFilesFromWhitelist="true"> <directory suffix=".php">src</directory> </whitelist> </filter> </phpunit>
This is from phpunit.xml
If we run vendor/bin/phpunit, it will work. But we haven't tested any yet. Let's do one:
namespace App\Tests; use App\Session; use PHPUnit\Framework\TestCase; class SessionTest extends TestCase { public function testClosureIsDecorated() { $user = ["id" => 1]; $session = new Session($user); $expected = null; $closure = function($user) use (&$expected) { $expected = "user: " . $user["id"]; }; $closureWithUser = $session ->closureWithUser($closure); $closureWithUser(); $this->assertEquals("user: 1", $expected); } }
This comes from tests/SessionTest.php
When we run vendor/bin/phpunit, a single test passes. yeah!
So far, everything went well. We wrote a small piece of code and tests of this piece of code. We don't even have to worry about how preprocessing works (a step up than a JavaScript project).
The problem begins when we try to check code coverage:
vendor/bin/phpunit --coverage-html coverage
As we tested the Session, coverage will be reported. It is a simple class, so we have achieved 100% coverage on it. But if we add another class:
namespace App; class BlackBox { public function get($key) { return $GLOBALS[$key]; } }
This comes from src/BlackBox.pre
What happens when we check coverage? Still 100%.
This happens because we don't have any tests loading BlackBox.pre, which means it's never compiled. Therefore, when PHPUnit looks for an overwritten PHP file, it cannot see this preprocessable file.
Let's create a new script to build all the Pre files before trying to run the test:
composer require pre/short-closures
This comes from tests/bootstrap.php
Here, we create 3 functions; one for getting the recursive file iterator (from the path), one for deleting this iterator, and one for recompiling the Pre file.
We need to replace the current bootstrap file in phpunit.xml:
"autoload": { "psr-4": { "App\": "src" } }, "autoload-dev": { "psr-4": { "App\Tests\": "tests" } }
This is from phpunit.xml
Now, whenever we run the test, this script will first clean and rebuild all Pre files to the PHP file. Coverage is reported correctly and we can continue our happy journey…
Our code base is small, but it doesn't need to be small. We can try this in a real application and immediately regret having to rebuild the file every time we want to test it.
In this project I mentioned, I have 101 Pre files. Just to run my (hopefully quick) unit test suite, this requires a lot of preprocessing. We need a way to monitor changes and rebuild only important parts. First, let's install a file observer:
namespace App; use Closure; class Session { private $user; public function __construct(array $user) { $this->user = $user; } public function closureWithUser(Closure $closure) { return () => { $closure($this->user); }; } }
Then, let's create a test script:
require_once __DIR__ . "/vendor/autoload.php"; $session = new App\Session(["id" => 1]); $closure = ($user) => { print "user: " . $user["id"] . PHP_EOL; }; $closureWithUser = $session->closureWithUser($closure); $closureWithUser();
This comes from scripts/watch-test
This script creates a Symfony finder (used to scan our src and tests folders). We defined a temporary change file, but this is not strictly required for what we are doing. We next use an infinite loop. ResourceWatcher has a method we can use to see if any file was created, modified, or deleted.
New, let's find which files have been changed and rebuild them:
require_once __DIR__ . "/vendor/autoload.php"; Pre\Plugin\process(__DIR__ . "/example.pre");
This comes from scripts/watch-test
This code is similar to what we do in the bootstrap file, but it only applies to the changed files. We should also rerun the test when the file changes:
php loader.php
This comes from scripts/watch-test
We are introducing several environment variables. You can manage these variables to your liking, but I prefer to add them to the composer script:
$closure = function ($user) { print "user: " . $user["id"] . PHP_EOL; };
This is from composer.json
APP_COVER is not that important. It just tells the observer whether the script contains code coverage. APP_REBUILD plays a more important role: it controls whether the Pre file is rebuilt when the tests/bootstrap.php file is loaded. We need to modify the file so that the file is rebuilt only when requested:
public function closureWithUser(Closure $closure) { return [$closure = $closure ?? null, "fn" => function () use (&$closure) { $closure($this->user); }]["fn"]; }
This comes from tests/bootstrap.php
We also need to modify the observer script to set this environment variable before including bootstrap code. The entire observer script looks like this:
composer require --dev phpunit/phpunit
This comes from scripts/watch-test
Now we should be able to start it and run our tests every time the preprocessable file changes...
A few things to remember (rawr). First, you need chmod x scripts/* to run the observer script. Second, you need to set config: {process-timeout: 0} (in composer.json), otherwise the observer will die after 300 seconds.
This test observer also enabled a cool side effect: the ability to use preprocessor/conversion in our PHPUnit tests. If we add some code to tests/bootstrap.php:
composer require pre/short-closures
This comes from tests/bootstrap.php
…and we enable preprocessing in the test file (for Pre, that means rename it to .pre). Then we can start using the same preprocessor in our test file:
"autoload": { "psr-4": { "App\": "src" } }, "autoload-dev": { "psr-4": { "App\Tests\": "tests" } }
This comes from tests/SessionTest.pre
I can't believe I've done so much preprocessor work before trying to create such a test observer. This proves what we can learn from other languages and frameworks. If I'm not involved in those JavaScript projects, I may continue to rebuild my files before each test run. nausea!
Is this method effective for you? It can adapt to an asynchronous HTTP server or other long-running processes. Please let us know what you think in the comments.
Setting up JavaScript-style test observers in PHP involves multiple steps. First, you need to install PHPUnit and PHPUnit-Watcher. PHPUnit is a testing framework for PHP that provides a way to write tests for code. PHPUnit-Watcher is a tool that monitors your code and runs PHPUnit tests when saving files. After installing these tools, you can configure PHPUnit-Watcher to monitor your code and automatically run your tests. This setting allows you to get feedback on code changes immediately, which can help you discover and fix errors faster.
There are many benefits to using test observers in PHP. It provides instant feedback on code changes, which can help you discover and fix errors faster. It also saves you time because you don't have to run the test manually after every code change. Additionally, it encourages you to write tests for your code, which can improve the quality of your code and make it easier to maintain.
Yes, you can use PHP code inside JavaScript functions, but this is not recommended. PHP is a server-side language, while JavaScript is a client-side language. This means that the PHP code is executed on the server before the page is sent to the client, while the JavaScript code is executed on the client after the page is received. Therefore, if you try to use PHP code inside a JavaScript function, the PHP code will be executed before the JavaScript function, which may lead to unexpected results.
Codeception is a testing framework for PHP that supports unit testing, functional testing, and acceptance testing. To test your PHP code with Codeception, you first need to install Codeception and configure it for your project. You can then write tests for your code using the syntax of Codeception and run your tests using the command line tool of Codeception.
While technically you can write PHP code in JavaScript, this is not recommended. PHP is a server-side language, while JavaScript is a client-side language. This means that the PHP code is executed on the server before the page is sent to the client, while the JavaScript code is executed on the client after the page is received. Therefore, if you try to write PHP code in JavaScript, the PHP code will be executed before the JavaScript code, which can lead to unexpected results. Instead, it is better to use AJAX to send data from the client to the server and vice versa.
The above is the detailed content of How to Write JavaScript-Style Test Watchers in PHP. For more information, please follow other related articles on the PHP Chinese website!