jimdelois/context-specification

PHPUnit 的一个库,用于支持 BDD ContextSpecification 风格的测试模式

0.0.2 2015-03-16 00:08 UTC

This package is not auto-updated.

Last update: 2024-09-24 02:06:02 UTC


README

Build Status

上下文规格测试框架

上下文规格(CS)库是一个PHPUnit包装器,旨在推广使用CS风格的测试模式。这种方法为测试驱动开发人员提供了一种与业务术语描述的行为更接近的语法,从而产生了越来越受欢迎的“行为驱动设计(BDD)”范式。

与“传统”单元测试相比,上下文规格风格的BDD似乎没有明显差异。虽然最初似乎只有语法的语义差异,但“不同”方法的微妙排列在功能逻辑组织...以及测试中起着巨大的作用。

具体来说,这种区别体现在将功能描述为分为三个部分时...(1)起点;一个初始状态;一个“上下文”,其中将采取特定的行动。(2)动作本身;从初始状态到新状态的“状态转换”。(3)状态转换完成后的新、最终或结果状态。

通过一次只测试一个单次转换事件或方法,然后验证关于新状态的所有方面都足够期望,同时正确地提供了初始状态,可以假设状态转换本身正在正确运行。由于计算机程序是通过不断地从一个状态转换到另一个状态来运行的,我们可以通过测试每个转换并从所有已知的初始状态来确保库的正确运行。这样的行为/功能考虑可能会带来更大的代码覆盖率、更少的错误,并且在需求可以用类似方式表达时,可以更准确地建模系统。

确实,可以不使用任何这样的包装器以类似粒度和行为覆盖范围实践相同类型的BDD。然而,希望提供轻量级的库并建议遵循一些规则,开发人员的测试将更易于阅读,同时也能提供更自然的测试失败(和成功!)报告。

最后,有几种方法可以大幅度加强BDD范式,类似于其他语言中的某些框架。然而,这个库是专门为PHPUnit开发的,考虑到了工具。也就是说,这个库应该在所有PHPUnit扩展中正确运行(只要它们没有明确删除默认行为),无论开发者或IDE放置了任何自定义的TestRunners、Commands等。

安装

建议通过Composer依赖管理工具安装上下文规格库。否则,可以从Github在任何时候下载最新压缩文件,并使用任何PSR-4自动加载器来包含其中的文件。

Composer安装

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

"require" : {
	"jimdelois/context-specification": "0.1.*@dev" ,
}

运行 composer installcomposer update 后,您应该在 {vendordir}/jimdelois/context-specification/src/ContextSpecification/Framework/ 目录中拥有正确的库。

基本用法

根据需要,简单地扩展 ConcernStaticConcern 类,然后开始正常测试!

如果在测试全局或独立功能时无法注入、插入或跟踪任何状态,那么StaticConcern将足以作为这个功能的框架。在任何情况下,开发者都必须提供以下两个抽象方法的实现。

abstract protected function context( );
abstract protected function because( );

context方法用于建立起点或初始状态。接下来将调用because方法,其中SUT或独立功能将执行其唯一的测试状态更改。

如果存在需要初始状态的系统(SUT),其依赖项需要在注入之前具有状态,或者必须检查其结果内部状态,则开发者应该扩展Concern类,并在此内部实例化此系统。

protected function createSUT( );

通过从该方法返回对象或系统,它将自动在Concern类中作为$this->sut可用。应该在测试特定的Context对象上设置Context,调用状态更改(在because方法中),并在观察中提出断言。

无论是否存在SUT,都可以在标准的PHPUnit "测试"方法中做出此类观察。任何以"test"开头的方法都将工作,尽管一般建议使用内置于PHPUnit的@test注解,并根据传统的CS语言命名观察方法,例如should等。这将带来更好的报告效果,例如,--testdox报告将类似于常规CS报告。

例如,考虑MyTest.php作为

<?php
    use ContextSpecification\Framework\Concern;
    class When_attempting_to_do_something_awesome_given_a_specific_context extends Concern {
        protected function context( ) { ... } // Fill this in!
        protected function because( ) { ... } // Fill this in!

        /**
         * @test
         */
        public function should_really_be_awesome( ) {
            $awesome = true;
            $this->assertTrue( $awesome );
        }

        /**
         * @test
         */
        public function then_it_should_not_be_boring( ) {
            $boring = false;
            $this->assertFalse( $boring );
        }
    }
?>

然后,大多数报告将类似于传统的CS报告(注意其详尽性!)

$ ../vendor/bin/phpunit --testdox MyTest.php
PHPUnit 3.7.33 by Sebastian Bergmann.

When_attempting_to_do_something_awesome_given_a_specific_context
[x] should_really_be_awesome
[x] then_it_should_not_be_boring

此外,还提供了decontext方法,可用于可选地"撤销"任何上下文建立。该方法在每次观察后调用,平衡对context的调用。请注意,依赖此方法通过测试真正的"单元"功能可能是由于糟糕的测试方法或更糟的设计(全局变量?!)。根据我们正在测试的代码,我们可能无法避免这一点,因此此功能提供了在观察之间"重置"任何内容的机会。在"集成"测试(不是"单元"测试)中,这样做更为常见,并且可以说是"可接受"的。

最后,Context Specification库提供了一些额外的功能,这些功能在处理状态更改引发的异常时很有用。正如应该测试所有可能的正面场景上下文一样,开发人员还应该测试可能无效的状态上下文。通常,目标功能(在SUT中)将在此情况下引发异常。如果我们直接将无效的输入提供给because方法的状态更改

protected function because( ) {
	$this->sut->setInteger( 'This is a string but the method expects an INT!' );
}

... 实际调用because将导致异常在我们准备之前被触发,并且PHPUnit将立即失败。正确的解决方案不是让because方法为空并将状态更改移动到观察中!这样做使我们回到了"标准"的TDD实践,其中状态更改在断言同一方法中触发 - 这正是我们试图避免的。相反,考虑在because方法中捕获异常,然后稍后评估它。或者,尝试在lambda函数中包装状态更改,然后稍后调用它。虽然后者方法只是与"不建议"的方法有语义上的差异,但它是一个重要的区分,有助于保持我们的测试一致性和井然有序。

由于测试无效状态并确保正确抛出异常是一个非常常见的操作(或者应该是),因此上下文规范框架简化了测试开发者处理这些细节的工作量。只需调用$this->becauseWillThrowException();,关注点将被配置为自动将because方法的内容包装在lambda中,这意味着可以像往常一样定义状态变化。然后,有两个额外的函数可用于从观察中验证和断言生成的异常:releaseExceptioncaptureException

示例

以下是我们如何使用上下文规范库测试服务上的单个方法的几个(愚蠢的)示例。

<?php
	use ContextSpecification\Framework\Concern;
	use My\Library\AppService\MyAwesomeAppService;
	use My\Library\Domain\Awesomeness;
	use Phake;

	class When_loading_first_awesomeness_from_service_for_date extends Concern {

		protected $dao_awesomeness;
		protected $date_time;

		// Establish a context in which we'll be testing our functionality
		protected function context( ) {

			$this->date_time = new \DateTime( );

			$this->result_expected = new Awesomeness( 'Today will be AWESOME. Maybe.' );

			$dao_return_array = array(
				$this->result_expected ,
				new Awesomeness( 'Should not be seeing this message' ) ,
				new Awesomeness( 'Three is a charm' )
			);

			$this->dao_awesomeness = Phake::mock( 'My\Library\DAO\AwesomenessInterface' );
			Phake::when( $this->dao_awesomeness )->loadAllByDate( $this->date_time )->thenReturn( $dao_return_array );
		}

		// Setup a System-Under-Test
		protected function createSUT( ) {
			return new MyAwesomeAppService( $this->dao_awesomeness );
		}

		// Execute the functionality; the "state change."
		protected function because( ) {
			$this->result_actual = $this->sut->getFirstAvailableAwesomenessForDate( $this->date_time );
		}

		/**
		 * @test
		 */
		public function should_call_appropriate_method_on_awesomeness_dao( ) {
			Phake::verify( $this->dao_awesomeness )->loadAllByDate( $this->date_time );
			Phake::verifyNoFurtherInteraction( $this->dao_awesomeness );
		}

		/**
         * @test
         */
        public function should_return_correct_awesomeness_object( ) {
        	$this->assertEquals( $this->result_expected , $this->result_actual );
        }
	}
?>

现在,假设我们已经正确实现了MyAwesomeAppService对象上getFirstAvailableAwesomenessForDate( )方法的功能(当然是在编写测试之后!),那么我们应该看到以下内容

$ ../vendor/bin/phpunit --testdox
PHPUnit 3.7.33 by Sebastian Bergmann.

When_loading_first_awesomeness_from_service_for_date
[x] should_call_appropriate_method_on_awesomeness_dao
[x] should_return_correct_awesomeness_object

这很好!让我们迭代getFirstAvailableAwesomenessForDate方法的功能,确保输入不是\DateTime对象时抛出异常。

<?php
	use ContextSpecification\Framework\Concern;
	use My\Library\AppService\MyAwesomeAppService;
	use My\Library\Domain\Awesomeness;
	use Phake;

	class When_loading_first_awesomeness_from_service_for_non_date_input extends Concern {

		protected $dao_awesomeness;
		protected $date_time_invalid;

		// Establish a context in which we'll be testing our functionality
		protected function context( ) {

			// This causes the library to trap the contents of "because" into a lambda for later execution.
			$this->becauseWillThrowException( );

			$this->date_time_invalid = 'THIS_IS_A_STRING';
			$this->dao_awesomeness = Phake::mock( 'My\Library\DAO\AwesomenessInterface' );

		}

		// Setup a System-Under-Test
		protected function createSUT( ) {
			return new MyAwesomeAppService( $this->dao_awesomeness );
		}

		// Execute the functionality; the "state change."
		protected function because( ) {
			$this->sut->getFirstAvailableAwesomenessForDate( $this->date_time_invalid );
		}

		/**
		 * @test
		 */
		public function should_raise_invalid_argument_exception( ) {
			$this->setExpectedException( '\InvalidArgumentException' );
			$this->releaseException( );
		}
	}
?>

请注意,即使调用会导致测试失败的错误,框架仍然允许我们保持一个干净、整洁的because方法。

或者,可以调用captureException,这样就会避免抛出异常,然后可以从$this->exception中获取它,以便进行审查和使用。也可能在抛出异常后从内部对$this->sut的状态进行断言。

理想情况下,在运行整个套件时,将产生以下结果

$ ../vendor/bin/phpunit --testdox
PHPUnit 3.7.33 by Sebastian Bergmann.

When_loading_first_awesomeness_from_service_for_date
[x] should_call_appropriate_method_on_awesomeness_dao
[x] should_return_correct_awesomeness_object

When_loading_first_awesomeness_from_service_for_non_date_input
[x] should_raise_invalid_argument_exception

这两个简单的示例有一些明显的缺陷,它们已经迫切需要一个共享的父级关注点。然而,这里意图的使用可能是比正确的测试设计或架构更重要的。

问题

  • 最好是将可配置的“测试方法前缀”扩展到PHPUnit基础框架中,就像它们目前允许在文件级别调整“测试后缀”一样。通过为方法前缀打开可配置性,我们可以避免使用@test注释任何“特别命名的CS测试方法”。扩展PHPUnit_Framework_TestSuite以标记此类方法是微不足道的,并且可以通过一组自定义运行器和/或命令将整个套件替换为基本套件 - 但正如这种方法可以用于将额外功能引入PHPUnit一样,这是其他开发人员和IDEs常用的方法...这意味着如果这个特定的库坚持使用自定义运行器和命令,那么在其他IDE或PHPUnit的其他扩展中几乎肯定不会工作。

    在将此类配置接受到PHPUnit核心框架并最终传播到其他工具之前,我们可能不得不使用test*作为测试方法,或者像建议的那样使用@test进行注释。

  • PHPUnit的setUp()方法是在每个测试方法之前运行的一个实例方法,或者在这个案例中是“观察”。然而,setUpBeforeClass()是一个静态方法,它为每个类运行一次。从理论上讲,Context规范测试用例中的上下文应该只为每个类设置一次(这可以说是传统测试和CS测试之间最大的区别之一 - 状态变化与断言和上下文建立如此清晰地分离,以至于实际上根本不需要为每个观察进行tearDown和setUp)。这种方法在其他CS框架中很常见,但区别在于它们基框架的“一次/类”等效物不是静态方法。

    这并不是说在库内无法完成,但从一个静态方法正确设置我们的关注点可能不是一件简单的事情。同样的情况也适用于 tearDowntearDownAfterClass。因此,重要的是要认识到,在每次观察方法前后,都会建立一个上下文,并解除上下文。 从实际的角度来看,这意味着任何包含多个观察和复杂或潜在设置的深度集成测试都可能存在性能影响。因此(以及其他原因),通常最好将所有“单元”测试套件与“集成”测试套件在逻辑上分开。

下一步

  • 理想情况下,可以添加额外的、类级别的注解,如 @concern {关注点名称},这将允许测试者将多个关注点分组,以便于在 多个不同的文件 中进行报告。当然,必须使用自定义报告器来适应这一点,尽管中间可能可以有一个监听器来执行一些技巧...

    类似地,通过某种类型的 @when 注解也可以在下一级别进行分组。

  • 研究解决上述关于每次观察建立上下文与每次关注点建立上下文“问题”的方法。

问题/反馈

Jim DeLois - %%PHPDOC_AUTHOR_EMAIL%%