filisko/testable-functions 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

filisko/testable-functions

Composer 安装命令:

composer require --dev filisko/testable-functions

包简介

A library for testing PHP's built-in functions or language constructs.

README 文档

README

Latest Version on Packagist Software License Testing Coverage Status Total Downloads

A library that provides an approach for testing code that heavily relies on PHP's built-in functions or language constructs (great for "include/require-oriented architectures", such as legacy projects) that are normally really hard to test.

There are no excuses for not testing PHP functions anymore!

Requirements

  • PHP >= 7.1

Installation

This package is installable and autoloadable via Composer as filisko/testable-functions.

composer require filisko/testable-functions

Usage

The package provides two main classes: Functions used in production and FakeFunctions used for testing.

These two classes allow you to use PHP's built-in functions and language constructs (require_once, include, echo, print, etc.) without having to worry about tests.

You can see a basic example here of production code and its tests, along with many comments to make the example clearer.

Functions class

This class is like a proxy for PHP functions. It uses the __call hook internally to forward function calls to PHP, and it also wraps PHP's language constructs like require_once inside functions so that they can be testable.

Using this class can be particularly useful for code that involves I/O operations because the results of those operations can be easily altered for testing purposes later on.

Imagine the following production code:

// this is passed to the constructor of the client class
$functions = new \Filisko\Functions();

// filesystem
$functions->file_exists($path);
$functions->is_dir($dirname);
$functions->is_file($filename);

// network
$functions->checkdnsrr($hostname);
$functions->fsockopen($hostname);

// clock
$functions->time();
$functions->date_create();

Then, by using the FakeFuctions class in the testing environment, the results of the functions can be easily altered like this:

// ----- inside the PHPUnit test case -----
use PHPUnit\Framework\Assert;

// this is the default value, and it means that
// undefined functions will fall back to their implementation
$failOnMissing = false;

$functions = new \Filisko\FakeFunctions([
    // if you don't add it here, it will fallback to PHP
    'time' => 1417011228,
    'date_create' => new DateTime('2025-05-15'),
    'is_dir' => function(string $path)  {
        // you can assert arguments here
        Assert::assertEquals('/path/to/dir', $path);

        return false;
    },
], $failOnMissing);

$fileManager = new FileManager($functions);

$this->assertEquals(false, $client->do());

// ------- inside the class under test -------

// returns 1417011228
$functions->time();

// returns DateTime('2025-05-15')
$functions->date_create();

// returns false
$functions->is_dir($dirname);

Legacy projects are usually "require/include oriented architectures", so the following can be very handy.

As you've read before, this package supports PHP language constructs (parsed differently than functions by PHP) wrapped in functions:

$functions->require_once($path);
$functions->require($path);
$functions->include_once($path);
$functions->include($path);
$functions->echo($text);
$functions->print($text);
$functions->exit($statusOrText);
$functions->die($statusOrText);

This makes it possible to alter them for testing purposes:

// ---------- inside a PHP Unit test ----------

$functions = new \Filisko\FakeFunctions([
    // simulating a file loading global vars
    'require_once' => function() {
        // you should never do this unless you're testing legacy code
        global $var
        $var = 1;
    },
    // you can also load anything (functions, classes, etc.) from a file
    'require' => function() {
        eval(file_get_contents(__DIR__ . '/functions.php'));
    },
    // simulating a file returning a value
    'include' => false,
]);

// ------- inside the class under test --------

// $var now is available
$functions->require_once($path);
global $var;

// returns false
$functions->include($dirname);

Keep in mind that loading things like globals will make them available across all the other tests, and you may not want this. To solve this issue, use PHPUnit's docblocks for each test method shown below:

  • globals (@backupGlobals)
  • classes or functions (@runInSeparateProcess, @preserveGlobalState disabled)
  • static variables or properties (@backupStaticAttributes)

... or at class level with @runTestsInSeparateProcesses.

/**
 * @runInSeparateProcess
 * @preserveGlobalState disabled
 */
public function test_with_functions_loading(): void
{
    // ...
}

Furthermore, passing --process-isolation to PHPUnit will globally apply @runInSeparateProcess to every single test, but it's not a good idea to do it unless you know what you're doing.

FakeFunctions class

As shown in the previous examples, this class is used as a replacement for the Functions class in the testing environment, but it also provides many helper methods for the tests.

$functions = new \Filisko\FakeFunctions([
    // any value  (can only be used once, otherwise an exception will be thrown)
    'some_function' => true,

    // callables (can only be used once, otherwise an exception will be thrown)
    'some_function' => function() {
        return true;
    },
    
    // this will return the same result no matter how many times it's called
    'some_function' => new FakeStatic($mixed),
   
    // a stack of values that will be used for each function call
    // it throws an exception when the stack is already consumed
    'some_function' => new FakeStack([true, false, 1, 2]),
]);

// it's also possible to preset the values using the set() method,
// this allows you to postpone the setting of a function's preset result until it is needed.
$functions->set('some_function', true);
$functions->set('some_function', new FakeStatic($mixed));
$functions->set('some_function', new FakeStack([true, false, 1, 2]));

// We can adjust whether we want to throw an exception when a result for a function is not set,
// yet the function was called anyway (like an unexpected call).
// This configuration defaults to false, which causes a fallback to PHP's native functions when a mock was not set.
// On the other hand, enabling it can be very useful to make sure that only expected calls are made.
// (e.g.: avoid unexpected DB calls in legacy code or external HTTP requests)
$failOnMissing = true;
$functions = new \Filisko\FakeFunctions([
    // this is used to fallback to the native implementation
    // even though $failOnMissing is enabled (it will fail otherwise)
    'mail' => new FakeFallback(),
    
    // if a call is made and not mocked here, it will fail
    'other_function' => true
], $failOnMissing);

// returns a bool of whether a function was called or not
$functions->wasCalled('require_once');

// returns an int of the number of times that a function was called
$functions->wasCalledTimes('require_once');

// returns a list (array<string,int>) of function names together with the pending calls
// e.g.: [ 'function_exists' => 2 ]  
$functions->pendingCalls();

// returns an int for the pending calls of a specific function (it will throw an exception for non-defined function calls)
$functions->pendingCalls('filter_var');

// returns the total of all the pending calls
// (this can be used to assert that all the values were consumed by the end of the test)
$functions->pendingCallsCount();

// returns an array of all the calls together with the arguments of each call
// the example below is the result of two calls for the same function with different argument each time
// e.g.: [ 'require_once' => [['file.php'], ['test.php']] ]
$functions->calls();

// returns an array of calls for one specific function
// e.g.: [['file.php'], ['test.php']]
$functions->calls('require_once');
// e.g.: ['test.php']
$functions->calls('require_once')[1];

// returns the first call of a function (throws an exception if it wasn't called yet)
// e.g.: ['argument']
$functions->first('filter_var');
// e.g.: 'argument'
$functions->first('filter_var')[0];

// returns the first argument of the first function call (throws an exception if it wasn't called yet)
// e.g.: 'argument'
$functions->firstArgument('filter_var');

// returns the list of returned results of a 'real function call' (a function that actually run != mocked value)
// it won't return the result of mocked/preset function results or language constructs like require, echo, etc.
// e.g.: ['fecbada3-0c27-47ce-addf-f840050ee204', '...']
$functions->results('uuid');
$functions->results('uuid')[0];

// the same as results() but it only returns the last result of the function call
// this is useful when you want to avoid hardcoding a result just so that you have control over it.
// this way, you let the code run and only get the result of the function to do something more with it (e.g.: an ID) 
// e.g.: 'fecbada3-0c27-47ce-addf-f840050ee204'
$functions->lastResult('uuid');
// you can also specify the number of result you want to get starting from the end
// 0 is last (default), 1 is penultimate, etc.
$functions->lastResult('uuid', 1);

// returns an array of string[] of all the echos
$functions->echos();

// returns a bool of whether the string was echoed or not
$functions->wasEchoed('Was I echoed?');

// returns an array of string[] of all the prints
$functions->prints();

// returns a bool of whether the string was printed or not
$functions->wasPrinted('Was I printed?');

// returns a bool of whether die() was called or not
$functions->died();

// returns the die code or string that was passed to die($status)
$functions->dieCode();

// returns a bool of whether exit() was called or not
$functions->exited();

// returns the exit code or string that was passed to exit($status)
$functions->exitCode();

// returns a bool of whether a file was included or not
$functions->wasIncluded('file.php');

// returns a bool of whether a file was included once or not
$functions->wasIncludedOnce('file.php');

// returns a bool of whether a file was required or not
$functions->wasRequired('file.php');

// returns a bool of whether a file was required once or not
$functions->wasRequiredOnce('file.php');

Why to use this package?

Why to choose this package over a hundred other tools out there?

Because we prefer simplicity over complicated mocking tools, and we just want enough to accomplish our goal.

This is a common example from other mocking tools:

$builder = new MockBuilder();
$builder->setNamespace(__NAMESPACE__)
        ->setName("time")
        ->setFunction(
            function () {
                return 1417011228;
            }
        );
                    
$mock = $builder->build()

$result = $mock->time();

While for us it's simply:

$functions = new FakeFunctions([
    'time' => 1417011228
]);

$result = $functions->time();

Cons

Injecting Functions into the class.

For some, this is a con; for us, it's simply how it should be. We follow the DI principle for almost everything, so why not for this?

We consider it a good practice to always set the Functions dependency in the constructor as the last one, so that "it doesn't get in our way" and also set it as default so that nothing needs to be passed to the constructor in production. Only when testing.

class Filesystem
{
    public function __construct(
        private SomeService $service,
        private Functions $functions = new Functions()
    ) {}
}

PHP functions autocomplete is lost in IDEs

This is a natural consequence of this lib's approach. PHP functions autocomplete is lost given that we use a 'Proxy' class.

However, we developed a tool that generates a 'Stub' file with all the PHP functions so that your IDE keeps the autocomplete for them.

All you have to do is run the following command:

./vendor/filisko/testable-functions/bin/generate_stub.php [optinal path folder, by default it's <project>/.phpstorm-stubs]

Keep in mind that the functions that it loads in the Stub file are based on the active PHP extensions at runtime and loaded by composer.

Using this tool is optional, but it's recommended.

Other testing utilities

License and Contribution

Please see CHANGELOG for more information about recent changes and CONTRIBUTING for contributing details.

The MIT License (MIT). Please see LICENSE for more information.

filisko/testable-functions 适用场景与选型建议

filisko/testable-functions 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 415 次下载、GitHub Stars 达 2, 最近一次更新时间为 2025 年 05 月 18 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「php」 「testing」 「mock」 「fake」 「unit」 「testable」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 filisko/testable-functions 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 filisko/testable-functions 我们能提供哪些服务?
定制开发 / 二次开发

基于 filisko/testable-functions 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 415
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 3
  • 点击次数: 18
  • 依赖项目数: 1
  • 推荐数: 0

GitHub 信息

  • Stars: 2
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2025-05-18