haijin / specs
一个使用简单DSL的测试框架,灵感来源于RSpec。
这个包的官方仓库似乎已经不存在,因此该包已被冻结。
Requires
- php: >=5.5.0
- clue/commander: ^1.3
- questocat/console-color: ^1.0.0
Requires (Dev)
README
一个使用简单DSL的测试框架,灵感来源于RSpec。
版本 2.0.0
如果您非常喜欢它,可以通过资助其开发来做出贡献。
目录
安装
将此库包含在您的项目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() { });
可以在任何级别使用和混合多个 beforeAll
、afterAll
、beforeEach
和 afterEach
。
使用 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期望的格式相同
要运行单个失败的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