sirbrillig/corretto

一个简单且表达性强的PHP测试运行器

v1.4.0 2016-12-30 15:57 UTC

This package is auto-updated.

Last update: 2024-09-20 10:17:32 UTC


README

Corretto 是一个简单且表达性强的PHP测试运行器。

模仿 mocha 设计,Corretto 提供了一种领域特定语言 (DSL),使得编写测试变得简单。无需类和长的函数名;只需用普通词语描述你的代码即可。毕竟,你更愿意花大部分时间在写代码而不是写测试上吗?

<?php
use function Corretto\describe, Corretto\it, Corretto\expect;

describe( 'isFive()', function() {
	it( 'returns true if its argument is five', function() {
		expect( isFive( 5 ) )->toBeTrue();
	} );

	it( 'returns false if its argument is not five', function() {
		expect( isFive( 6 ) )->toBeFalse();
	} );
} );

安装

首先,你需要 Composer。然后在你的项目中

composer require --dev sirbrillig/corretto

或者,要全局安装

composer global require sirbrillig/corretto

只需确保你的全局供应商二进制目录在 $PATH 中。有关更多信息,请参阅 全局安装文档

断言

Corretto 支持基本的断言

  • assertTrue()
  • assertFalse()
  • assertEquals()
  • assertNotEquals()

它还支持期望语法,这被推荐使用

  • expect( $actual )->toBeTrue()
  • expect( $actual )->toBeFalse()
  • expect( $actual )->toEqual( $expected )
  • expect( $actual )->toNotEqual( $expected )
  • expect( $actual )->toBeGreaterThan( $expected )
  • expect( $actual )->toBeLessThan( $expected )
  • expect( $actual )->toContain( $expected )
  • expect( $actual )->toNotContain( $expected )

自定义断言

编写自定义断言很简单。任何抛出 Exception 的函数都被视为测试失败!

(你也可以抛出 \Corretto\AssertionFailure,这将提供略微不那么嘈杂的失败信息。)

要向 expect() 添加新方法,你需要创建一个继承自 Corretto\Expectation 的类。在类中,添加测试 $this->actual 值的方法,这个值是传递给 expect() 的。然后,将类传递给函数 Corretto\extendExpectation()

例如,要添加 toBeFoo() 方法,你可以编写以下内容

class FooExpectation extends Corretto\Expectation {
	public function toBeFoo() {
		if ( ! $this->actual === 'foo' ) {
			throw new \Exception( 'not foo' );
		}
	}
}
Corretto\extendExpectation( 'FooExpectation' );

然后,你可以在测试中使用这个方法,例如

test( 'string is "foo"', function() {
	$string = 'foo';
	expect( $string )->toBeFoo();
} );

测试

测试是通过函数 it( string $name, callable $callable ) 定义的。此函数还别名为 test( string $name, callable $callable )specify( string $name, callable $callable )

可调用参数应该是一个匿名函数,其中至少包含一个断言。

it( 'does something', function() {
	...
} );

你可以通过省略其可调用参数来跳过测试,例如

it( 'does something not yet defined' );

你还可以通过添加字符串 SKIP 作为其第一个参数来跳过测试

it( 'SKIP', 'does something we should not run', function() {
	...
} );

套件

可以使用与测试类似的语法将测试组织成套件:describe( string $name, callable $callable )。这还别名为 suite( string $name, callable $callable )context( string $name, callable $callable )

describe( 'some tests to run', function() {
	test( '...' );
	test( '...' );
	test( '...' );
} );

默认情况下,总是定义了一个“根”套件,因此测试可以独立存在。

套件可以嵌套到任何深度。

describe( 'MyObject', function() {
	describe( 'getName()', function() {
		describe( 'when the name is missing', function() {
			it( 'returns a default name', function() {
				$obj = new MyObject();
				expect( $obj->getName() )->toEqual( 'default' );
			} );
		} );

		describe( 'when the name is set', function() {
			it( 'returns the name', function() {
				$obj = new MyObject( 'name' );
				expect( $obj->getName() )->toEqual( 'name' );
			} );
		} );
	} );
} );

你可以在套件的第一参数添加字符串 SKIP 来跳过套件中的所有测试

describe( 'SKIP', 'some tests not to run', function() {
	...
} );

Before,After

每个套件都可以有一个 before( callable $callable ),它将在套件中的所有测试运行之前被调用。同样,after( callable $callable ) 将在所有测试完成后运行。

还有 beforeEach( callable $callable )afterEach( callable $callable ),它们在套件(或任何嵌套套件)中的每个测试之前/之后运行其可调用参数。这些可以用来设置和恢复每个测试之间共享的数据。

describe( 'MyObject', function() {
	$ctx = new \StdClass();
	beforeEach( function() use ( &$ctx ) {
		$ctx->color = 'blue';
	} );

	describe( 'getName()', function() use ( &$ctx ) {
		beforeEach( function() use ( &$ctx ) {
			$ctx->obj = new MyObject();
		} );

		it( 'returns a default name when the name is missing', function() use ( &$ctx ) {
			expect( $ctx->obj->getName() )->toEqual( 'default' );
		} );

		it( 'returns the name', function() use ( &$ctx ) {
			$ctx->obj->name = 'name';
			expect( $ctx->obj->getName() )->toEqual( 'name' );
		} );

		it( 'returns a name matching the color', function() use ( &$ctx ) {
			$ctx->obj->name = $ctx->color;
			expect( $ctx->obj->getName() )->toEqual( $ctx->color );
		} );
	} );
} );

在PHP中使用use表达式向闭包传递变量是冗长的,并且这种模式很可能在测试中频繁使用。为了使这更容易,每个测试以及每个beforeEach/afterEach/before/after函数都会接收一个共享对象,该对象可以用于在函数之间传递数据。以下是使用对象而不是闭包的相同测试序列。

describe( 'MyObject', function() {
	beforeEach( function( $ctx ) {
		$ctx->color = 'blue';
	} );

	describe( 'getName()', function() {
		beforeEach( function( $ctx ) {
			$ctx->obj = new MyObject();
		} );

		it( 'returns a default name when the name is missing', function( $ctx ) {
			expect( $ctx->obj->getName() )->toEqual( 'default' );
		} );

		it( 'returns the name', function( $ctx ) {
			$ctx->obj->name = 'name';
			expect( $ctx->obj->getName() )->toEqual( 'name' );
		} );

		it( 'returns a name matching the color', function( $ctx ) {
			$ctx->obj->name = $ctx->color;
			expect( $ctx->obj->getName() )->toEqual( $ctx->color );
		} );
	} );
} );

运行器

使用corretto命令行工具来执行测试。它可以接受一个测试文件或测试文件的目录。如果没有提供文件,它将默认在当前目录中查找名为tests的目录。

该工具有几个名为报告器的输出选项,可以使用-R--reporter选项进行更改。默认报告器是spec,但还有一个base,它更简单,还有一个dots,它更类似于PHPUnit的默认输出。

通过扩展Corretto\Reporters\Base可以非常容易地编写自定义报告器。运行器会在发生任何事情时发出事件,报告器可以使用这些事件按自己的方式使用。

示例

<?php

use function Corretto\describe, Corretto\it;

use function Corretto\assertTrue, Corretto\assertFalse, Corretto\assertEquals, Corretto\assertNotEquals;

use function Corretto\test, Corretto\suite;

use function Corretto\specify, Corretto\context;

use function Corretto\beforeEach, Corretto\afterEach, Corretto\before, Corretto\after;

use function Corretto\expect;

it( 'allows tests outside a suite', function() {
	assertTrue( true );
} );

test( 'tests can use "test" as well as "it"', function() {
	assertTrue( true );
} );

specify( 'tests can use "specify" as well as "it"', function() {
	assertTrue( true );
} );

describe( 'describe()', function() {
	describe( 'when nested', function() {
		describe( 'more than once', function() {
			it( 'passes if its argument is true', function() {
				assertTrue( true );
			} );
		} );
		it( 'skips tests with no function' );
		it( 'SKIP', 'skips tests with the SKIP string as the first argument', function() {
			assertTrue( false );
		} );
		it( 'passes if its argument is true', function() {
			assertTrue( true );
		} );
	} );

	it( 'supports non-nested tests along with nested ones', function() {
		assertTrue( true );
	} );

	describe( 'when multiple tests are nested at the same level', function() {
		it( 'passes if its argument is true', function() {
			assertTrue( true );
		} );
	} );

	describe( 'SKIP', 'allows skipping whole suites', function() {
		it( 'passes if its argument is true', function() {
			assertTrue( false );
		} );
	} );
} );

context( 'a bunch of tests', function() {
	specify( 'suites can use "context" as well as "describe"', function() {
		assertTrue( true );
	} );
} );

suite( 'my tests', function() {
	test( 'suites can use "suite" as well as "describe"', function() {
		assertTrue( true );
	} );

	suite( 'there are many assertions', function() {
		test( 'assertEquals()', function() {
			$actual = 'expected';
			assertEquals( 'expected', $actual );
		} );

		test( 'assertNotEquals()', function() {
			$actual = 'actual';
			assertNotEquals( 'expected', $actual );
		} );

		test( 'assertTrue()', function() {
			assertTrue( true );
		} );

		test( 'assertFalse()', function() {
			assertFalse( false );
		} );
	} );

	suite( 'expectation syntax also works for assertions', function() {
		suite( 'expect()', function() {
			test( '->toBeTrue()', function() {
				expect( true )->toBeTrue();
			} );

			test( '->toBeFalse()', function() {
				expect( false )->toBeFalse();
			} );

			test( '->toEqual()', function() {
				expect( 'hi' )->toEqual( 'hi' );
			} );

			test( '->toNotEqual()', function() {
				expect( 'hi' )->toNotEqual( 'bye' );
			} );
		} );
	} );
} );

describe( 'set up and tear down', function() {
	$ctx = new \StdClass();

	describe( 'beforeEach()', function() use ( &$ctx ) {
		beforeEach( function() use ( &$ctx ) {
			$ctx->name = 'hello';
		} );

		it( 'sets up the test context', function() use ( &$ctx ) {
			expect( $ctx->name )->toEqual( 'hello' );
			$ctx->name = 'bye';
		} );

		it( 'runs again before each test', function() use ( &$ctx ) {
			expect( $ctx->name )->toNotEqual( 'bye' );
		} );
	} );

	describe( 'before()', function() use ( &$ctx ) {
		before( function() use ( &$ctx ) {
			$ctx->name = 'hello';
		} );

		it( 'sets up the test context', function() use ( &$ctx ) {
			expect( $ctx->name )->toEqual( 'hello' );
			$ctx->name = 'bye';
		} );

		it( 'runs only once before the suite runs', function() use ( &$ctx ) {
			expect( $ctx->name )->toEqual( 'bye' );
		} );
	} );

	describe( 'afterEach()', function() {
		$name = 'hello';
		afterEach( function() use ( &$name ) {
			$name = 'bye';
		} );

		it( 'is run after each test', function() use( &$name ) {
			expect( $name )->toEqual( 'hello' );
		} );

		it( 'runs again after each test', function() use ( &$name ) {
			expect( $name )->toEqual( 'bye' );
		} );
	} );

	$name = 'hello';
	describe( 'after()', function() use( &$name ) {
		after( function() use ( &$name ) {
			$name = 'bye';
		} );

		it( 'is run after all tests in a suite', function() use( &$name ) {
			expect( $name )->toEqual( 'hello' );
		} );
	} );

	describe( 'after() (continued)', function() use( &$name ) {
		it( 'is run at the end of a suite', function() use ( &$name ) {
			expect( $name )->toEqual( 'bye' );
		} );
	} );
} );