haijin/specs

一个使用简单DSL的测试框架,灵感来源于RSpec。

这个包的官方仓库似乎已经不存在,因此该包已被冻结。

v2.0.0 2019-04-28 14:11 UTC

This package is auto-updated.

Last update: 2022-12-28 23:11:19 UTC


README

一个使用简单DSL的测试框架,灵感来源于RSpec。

Latest Stable Version Build Status License

版本 2.0.0

如果您非常喜欢它,可以通过资助其开发来做出贡献。

目录

  1. 安装
  2. 使用
    1. 规格定义
    2. 内置期望
      1. expect( $object ) ->to() ->be() ->like(...)
      2. expect( $object ) ->to() ->be() ->exactlyLike(...)
    3. 规格结构
    4. 在运行期望前后评估代码
    5. 使用let(...)表达式定义值
    6. 使用def(...)定义方法
    7. 定义自定义期望
      1. 期望定义结构
      2. 获取正在验证的值
      3. 定义闭包的参数
      4. 引发期望错误
      5. 在自定义期望中评估闭包
      6. 完整示例
    8. 临时跳过一个规格
    9. 分解和重用规格行为
    10. 从命令行运行规格
    11. 生成覆盖率报告
  3. 运行此项目测试

安装

将此库包含在您的项目composer.json文件中

{
    ...

    "require-dev": {
        ...
        "haijin/specs": "^2.0",
        ...
    },

    ...
}

使用

在项目文件夹中运行

composer install

php ./vendor/bin/specs init

这将创建一个名为tests/specs的目录和一个specsBoot.php文件。

tests/specs的任何嵌套子目录中创建包含规格定义的文件。这些文件不需要遵循任何命名约定,所有文件都将被视为规格文件。

tests/specsBoot.php是一个可选的常规PHP脚本文件,在加载任何其他规格文件之前加载,并可用于自定义规格运行器。

规格定义

规格文件包含对功能或功能的期望。

规格文件看起来像这样

<?php

$spec->describe( "When formatting a user's full name", function() {

    $this->it( "appends the user's last name to the user's name", function() {

        $user = new User( "Lisa", "Simpson" );

        $this->expect( $user->getFullName() ) ->to() ->equal( "Lisa Simpson" );

    });

});

内置期望

期望与PHPUnit中使用的断言相当。

期望有两个主要部分,期望所表达的价值,例如一个包含用户全名的字符串

$this->expect( $user->getFullName() )

以及对该值的期望

->to() ->equal('Lisa Simpson');

Specs库内置了最常见的期望

// Comparison expectations

$this->expect( $value ) ->to() ->equal( $anotherValue );
$this->expect( $value ) ->to() ->be( ">" ) ->than( $anotherValue );
$this->expect( $value ) ->to() ->be( "===" ) ->than( $anotherValue );
$this->expect( $value ) ->to() ->be() ->null();
$this->expect( $value ) ->to() ->be() ->true();
$this->expect( $value ) ->to() ->be() ->false();
$this->expect( $value ) ->to() ->be() ->like([
    "name" => "Lisa",
    "lastName" => "Simpson",
    "address" => [
        "streetName" => "Evergreen",
        "streetNumber" => 742
    ]
]);
$this->expect( $value ) ->to() ->be() ->exactlyLike([
    "name" => "Lisa",
    "lastName" => "Simpson",
    "address" => [
        "streetName" => "Evergreen",
        "streetNumber" => 742
    ]
]);

// Types expectations

$this->expect( $value ) ->to() ->be() ->string();
$this->expect( $value ) ->to() ->be() ->int();
$this->expect( $value ) ->to() ->be() ->double();
$this->expect( $value ) ->to() ->be() ->number();
$this->expect( $value ) ->to() ->be() ->bool();
$this->expect( $value ) ->to() ->be() ->array();
$this->expect( $value ) ->to() ->be() ->a( SomeClass::class );
$this->expect( $value ) ->to() ->be() ->instanceOf( SomeClass::class );

// String expectations

$this->expect( $stringValue ) ->to() ->beginWith( $substring );
$this->expect( $stringValue ) ->to() ->endWith( $substring );
$this->expect( $stringValue ) ->to() ->contain( $substring );
$this->expect( $stringValue ) ->to() ->match( $regexp );
$this->expect( $stringValue ) ->to() ->match( $regexp, function($matched) {
    // further expectations on the $matched elements, for instance:
    $this->expect( $matched[1] ) ->to() ->equal(...) ;
});

// Array expectations

$this->expect( $arrayValue ) ->to() ->include( $value );
$this->expect( $arrayValue ) ->to() ->includeAll( $values );
$this->expect( $arrayValue ) ->to() ->includeAny( $values );
$this->expect( $arrayValue ) ->to() ->includeNone( $values );
$this->expect( $arrayValue ) ->to() ->includeKey( $key );
$this->expect( $arrayValue ) ->to() ->includeKey( $key, funtion($value) {
    // further expectations on the $value, for instance:
    $this->expect( $value ) ->to() ->equal(...) ;
});
$this->expect( $arrayValue ) ->to() ->includeValue( $value );

// File expectations

$this->expect( $filePath ) ->to() ->be() ->aFile();
$this->expect( $filePath ) ->to() ->haveFileContents( function($contents) {
    // further expectations on the $contents, for instance:
    $this->expect( $contents ) ->to() ->match(...) ;
});

$this->expect( $filePath ) ->to() ->be() ->aDirectory();
$this->expect( $filePath ) ->to() ->haveDirectoryContents( function($files, $filesBasePath) {
    // further expectations on the $files
});

// Exceptions

$this->expect( function() {

    throw Exception();

}) ->to() ->raise( Exception::class );


$this->expect( function() {

        throw Exception( "Some message." );

}) ->to() ->raise( Exception::class, function($e) {
    // further expectations on the Exception instance, for instance:
    $this->expect( $e->getMessage() ) ->to() ->equal(...);        

});

大多数期望也可以用以下方式否定

$this->expect( $value ) ->not() ->to() ->equal( $anotherValue );

$this->expect( function() {

    throw Exception();

}) ->not() ->to() ->raise( Exception::class );

expect( $object ) ->to() ->be() ->like(...)

期望expect( $object ) ->to() ->be() ->like(...)评估数组、关联数组、对象及其任何组合的嵌套期望。

示例

$user = [
    'name' => "Lisa",
    'lastName' => "Simpson",
    'address' => [
        'streetName' => "Evergreen",
        'streetNumber' => 742
    ],
    'ignoredAttribute' => ""
];

$this->expect( $user ) ->to() ->be() ->like([
    'name' => "Lisa",
    'lastName' => "Simpson",
    'address' => [
        'streetName' => "Evergreen",
        'streetNumber' => 742
    ]
]);

它还适用于getter函数

$this->expect( $user ) ->to() ->be() ->like([
    'getName()' => "Lisa",
    'getLastName()' => "Simpson",
    'getAddress()' => [
        'getStreetName()' => "Evergreen",
        'getStreetNumber()' => 742
    ]
]);

期望使用等式(==)来比较值。要使用自定义期望对单个值使用闭包

$this->expect( $user ) ->to() ->be() ->like([
    'getName()' => function($value) { $this->expect( $value ) ->not() ->to() ->be() ->null(); },
    'getLastName()' => "Simpson",
    'getAddress()' => [
        'getStreetName()' => "Evergreen",
        'getStreetNumber()' => 742
    ]
]);

expect( $object ) ->to() ->be() ->exactlyLike(...)

expect( $object ) ->to() ->be() ->like(...) 相同,但如果对象是一个数组并且具有比预期值更多或更少的属性,则期望失败。

规格结构

一个规格以一个 $spec->decribe(...) 语句开始,可以包含任意数量的附加嵌套 $this->describe() 语句。每个 describe() 语句记录了一组相关期望,例如,因为它们声明了相同功能的不同预期行为。

->it(...) 语句是声明期望的地方。

$spec->describe( "When formatting a user's full name", function() {

    $this->describe( "with both name and last name defined", function() {

        $this->it( "appends the user's last name to the user's name", function() {

            $user = new User( "Lisa", "Simpson" );

            $this->expect( $user->getFullName() ) ->to() ->equal( "Lisa Simpson" );

        });

    });

    $this->describe( "with the name undefined", function() {

        $this->it( "returns only the last name", function() {

            $user = new User( "", "Simpson" );

            $this->expect( $user->getFullName() ) ->to() ->equal( "Simpson" );

        });

    });

    $this->describe( "with the last name undefined", function() {

        $this->it( "returns only the name", function() {

            $user = new User( "Lisa", "" );

            $this->expect( $user->getFullName() ) ->to() ->equal( "Lisa" );

        });

    });
});

在运行期望之前和之后评估代码

要评估在运行每个规格之前和之后的语句,请在任何 describe 语句中使用 beforeEach($closure)afterEach($closure)。它们是 TestCase 上的 setUp()tearDown() 函数的等价物。

$spec->describe( "When formatting a user's full name", function() {

    $this->beforeEach( function() {
        $this->n = 0;
    });

    $this->afterEach( function() {
        $this->n = null;
    });

    $this->describe( "with both name and last name defined", function() {

        $this->beforeEach( function() {
            $this->n += 1;
        });

        $this->afterEach( function() {
            $this->n -= 1;
        });


        $this->it( "...", function() {
            print $this->n;
        });

    });

});

要评估在运行一个 describe 语句的所有规格之前和之后的语句,请使用 beforeAll($closure)afterAll($closure) 语句。

$spec->describe( "When formatting a user's full name", function() {

    $this->beforeAll( function() {
        $this->n = 0;
    });

    $this->afterAll( function() {
        $this->n = null;
    });

    $this->describe( "with both name and last name defined", function() {

        $this->beforeAll( function() {
            $this->n += 1;
        });

        $this->afterAll( function() {
            $this->n -= 1;
        });


        $this->it( "...", function() {
            print $this->n;
        });

    });

});

要评估在运行任何规格之前和之后的语句,例如建立数据库连接、创建表或创建复杂文件夹结构,或者在每个单独的语句之前和之后,请在 tests/specsBoot.php 文件中创建或添加以下配置

// tests/specsBoot.php

$specs->beforeAll( function() {

});

$specs->afterAll( function() {

});

$specs->beforeEach( function() {

});

$specs->afterEach( function() {

});

可以在任何级别使用和混合多个 beforeAllafterAllbeforeEachafterEach

使用 let(...) 表达式定义值

使用 let( $expressionName, $closure ) 语句定义表达式和常量。

使用 $this->let() 与在 TestCase 中的 setUp() 方法中初始化实例变量类似。

使用 let(...) 定义的表达式在第一次被每个规格引用时才会懒加载。

let(...) 表达式由子 describe(...) 规格继承,并且可以在子 describe(...) 的作用域内安全地覆盖。

let(...) 表达式可以引用另一个 let(...) 表达式。

示例

$spec->describe( "When searching for users", function() {

    $this->let( "userId", function() {
        return 1;
    });

    $this->it( "finds the user by id", function(){

        $user = Users::findById( $this->userId );

        $this->expect( $user ) ->not() ->to() ->beNull();

    });

    $this->describe( "the retrieved user data", function() {

        $this->let( "user", function() {
            return Users::findById( $this->userId );
        });

        $this->it( "includes the name", function() {

            $this->expect( $this->user->getName() ) ->to() ->equal( "Lisa" );

        });

        $this->it( "includes the lastname", function() {

            $this->expect( $this->user->getLastname() ) ->to() ->equal( "Simpson" );

        });

    });

});

还可以在 specsBoot.php 文件中定义全局级别的命名表达式,但请注意,这会使每个规格的表达性降低,并使其更难理解

// tests/specsBoot.php

$specs->let( "userId", function() {
    return 1;
});

使用 def(...) 定义方法

使用 def($methodName, $closure) 语句定义方法。

方法的行为和范围与 let(...) 表达式相同。

示例

$spec->describe( "...", function() {

    $this->def( "sum", function($n, $m) {
        return $n + $m;
    });

    $this->it( "...", function(){

        $this->expect( $this->sum( 3, 4 ) ->to() ->equal( 7 );

    });

});

定义自定义期望

期望定义结构

期望定义有 4 个部分,每个部分都使用闭包定义。

第一个是 $this->before($closure) 闭包。此闭包在评估值上的期望之前评估。此块是可选的,但可以用于执行期望所需的复杂计算,无论是断言还是否定闭包。

第二个是 $this->assertWith($closure) 闭包。此闭包用于评估值上的正期望。

第三个是 $this->negateWith($closure) 闭包。此闭包用于评估值上的否定期望。

第四个是 $this->after($closure) 闭包。在期望运行后评估此闭包,即使在抛出 ExpectationFailure 的情况下也是如此。此闭包是可选的,但可以用于释放之前闭包评估期间分配的资源。

获取待验证的值

要获取待验证的值,请使用 $this->actualValue

定义闭包的参数

4个闭包的参数是传递给Spec中期望的参数。例如,如果spec声明为

$this->expect( 1 ) ->not() ->to() ->equal( 2 );

对于equal期望的4个闭包的参数将是期望值2

$this->before( function($expectedValue) {
});

$this->assertWith( function($expectedValue) {
});

$this->negateWith( function($expectedValue) {
});

$this->after( function($expectedValue) {
});

引发期望错误

要引发期望失败,请使用 $this->raiseFailure($failureMessage)

在自定义期望中评估闭包

验证的值或某些期望参数可能是闭包。

要在自定义期望定义中评估闭包,请使用 evaluateClosure($closure, ...$params)

这是为了使闭包能够使用正确的绑定进行评估。

示例

Value_Expectations::defineExpectation( "customExpectation", function() {

    $this->assertWith( function($expectedClosure) {

        $this->evaluateClosure( $expectedClosure, $this->actualValue );

        // ...
    });
);

完整示例

以下是一个自定义验证的完整示例

Value_Expectations::defineExpectation( "equal", function() {

    $this->before( function($expectedValue) {
        $this->gotExpectedValue = $expectedValue == $this->actualValue;
    });

    $this->assertWith( function($expectedValue) {

        if( $this->gotExpectedValue ) {
            return;
        }

        $this->raiseFailure(
            "Expected value to equal {$expectedValue}, got {$this->actualValue}."
        );
    });

    $this->negateWith( function($expectedValue) {

        if( ! $this->gotExpectedValue ) {
            return;
        }

        $this->raiseFailure(
            "Expected value not to equal {$expectedValue}, got {$this->actualValue}."
        );
    });

    $this->after( function($expectedValue) {
    });
});

暂时跳过spec

要暂时跳过一个spec或一组spec,请在其定义前加上一个x

$spec->describe( "When searching for users", function() {

    $this->let( "userId", function() {
        return 1;
    });

    $this->xit( "finds the user by id", function(){

        $user = Users::findById( $this->userId );

        $this->expect( $user ) ->not() ->to() ->beNull();

    });

    $this->xdescribe( "the retrieved user data", function() {

        $this->let( "user", function() {
            return Users::findById( $this->userId );
        });

        $this->it( "includes the name", function() {

            $this->expect( $this->user->getName() ) ->to() ->equal( "Lisa" );

        });

        $this->it( "includes the lastname", function() {

            $this->expect( $this->user->getLastname() ) ->to() ->equal( "Simpson" );

        });

    });

});

分解和重用spec行为

要重用自定义spec方法和属性,请将它们定义在类的静态函数中

class HtmlSpecsMethods {

    static public function addTo($spec)
    {
        $spec->def( "navigateTo", function($requestUri) {
            /// ...
        });

        $spec->def( "clickLink", function($id) {
            /// ...
        });

    }

}

并使用以下方式包含方法:

// tests/specsBoot.php


HtmlSpecsMethods::addTo( $specs );

从命令行运行spec

使用以下命令运行所有spec

php ./vendor/bin/specs

或在项目的composer.json中添加以下行

"scripts": {
    "specs": "php ./vendor/bin/specs"
}

然后使用以下命令运行spec

composer specs

使用以下命令运行单个文件或文件夹中的所有spec

composer specs tests/specs/variables-scope/variables-scope.php

使用以下命令在行号上运行单个spec

composer specs tests/specs/variables-scope/variables-scope.php:49

行号必须在spec的作用域内。

当从命令行运行spec时,失败将被记录在文件名和行号中,格式与运行单个spec的runner期望的格式相同

Screenshot

要运行单个失败的spec,请从控制台摘要中复制失败的spec行,并将其粘贴到新的命令中

composer specs /home/php-specs/tests/specs/variablesScope/variablesScope.php:49

生成覆盖率报告

要生成测试代码覆盖率html报告,请按照以下步骤操作

安装您偏好的PHP调试工具。

例如,xdebug

Docker镜像haijin/php-dev:7.2已经安装了xdebug。

php-code-coverage包添加到项目的开发要求中。

在项目的composer.json文件中添加以下内容

 "require-dev": {
  ...
  "phpunit/php-code-coverage": "^7.0"
  ...
},
在评估spec之前初始化php-code-coverage

tests/specsBoot.php文件中添加以下内容

// tests/specsBoot.php
declare(strict_types=1);

use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Report\Html\Facade;

$specs->beforeAll( function() {
    $this->coverage = initializeCoverageReport();
});

$specs->afterAll( function() {
    generateCoverageReport($this->coverage);
});

function initializeCoverageReport()
{
    $coverage = new CodeCoverage;
    $coverage->filter()->addDirectoryToWhitelist('src/');
    $coverage->start('specsCoverage');

    return $coverage;
};

function generateCoverageReport($coverage)
{
    $coverage->stop();
    $writer = new Facade;
    $writer->process($coverage, 'coverage-report/');
};

这将在项目文件夹 coverage-report/ 中留下一个HTML覆盖率报告。

运行此项目测试

要运行此项目的测试,请执行以下操作

composer specs

或者,如果您想使用带有PHP 7.2的Docker镜像来运行测试

sudo docker run -ti -v $(pwd):/home/php-specs --rm --name php-specs haijin/php-dev:7.2 bash
cd /home/php-specs/
composer install
composer specs