javascript编写_如何在PHP中编写JavaScript样式的测试观察程序

javascript编写

This article was peer reviewed by Younes Rafie. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

该文章由Younes Rafie进行了同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!



I didn’t start out writing tests for my code. Like many before and since, my “testing” was to write code and refresh the page. “Does it look right?”, I’d ask myself. If I thought so, I’d move on.

我不是开始为代码编写测试。 像之前和之后的许多事情一样,我的“测试”是编写代码并刷新页面。 “看起来不错吗?”,我问自己。 如果我这样想,我会继续前进。

In fact, most of the jobs I’ve had have been with companies who don’t much care for other forms of testing. It’s taken many years, and wise words from people like Chris Hartjes, for me to see the value in testing. And I’m still learning what good tests look like.

实际上,我所从事的大部分工作都是在对其他形式的测试不太在意的公司中进行的。 我花了很多年的时间,并且从Chris Hartjes这样的人那里获得了明智的话,我才看到测试的价值。 而且我还在学习好的测试是什么样的。

Vector icon with eye

I recently started working on a few JavaScript projects which had bundled test watchers.

我最近开始从事一些捆绑了测试观察程序JavaScript项目。

Here’s a great premium video tutorial about test driven NodeJS development!

这是有关测试驱动的NodeJS开发的优质视频教程

In the land of JavaScript, it’s not uncommon to preprocess source code. In the land of JavaScript, developers write in syntax not widely supported, and the code is transformed into syntax that is widely supported, usually using a tool called Babel.

在JavaScript领域,预处理源代码并不罕见。 在JavaScript领域,开发人员使用不受广泛支持的语法进行编写,通常使用称为Babel的工具将代码转换为受到广泛支持的语法。

In order to reduce the burden of invoking the transformation scripts, boilerplate projects have started to include scripts to automatically watch for file changes; and thereafter invoke these scripts.

为了减轻调用转换脚本的负担,样板项目已开始包含自动监视文件更改的脚本。 然后调用这些脚本。

These projects I’ve worked on have used a similar approach to re-run unit tests. When I change the JavaScript files, these files are transformed and the unit tests are re-run. This way, I can immediately see if I’ve broken anything.

我从事的这些项目使用了类似的方法来重新运行单元测试。 当我更改JavaScript文件时,将转换这些文件并重新运行单元测试。 这样,我可以立即查看是否损坏了任何东西。

The code for this tutorial can be found on Github. I’ve tested it with PHP 7.1.

可以在Github上找到本教程的代码。 我已经用PHP 7.1对其进行了测试。

设置项目 (Setting up the Project)

Since starting to work on these projects, I’ve started to set a similar thing up for PHPUnit. In fact, the first project I set up the PHPUnit watcher script on was a PHP project that also preprocesses files.

自从开始从事这些项目以来,我就开始为PHPUnit设置类似的设置。 实际上,我在上面设置了PHPUnit watcher脚本的第一个项目是一个还预处理文件PHP项目。

It all started after I added preprocessing scripts to my project:

在将预处理脚本添加到项目后,一切都开始了:

composer require pre/short-closures

These particular preprocessing scripts allow me to rename PSR-4 autoloaded classes (from path/to/file.phppath/to/file.pre), to opt-in to the functionality they provide. So I added the following to my composer.json file:

这些特殊的预处理脚本允许我重命名PSR-4自动加载的类(从path/to/file.php path/to/file.pre ),以选择加入它们提供的功能。 所以我将以下内容添加到我的composer.json文件中:

"autoload": {
    "psr-4": {
        "App\\": "src"
    }
},
"autoload-dev": {
    "psr-4": {
        "App\\Tests\\": "tests"
    }
}

This is from composer.json

这是来自composer.json

I then added a class to generate functions with 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 is from src/Session.pre

这是来自src/Session.pre

To check if this works, I’ve set up a small example 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 is from example.pre

这是来自example.pre

…And because I want to use the short closures in a non-PSR-4 class, I also need to set up a loader:

…并且因为我想在非PSR-4类中使用简短的闭包,所以我还需要设置一个加载器:

require_once __DIR__ . "/vendor/autoload.php";

Pre\Plugin\process(__DIR__ . "/example.pre");

This is from loader.php

这是来自loader.php

This is a lot of code to illustrate a small point. The Session class has a closureWithUser method, which accepts a closure and returns another. When called, this new closure will call the original closure, providing the user session array as an argument.

这是很多代码来说明的一点。 Session类具有一个closureWithUser方法,该方法接受一个闭包并返回另一个闭包。 调用时,此新闭包将调用原始闭包,并提供用户会话数组作为参数。

To run all of this, type into terminal:

要运行所有这些,请在终端中输入:

php loader.php

As a side-note, the valid PHP syntax that these preprocessors generated is lovely. It looks like this:

附带说明一下,这些预处理程序生成的有效PHP语法很漂亮。 看起来像这样:

$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 probably don’t want to commit both php and pre files to the repo. I’ve added app/**/*.php and examples.php to .gitignore for that reason.

您可能不想将phppre文件都提交到存储库。 因此,我已经将app/**/*.phpexamples.php.gitignore中。

设置测试 (Setting up the Tests)

So how do we test this? Let’s start by installing PHPUnit:

那么我们如何测试呢? 让我们从安装PHPUnit开始:

composer require --dev phpunit/phpunit

Then, we should create a config 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

这是从phpunit.xml

Were we to run vendor/bin/phpunit, it would work. But we don’t have any tests yet. Let’s make one:

如果我们运行vendor/bin/phpunit ,它将起作用。 但是我们还没有任何测试。 让我们做一个:

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 is from tests/SessionTest.php

这是来自tests / SessionTest.php

When we run vendor/bin/phpunit, the single test passes. Yay!

当我们运行vendor/bin/phpunit ,单个测试通过。 好极了!

我们缺少什么? (What Are We Missing?)

So far, so good. We’ve written a tiny bit of code, and a test for that code. We don’t even need to worry about how the preprocessing works (a step up from JavaScript projects).

到目前为止,一切都很好。 我们已经编写了一些代码,并对该代码进行了测试。 我们甚至不必担心预处理的工作原理(比JavaScript项目更进一步)。

The troubles begin when we try to check code coverage:

当我们尝试检查代码覆盖率时,麻烦就开始了:

vendor/bin/phpunit --coverage-html coverage

Since we have a test for Session, the coverage will be reported. It’s a simple class, so we already have 100% coverage for it. But if we add another class:

由于我们已经测试了Session ,因此将报告覆盖率。 这是一个简单的类,所以我们已经有100%的覆盖率。 但是,如果我们添加另一个类:

namespace App;

class BlackBox
{
    public function get($key)
    {
        return $GLOBALS[$key];
    }
}

This is from src/BlackBox.pre

这是来自src/BlackBox.pre

What happens when we check the coverage? Still 100%.

我们检查承保范围会怎样? 仍然是100%。

This happens because we don’t have any tests which load BlackBox.pre, which means it is never compiled. So, when PHPUnit looks for covered PHP files, it doesn’t see this preprocess-able file.

发生这种情况是因为我们没有任何加载BlackBox.pre测试,这意味着它永远不会被编译。 因此,当PHPUnit查找覆盖PHP文件时,看不到此可预处理文件。

测试前生成所有文件 (Building All Files before Testing)

Let’s create a new script to build all the Pre files, before trying to run the tests:

在尝试运行测试之前,让我们创建一个新脚本来构建所有Pre文件:

require_once __DIR__ . "/../vendor/autoload.php";

function getFileIteratorFromPath($path) {
    return new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($path),
        RecursiveIteratorIterator::SELF_FIRST
    );
}

function deleteFilesBeforeTests($path) {
    foreach (getFileIteratorFromPath($path) as $file) {
        if ($file->getExtension() === "php") {
            unlink($file->getPathname());
        }
    }
}

function compileFilesBeforeTests($path) {
    foreach (getFileIteratorFromPath($path) as $file) {
        if ($file->getExtension() === "pre") {
            $pre = $file->getPathname();
            $php = preg_replace("/pre$/", "php", $pre);

            Pre\Plugin\compile($pre, $php, true, true);

            print ".";
        }
    }
}

print "Building files" . PHP_EOL;

deleteFilesBeforeTests(__DIR__ . "/../src");
compileFilesBeforeTests(__DIR__ . "/../src");

print PHP_EOL;

This is from tests/bootstrap.php

这是来自tests/bootstrap.php

Here we create 3 functions; one for getting a recursive file iterator (from a path), one for deleting the files of this iterator, and one for re-compiling the Pre files.

在这里,我们创建3个函数; 一种用于获取递归文件迭代器(从路径),一种用于删除该迭代器的文件,一种用于重新编译Pre文件。

We need to substitute the current bootstrap file in phpunit.xml:

我们需要在phpunit.xml替换当前的引导文件:

<phpunit
    bootstrap="tests/bootstrap.php"
    ...
>

This is from phpunit.xml

这是从phpunit.xml

Now, whenever we run the tests, this script will first clean and rebuild all the Pre files to PHP files. Coverage is correctly reported, and we can be on our merry way…

现在,无论何时运行测试,此脚本都将首先清理所有Pre文件并将其重建为PHP文件。 覆盖范围已正确报告,我们可以继续前进……

除了这件事... (Except for This Other Thing…)

Our codebase is tiny, but it doesn’t need to be. We could try this in a real application, and immediately regret having to rebuild files every time we want to test.

我们的代码库很小,但不是必须的。 我们可以在真实的应用程序中尝试此操作,并且立即后悔每次都要进行测试时都必须重新生成文件。

In this project I mentioned, I have 101 Pre files. That’s a lot of preprocessing just to run my (hopefully fast) unit test suite. What we need is a way to watch for changes, and only rebuild the bits that matter. To begin, let’s install a file watcher:

在我提到的这个项目中,我有101个Pre文件。 为了运行我的(希望很快)的单元测试套件,需要进行大量的预处理。 我们需要的是一种观察变化的方法,并且仅重建重要的部分。 首先,让我们安装文件监视程序:

composer require --dev yosymfony/resource-watcher

Then, let’s create a test script:

然后,让我们创建一个测试脚本:

#!/usr/bin/env php
<?php

require_once __DIR__ . "/../tests/bootstrap.php";

use Symfony\Component\Finder\Finder;
use Yosymfony\ResourceWatcher\ResourceWatcher;
use Yosymfony\ResourceWatcher\ResourceCacheFile;

$finder = new Finder();

$finder->files()
    ->name("*.pre")
    ->in([
        __DIR__ . "/../src",
        __DIR__ . "/../tests",
    ]);

$cache = new ResourceCacheFile(
    __DIR__ . "/.test-changes.php"
);

$watcher = new ResourceWatcher($cache);
$watcher->setFinder($finder);

while (true) {
    $watcher->findChanges();

    if ($watcher->hasChanges()) {
        // ...do some rebuilding
    }

    usleep(100000);
}

This is from scripts/watch-test

这是来自scripts/watch-test

The script creates a Symfony finder (to scan our src and tests folders). We define a temporary change file, but it’s not strictly required for what we’re doing. We follow this up with an infinite loop. The ResourceWatcher has a method we can use to see if any files have been created, modified, or deleted.

该脚本创建一个Symfony finder(以扫描我们的srctests文件夹)。 我们定义了一个临时更改文件,但是我们所做的并不是严格要求的。 我们通过无限循环进行跟踪。 ResourceWatcher提供了一种方法,可以用来查看是否已创建,修改或删除任何文件。

New, let’s find which files have changed, and rebuild them:

新建,让我们查找哪些文件已更改,然后重建它们:

if ($watcher->hasChanges()) {
    $resources = array_merge(
        $watcher->getNewResources(),
        $watcher->getDeletedResources(),
        $watcher->getUpdatedResources()
    );

    foreach ($resources as $resource) {
        $pre = realpath($resource);
        $php = preg_replace("/pre$/", "php", $pre);

        print "Rebuilding {$pre}" . PHP_EOL;

        Pre\Plugin\compile($pre, $php, true, true);
    }

    // ...re-run tests
}

This is from scripts/watch-test

这是来自scripts/watch-test

This code is similar to what we did in the bootstrap file, but it is only applied to changed files. When a file changes, we should also re-run the tests:

此代码类似于我们在引导文件中执行的代码,但仅适用于更改的文件。 当文件更改时,我们还应该重新运行测试:

if (empty(getenv("APP_COVER"))) {
    passthru("APP_REBUILD=0 composer run test");
} else {
    passthru("APP_REBUILD=0 composer run test:coverage");
}

This is from scripts/watch-test

这是来自scripts/watch-test

We’re introducing a couple of environment variables. You can manage these however you like, but my preference is to add them to composer scripts:

我们将介绍几个环境变量。 您可以随意管理它们,但我的首选是将它们添加到作曲家脚本中:

"scripts": {
    "test": "vendor/bin/phpunit",
    "test:cover": "vendor/bin/phpunit --coverage-html cover",
    "watch:test": "APP_COVER=0 scripts/watch-test",
    "watch:test:cover": "APP_COVER=1 scripts/watch-test",
},

This is from composer.json

这是来自composer.json

APP_COVER isn’t all that important. It just tells the watcher script whether or not to include code coverage. APP_REBUILD plays a more important role: it controls whether the Pre files are rebuilt when the tests/bootstrap.php file is loaded. We need to modify that file, so that the files are only rebuilt when requested:

APP_COVER并不是那么重要。 它只是告诉观察者脚本是否包括代码覆盖率。 APP_REBUILD扮演着更重要的角色:它控制在加载tests/bootstrap.php文件时是否重建Pre文件。 我们需要修改该文件,以便仅在需要时才重建文件:

if (!empty(getenv("APP_REBUILD"))) {
    print "Building files" . PHP_EOL;

    deleteFilesBeforeTests(__DIR__ . "/../src");
    compileFilesBeforeTests(__DIR__ . "/../src");

    print PHP_EOL;
}

This is from tests/bootstrap.php

这是来自tests/bootstrap.php

We also need to modify the watcher script to set this environment variable before including the bootstrap code. The whole watcher script looks like this:

在包含引导程序代码之前,我们还需要修改监视程序脚本以设置此环境变量。 整个观察者脚本如下所示:

#!/usr/bin/env php
<?php

putenv("APP_REBUILD=1");
require_once __DIR__ . "/../tests/bootstrap.php";

use Symfony\Component\Finder\Finder;
use Yosymfony\ResourceWatcher\ResourceWatcher;
use Yosymfony\ResourceWatcher\ResourceCacheFile;

$finder = new Finder();

$finder->files()
    ->name("*.pre")
    ->in([
        __DIR__ . "/../src",
        __DIR__ . "/../tests",
    ]);

$cache = new ResourceCacheFile(
    __DIR__ . "/.test-changes.php"
);

$watcher = new ResourceWatcher($cache);
$watcher->setFinder($finder);

while (true) {
    $watcher->findChanges();

    if ($watcher->hasChanges()) {
        $resources = array_merge(
            $watcher->getNewResources(),
            $watcher->getDeletedResources(),
            $watcher->getUpdatedResources()
        );

        foreach ($resources as $resource) {
            $pre = realpath($resource);
            $php = preg_replace("/pre$/", "php", $pre);

            print "Rebuilding {$pre}" . PHP_EOL;

            Pre\Plugin\compile($pre, $php, true, true);
        }

        if (empty(getenv("APP_COVER"))) {
            passthru("APP_REBUILD=0 composer run test");
        } else {
            passthru("APP_REBUILD=0 composer run test:cover");
        }
    }

    usleep(100000);
}

This is from scripts/watch-test

这是来自scripts/watch-test

Now we should be able to launch this, and have it run our tests every time a preprocess-able file changes…

现在我们应该能够启动它,并在每次可预处理文件更改时让它运行我们的测试……

https://www.sitepoint.com/wp-content/uploads/2017/07/1500534190watcher.gif

There are a couple of things to bear in mind (rawr). The first is that you’ll need to chmod +x scripts/* to be able to run the watcher script. The second is that you’ll need to set config: {process-timeout: 0} (in composer.json) or the watcher will die after 300 seconds.

有几件事要牢记(原始)。 首先是您需要chmod +x scripts/*才能运行观察程序脚本。 第二个是您需要设置config: {process-timeout: 0} (在composer.json ),否则观察者将在300秒后死亡。

奖金回合! (Bonus Round!)

This test watcher also enables a cool side-effect: being able to use preprocessors/transformations in our PHPUnit tests. If we add a bit of code to tests/bootstrap.php:

这个测试观察器还带来了很酷的副作用:能够在我们PHPUnit测试中使用预处理器/转换。 如果我们将一些代码添加到tests/bootstrap.php

if (!empty(getenv("APP_REBUILD"))) {
    print "Building files" . PHP_EOL;

    deleteFilesBeforeTests(__DIR__ . "/../src");
    compileFilesBeforeTests(__DIR__ . "/../src");
    deleteFilesBeforeTests(__DIR__ . "/../tests");
    compileFilesBeforeTests(__DIR__ . "/../tests");

    print PHP_EOL;
}

This is from tests/bootstrap.php

这是来自tests/bootstrap.php

…And we enable preprocessing in our test files (for Pre that means renaming them to .pre). Then we, can start to use the same preprocessors in our test files:

…并且我们在测试文件中启用了预处理(对于Pre意味着将它们重命名为.pre )。 然后,我们可以开始在测试文件中使用相同的预处理器:

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 = ($user) => {
            $expected = "user: " . $user["id"];
        };

        $closureWithUser = $session
            ->closureWithUser($closure);

        $closureWithUser();

        $this->assertEquals("user: 1", $expected);
    }
}

This is from tests/SessionTest.pre

这是来自tests/SessionTest.pre

结论 (Conclusion)

I can’t believe I did so much preprocessor work before trying to create this kind of test watcher. It’s a testament of what we can learn from other languages and frameworks. Had I not worked on those JavaScript projects, I might have gone on rebuilding my files before each test run. Yuck!

我不敢相信在尝试创建这种测试监视程序之前,我做了太多的预处理工作。 这证明了我们可以从其他语言和框架中学到什么。 如果我不从事那些JavaScript项目,那么我可能会在每次测试运行之前继续重建文件。 !

Has this approach worked well for you? It can be adapted to work with async HTTP servers, or other long-running processes. Let us know what you think in the comments.

这种方法对您有效吗? 它可以适合与异步HTTP服务器或其他长时间运行的进程一起使用。 请在留言中让我们知道你的想法。

翻译自: https://www.sitepoint.com/write-javascript-style-test-watchers-php/

javascript编写

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值