├── .gitignore ├── support ├── fixtures │ ├── exclude │ │ ├── test_exclude.php │ │ └── test_include.php │ ├── fake_folders │ │ ├── test_fake_test.php │ │ ├── test_another_fake_test.php │ │ ├── subfolder │ │ │ └── test_sub_folder_test.php │ │ └── not_a_test.txt │ └── tests │ │ ├── test_dynamically_generated_test.php │ │ └── test_failing_and_skipping_test.php └── lib │ ├── User.php │ ├── Spy.php │ ├── Group.php │ └── Util.php ├── docs ├── matura_in_action.gif └── sample_shell_output.png ├── .travis.yml ├── lib ├── Blocks │ ├── Methods │ │ ├── ExpectMethod.php │ │ ├── HookMethod.php │ │ ├── AfterHook.php │ │ ├── BeforeHook.php │ │ ├── AfterAllHook.php │ │ ├── Method.php │ │ ├── BeforeAllHook.php │ │ └── TestMethod.php │ ├── README.md │ ├── Suite.php │ ├── Describe.php │ └── Block.php ├── Events │ ├── Listener.php │ ├── Emitter.php │ └── Event.php ├── Core │ ├── Environment.php │ ├── ErrorHandler.php │ ├── ResultComponent.php │ ├── Context.php │ ├── InvocationContext.php │ ├── Result.php │ ├── ResultSet.php │ └── Builder.php ├── Filters │ ├── Defaults.php │ └── FilePathIterator.php ├── Exceptions │ ├── SkippedException.php │ ├── IncompleteException.php │ ├── AssertionException.php │ ├── Exception.php │ └── Error.php ├── Console │ ├── Commands │ │ ├── ExportDSL.php │ │ └── Test.php │ └── Output │ │ └── Printer.php ├── Runners │ ├── Runner.php │ ├── TestRunner.php │ └── SuiteRunner.php ├── Matura.php └── functions.php ├── test ├── performance │ └── test_stress.php ├── functional │ ├── test_context.php │ ├── test_model.php │ └── test_ordering.php └── integration │ └── test_test_runner.php ├── bin └── mat ├── composer.json ├── CHANGELOG.md ├── LICENSE ├── examples └── test_simple.php ├── README.md └── composer.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | -------------------------------------------------------------------------------- /support/fixtures/exclude/test_exclude.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /support/fixtures/exclude/test_include.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /support/fixtures/fake_folders/test_fake_test.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /support/fixtures/fake_folders/test_another_fake_test.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /support/fixtures/fake_folders/subfolder/test_sub_folder_test.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /support/fixtures/fake_folders/not_a_test.txt: -------------------------------------------------------------------------------- 1 | This should not be picked up by the TestRunner. 2 | -------------------------------------------------------------------------------- /docs/matura_in_action.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacobstr/matura/HEAD/docs/matura_in_action.gif -------------------------------------------------------------------------------- /docs/sample_shell_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacobstr/matura/HEAD/docs/sample_shell_output.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.4 4 | install: 5 | - composer install 6 | script: composer test 7 | -------------------------------------------------------------------------------- /lib/Blocks/Methods/ExpectMethod.php: -------------------------------------------------------------------------------- 1 | name = $name; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/Exceptions/SkippedException.php: -------------------------------------------------------------------------------- 1 | 2, 7 | 'tests' => 2, 8 | 'befores' => 2, 9 | 'before_alls' => 2, 10 | 'afters' => 2, 11 | 'after_alls' => 2, 12 | )); 13 | -------------------------------------------------------------------------------- /support/lib/Spy.php: -------------------------------------------------------------------------------- 1 | invocations[] = $name; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/Blocks/README.md: -------------------------------------------------------------------------------- 1 | # Blocks 2 | Classes that model the Object graph of our test suite. In the most abstract sense 3 | every Blocks is a node in the tree-graph representing our test suite. 4 | 5 | TestMethods, created via `it`, must be leaf nodes. 6 | 7 | The most common concrete subclasses are Describe and TestMethod. 8 | -------------------------------------------------------------------------------- /support/lib/Group.php: -------------------------------------------------------------------------------- 1 | name = $name; 13 | static::$groups[] = $this; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/Blocks/Methods/Method.php: -------------------------------------------------------------------------------- 1 | invoke(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/Exceptions/AssertionException.php: -------------------------------------------------------------------------------- 1 | 5, 'tests' => 15, 'befores' => 5)); 7 | }); 8 | 9 | describe('Shallow', function ($ctx) use (&$gensuite) { 10 | Util::gensuite(array('depth' => 1, 'tests' => 1000, 'befores' => 5)); 11 | }); 12 | -------------------------------------------------------------------------------- /bin/mat: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add(new TestCommand); 12 | $application->add(new ExportDSLCommand); 13 | $application->run(); 14 | -------------------------------------------------------------------------------- /lib/Exceptions/Exception.php: -------------------------------------------------------------------------------- 1 | getPrevious()) { 14 | return $previous->getTrace(); 15 | } else { 16 | return $this->getTrace(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/Events/Event.php: -------------------------------------------------------------------------------- 1 | name = $name; 10 | $this->context = array_merge($this->context, $context); 11 | } 12 | 13 | public function getName() 14 | { 15 | return $this->name; 16 | } 17 | 18 | public function getContext() 19 | { 20 | return $this->context; 21 | } 22 | 23 | public function __get($name) 24 | { 25 | return $this->context[$name]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/Console/Commands/ExportDSL.php: -------------------------------------------------------------------------------- 1 | setName('export-dsl') 15 | ->setDescription('Regenerates the DSL workaround file.'); 16 | } 17 | 18 | protected function execute(InputInterface $input, OutputInterface $output) 19 | { 20 | Matura::exportDSL(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jacobstr/matura", 3 | "description": "A mocha/rspec inspired testing framework for php.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "jacobstr", 8 | "email": "jacobstr@gmail.com" 9 | } 10 | ], 11 | "bin" : [ "bin/mat" ], 12 | "require": { 13 | "jacobstr/esperance": "~0.1", 14 | "symfony/console": "~2" 15 | }, 16 | "require-dev": { 17 | "mockery/mockery": "dev-master", 18 | "mover-io/belt": "~1.0" 19 | }, 20 | "autoload" : { 21 | "psr-4": { 22 | "Matura\\": "lib", 23 | "Matura\\Test\\": "support/lib" 24 | } 25 | }, 26 | "scripts": { 27 | "test": "bin/mat test test" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/Blocks/Methods/BeforeAllHook.php: -------------------------------------------------------------------------------- 1 | invoked) { 13 | return $this->result; 14 | } else { 15 | $this->result = $this->invokeWithin($this->fn, array($this->createContext())); 16 | $this->invoked = true; 17 | return $this->result; 18 | } 19 | } 20 | 21 | public function createContext() 22 | { 23 | if($this->context) { 24 | return $this->context; 25 | } else { 26 | return parent::createContext(); 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ##### 0.2.0 - November 7 2014 2 | 3 | * Modified file filters to work on the path's basename. 4 | * The default inclusion filter has been changed to require files that start with 5 | `test_` 6 | * Added a filename `exclude` filter. 7 | * Renamed `filter` to `include` for symmetry with `exclude`. 8 | * Restructured test folder to establish a better default folder structure. 9 | One should be able to point `bin/mat test` at their test folder and not 10 | encounter other cruft like fixtures. I had considered making the exlusion 11 | process operate on a default folder set but I'm not ready to dictate that yet. 12 | * Putting the DSL methods in the `Matura\Tests` namespace. I was never comfortable 13 | with the global methods - especially because their exceedingly collision prone 14 | names. 15 | -------------------------------------------------------------------------------- /lib/Blocks/Suite.php: -------------------------------------------------------------------------------- 1 | invoke(); 20 | foreach($block->describes() as $describe) { 21 | $describe->invokeWithin($builder_for($describe)); 22 | } 23 | }; 24 | }; 25 | 26 | return $this->invokeWithin($builder_for($this)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/Core/ErrorHandler.php: -------------------------------------------------------------------------------- 1 | error_reporting(), 9 | 'error_class' => '\Matura\Exceptions\Error' 10 | ); 11 | 12 | $final_options = array_merge($default_options, $options); 13 | 14 | $this->error_reporting = $final_options['error_reporting']; 15 | $this->error_class = $final_options['error_class']; 16 | } 17 | 18 | public function handleError($errno, $errstr, $errfile, $errline) 19 | { 20 | if ($errno & $this->error_reporting === 0) { 21 | return false; 22 | } 23 | 24 | $error_class = $this->error_class; 25 | 26 | throw new $error_class($errno, $errstr, $errfile, $errline, debug_backtrace()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/Core/ResultComponent.php: -------------------------------------------------------------------------------- 1 | basename_include = $basename_include; 23 | $this->basename_exclude = $basename_exclude; 24 | } 25 | 26 | public function accept() 27 | { 28 | $file_info = $this->getInnerIterator()->current(); 29 | return preg_match($this->basename_include, $file_info->getBaseName()) 30 | && !preg_match($this->basename_exclude, $file_info->getBaseName()); 31 | 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jacob Straszynski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /lib/Blocks/Describe.php: -------------------------------------------------------------------------------- 1 | path() == $path) { 36 | return $this; 37 | } 38 | 39 | foreach ($this->tests() as $test) { 40 | if ($test->path() == $path) { 41 | return $test; 42 | } 43 | } 44 | foreach ($this->describes() as $block) { 45 | $found = $block->find($path); 46 | if ($found !== null) { 47 | return $found; 48 | } 49 | } 50 | 51 | return null; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /lib/Blocks/Methods/TestMethod.php: -------------------------------------------------------------------------------- 1 | traversePost(function ($block) use (&$befores) { 14 | $befores = array_merge($befores, $block->befores()); 15 | }); 16 | return $befores; 17 | } 18 | 19 | public function collectOrderedAfters() 20 | { 21 | $afters = array(); 22 | $this->traversePre(function ($block) use (&$afters) { 23 | $afters = array_merge($afters, $block->afters()); 24 | }); 25 | return $afters; 26 | } 27 | 28 | public function aroundEach($fn) 29 | { 30 | foreach(array_merge( 31 | $this->collectOrderedBefores(), 32 | array($this), 33 | $this->collectOrderedAfters() 34 | ) as $block) { 35 | $fn($block); 36 | } 37 | } 38 | 39 | public function invoke() 40 | { 41 | if ($this->hasSkippedAncestors()) { 42 | return $this->invokeWithin( 43 | function() { throw New SkippedException(); }, 44 | array($this->createContext()) 45 | ); 46 | } else { 47 | return $this->invokeWithin($this->fn, array($this->createContext())); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/Runners/Runner.php: -------------------------------------------------------------------------------- 1 | listeners[] = $listener; 26 | } 27 | 28 | public function emit($name, $arguments = array()) 29 | { 30 | $event = new Event($name, $arguments); 31 | foreach ($this->listeners as $listener) { 32 | $this->invokeEventHandler($event, $listener); 33 | } 34 | } 35 | 36 | protected function invokeEventHandler(Event $event, Listener $listener) 37 | { 38 | $parts = array_map('ucfirst', array_filter(preg_split('/_|\./', $event->name))); 39 | $name = 'on'.implode($parts); 40 | 41 | if (is_callable(array($listener, $name))) { 42 | return call_user_func(array($listener, $name), $event); 43 | } else { 44 | return call_user_func(array($listener, 'onMaturaEvent'), $event); 45 | } 46 | } 47 | 48 | public function getResultSet() 49 | { 50 | return $this->result_set; 51 | } 52 | 53 | /** 54 | * @return ResultSet 55 | */ 56 | abstract public function run(); 57 | } 58 | -------------------------------------------------------------------------------- /lib/Exceptions/Error.php: -------------------------------------------------------------------------------- 1 | 'E_ERROR', 13 | /* 2 */ E_WARNING => 'E_WARNING', 14 | /* 4 */ E_PARSE => 'E_PARSE', 15 | /* 8 */ E_NOTICE => 'E_NOTICE', 16 | /* 16 */ E_CORE_ERROR => 'E_CORE_ERROR', 17 | /* 32 */ E_CORE_WARNING => 'E_CORE_WARNING', 18 | /* 64 */ E_COMPILE_ERROR => 'E_COMPILE_ERROR', 19 | /* 128 */ E_COMPILE_WARNING => 'E_COMPILE_WARNING', 20 | /* 256 */ E_USER_ERROR => 'E_USER_ERROR', 21 | /* 512 */ E_USER_WARNING => 'E_USER_WARNIng', 22 | /* 1024 */ E_USER_NOTICE => 'E_USER_NOTICE', 23 | /* 2048 */ E_STRICT => 'E_STRICT', 24 | /* 4096 */ E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR', 25 | /* 8192 */ E_DEPRECATED => 'E_DEPRECATED', 26 | /* 16384 */ E_USER_DEPRECATED => 'E_USER_DEPRECATED' 27 | ); 28 | 29 | protected $errno; 30 | protected $errstr; 31 | protected $errfile; 32 | protected $errline; 33 | protected $backtrace; 34 | 35 | public function __construct($errno, $errstr, $errfile, $errline, $backtrace) 36 | { 37 | $this->errno = $errno; 38 | $this->errstr = $errstr; 39 | $this->errfile = $errfile; 40 | $this->errline = $errline; 41 | $this->backtrace = $backtrace; 42 | 43 | $this->message = $this->errstr . ' via '.$errfile.':'.$errline; 44 | } 45 | 46 | public function getCategory() 47 | { 48 | return 'Error '.static::$error_names[$this->errno]; 49 | } 50 | 51 | public function originalTrace() 52 | { 53 | return $this->backtrace; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/test_simple.php: -------------------------------------------------------------------------------- 1 | first_name = 'bob'; 12 | $bob->group = $admins; 13 | 14 | $ctx->bob = $bob; 15 | $ctx->admins = $admins; 16 | }); 17 | 18 | it('should set the bob user', function ($ctx) { 19 | $ctx->sibling_value = 10; 20 | expect($ctx->bob)->to->be->a('Matura\Test\User'); 21 | }); 22 | 23 | it('should not inherit a sibling\'s context modifications', function ($ctx) { 24 | expect($ctx->sibling_value)->to->be(null); 25 | }); 26 | 27 | it('should set the admins group', function ($ctx) { 28 | expect($ctx->admins)->to->be->a('Matura\Test\Group'); 29 | }); 30 | 31 | it('should skip this test when invoked', function ($ctx) { 32 | skip(); 33 | }); 34 | 35 | xit('should skip this test when constructed', function ($ctx) { 36 | }); 37 | 38 | // This test is expected to fail. 39 | it('should be strict about undefined variables', function ($ctx) { 40 | $arr = array(0); 41 | $result = $arr[0] + $arr[1]; 42 | }); 43 | 44 | // Nested blocks help organize tests and allow progressive augmentation of 45 | // test context. 46 | describe('Inner Block with Before All and Context Clobbering', function ($ctx) { 47 | before_all(function ($ctx) { 48 | // Do something costly like purge and re-seed a database. 49 | $ctx->purged_database = true; 50 | }); 51 | 52 | before(function ($ctx) { 53 | $ctx->admins = new Group('modified_admins'); 54 | }); 55 | 56 | it('should inherit context from outer before blocks', function ($ctx) { 57 | expect($ctx->bob)->to->be->a('Matura\Test\User'); 58 | }); 59 | 60 | it('should shadow context variables from outer contexts if assigned', function ($ctx) { 61 | expect($ctx->admins->name)->to->eql('modified_admins'); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /support/fixtures/tests/test_failing_and_skipping_test.php: -------------------------------------------------------------------------------- 1 | first_name = 'bob'; 13 | $bob->group = $admins; 14 | 15 | $ctx->bob = $bob; 16 | $ctx->admins = $admins; 17 | }); 18 | 19 | it('should set the bob user', function ($ctx) { 20 | $ctx->sibling_value = 10; 21 | expect($ctx->bob)->to->be->a('Matura\Test\User'); 22 | }); 23 | 24 | it('should not inherit a sibling\'s context modifications', function ($ctx) { 25 | expect($ctx->sibling_value)->to->be(null); 26 | }); 27 | 28 | it('should set the admins group', function ($ctx) { 29 | expect($ctx->admins)->to->be->a('Matura\Test\Group'); 30 | }); 31 | 32 | it('should skip this test when invoked', function ($ctx) { 33 | skip(); 34 | }); 35 | 36 | it('should be strict about undefined variables', function ($ctx) { 37 | $arr = array(0); 38 | $result = $arr[0] + $arr[1]; 39 | }); 40 | 41 | // Nested blocks help organize tests and allow progressive augmentation of 42 | // test context. 43 | describe('Inner Block with Before All and Context Clobbering', function ($ctx) { 44 | before_all(function ($ctx) { 45 | // Do something costly like purge and re-seed a database. 46 | $ctx->purged_database = true; 47 | }); 48 | 49 | before(function ($ctx) { 50 | $ctx->admins = new Group('modified_admins'); 51 | }); 52 | 53 | it('should inherit context from outer before blocks', function ($ctx) { 54 | expect($ctx->bob)->to->be->a('Matura\Test\User'); 55 | }); 56 | 57 | it('should shadow context variables from outer contexts if assigned', function ($ctx) { 58 | expect($ctx->admins->name)->to->eql('modified_admins'); 59 | }); 60 | }); 61 | 62 | xdescribe('Skipped Block', function ($ctx) { 63 | it('should skip me because my block has been marked skipped', function ($ctx) { 64 | throw new Exception('I should not be invoked'); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /support/lib/Util.php: -------------------------------------------------------------------------------- 1 | 0, 9 | 'before_alls' => 0, 10 | 'afters' => 0, 11 | 'after_alls' => 0, 12 | 'tests' => 1, 13 | 'depth' => 0, 14 | 'describes' => array('L', 'R'), 15 | 'callbacks' => array( 16 | 'it' => function ($ctx) { 17 | expect(true)->to->eql(true); 18 | }, 19 | 'before' => function ($ctx) { 20 | $ctx->value = 3; 21 | }, 22 | 'before_all' => function ($ctx) { 23 | $ctx->value = 5; 24 | }, 25 | 'after' => function ($ctx) { 26 | $ctx->value = 7; 27 | }, 28 | 'after_all' => function ($ctx) { 29 | $ctx->value = 11; 30 | } 31 | ) 32 | ), $config); 33 | 34 | if ($config['depth'] == 0) { 35 | return; 36 | } 37 | 38 | foreach($config['describes'] as $side) { 39 | describe("Level {$side}{$current_depth}", function ($ctx) use ( 40 | $config, 41 | $current_depth 42 | ) 43 | { 44 | for ($i = 1; $i <= $config['tests']; $i++) { 45 | it("nested $i", $config['callbacks']['it']); 46 | } 47 | 48 | for ($i = 1; $i <= $config['befores']; $i++) { 49 | before($config['callbacks']['before']); 50 | } 51 | 52 | for ($i = 1; $i <= $config['before_alls']; $i++) { 53 | before_all($config['callbacks']['before_all']); 54 | } 55 | 56 | for ($i = 1; $i <= $config['after_alls']; $i++) { 57 | after_all($config['callbacks']['after_all']); 58 | } 59 | 60 | for ($i = 1; $i <= $config['afters']; $i++) { 61 | after($config['callbacks']['after']); 62 | } 63 | 64 | $config['depth']--; 65 | 66 | Util::gensuite($config, $current_depth + 1); 67 | }); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/functional/test_context.php: -------------------------------------------------------------------------------- 1 | before_scalar = 5; 10 | $ctx->empty_array = array(); 11 | $ctx->false = false; 12 | $ctx->user = new User('bob'); 13 | $ctx->func = function ($value) { 14 | return $value; 15 | }; 16 | }); 17 | 18 | before_all(function ($ctx) { 19 | $ctx->once_before_scalar = 10; 20 | $ctx->group = new Group('admins'); 21 | }); 22 | 23 | it('should return null for an undefined value', function ($ctx) { 24 | expect($ctx->never_set)->to->be(null); 25 | }); 26 | 27 | it('should allow and preserve setting empty arrays', function ($ctx) { 28 | expect($ctx->empty_array)->to->be(array()); 29 | }); 30 | 31 | it('should allow and preserve setting false', function ($ctx) { 32 | expect($ctx->false)->to->be(false); 33 | }); 34 | 35 | it('should have a user', function ($ctx) { 36 | expect($ctx->user)->to->be->a('\Matura\Test\User'); 37 | expect($ctx->user->name)->to->eql('bob'); 38 | }); 39 | 40 | it('should have a group', function ($ctx) { 41 | expect($ctx->group)->to->be->a('\Matura\Test\Group'); 42 | expect($ctx->group->name)->to->eql('admins'); 43 | }); 44 | 45 | it('should have a scalar from the before hook', function ($ctx) { 46 | expect($ctx->before_scalar)->to->be(5); 47 | }); 48 | 49 | it('should have a scalar from the once before hook', function ($ctx) { 50 | expect($ctx->once_before_scalar)->to->be(10); 51 | }); 52 | 53 | it('should invoke methods', function ($ctx) { 54 | expect($ctx->func(5))->to->eql(5); 55 | }); 56 | 57 | describe('Nested, Undefined Values', function ($ctx) { 58 | it('should return null for an undefined value when nested deeper', function ($ctx) { 59 | expect($ctx->another_never_set)->to->be(null); 60 | }); 61 | }); 62 | 63 | describe('Sibling-Of `Isolation` Block', function ($ctx) { 64 | before_all(function ($ctx) { 65 | $ctx->once_before_scalar = 15; 66 | }); 67 | 68 | before(function ($ctx) { 69 | $ctx->before_scalar = 10; 70 | $ctx->group = new Group('staff'); 71 | }); 72 | 73 | it("should have the clobbered value of `once_before_scalar`", function ($ctx) { 74 | expect($ctx->once_before_scalar)->to->be(15); 75 | }); 76 | 77 | it("should have the clobbered value of `group`", function ($ctx) { 78 | expect($ctx->group->name)->to->be('staff'); 79 | }); 80 | }); 81 | 82 | describe('Isolation', function ($ctx) { 83 | it("should have the parent `once_before_scalar` and not a sibling's", function ($ctx) { 84 | expect($ctx->once_before_scalar)->to->be(10); 85 | }); 86 | 87 | it("should have the parent `before_scalar` and not a sibling's", function ($ctx) { 88 | expect($ctx->before_scalar)->to->be(5); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /lib/Runners/TestRunner.php: -------------------------------------------------------------------------------- 1 | Defaults::MATCH_TEST, 35 | 'exclude' => Defaults::MATCH_NONE, 36 | 'grep' => Defaults::MATCH_ALL 37 | ); 38 | 39 | /** @var The directory or folder containing our test file(s). */ 40 | protected $path; 41 | 42 | public function __construct($path, $options = array()) 43 | { 44 | $this->path = $path; 45 | $this->options = array_merge($this->options, $options); 46 | $this->result_set = new ResultSet(); 47 | } 48 | 49 | /** 50 | * Recursively obtains all test files under `$this->path` and returns 51 | * the filtered result after applying our filtering regex. 52 | * 53 | * @return Iterator 54 | */ 55 | public function collectFiles() 56 | { 57 | if (is_dir($this->path)) { 58 | $directory = new RecursiveDirectoryIterator($this->path, FilesystemIterator::SKIP_DOTS); 59 | $iterator = new RecursiveIteratorIterator($directory); 60 | return new FilePathIterator($iterator, $this->options['include'], $this->options['exclude']); 61 | } else { 62 | return new ArrayIterator(array(new SplFileInfo($this->path))); 63 | } 64 | } 65 | 66 | /** 67 | * Bootstraps parts of our test enviornment and iteratively invokes each 68 | * file. 69 | * 70 | * @return ResultSet 71 | */ 72 | public function run() 73 | { 74 | $tests = $this->collectFiles(); 75 | 76 | $this->emit('test_run.start'); 77 | 78 | foreach ($tests as $test_file) { 79 | $suite = new Suite( 80 | new InvocationContext(), 81 | function () use ($test_file) { 82 | require $test_file; 83 | }, 84 | $test_file->getPathName() 85 | ); 86 | 87 | $suite->build(); 88 | 89 | $suite_result = new ResultSet(); 90 | $suite_runner = new SuiteRunner($suite, $suite_result, array( 91 | 'grep' => $this->options['grep'] 92 | )); 93 | $this->result_set->addResult($suite_result); 94 | 95 | // Forward my listeners. 96 | foreach ($this->listeners as $listener) { 97 | $suite_runner->addListener($listener); 98 | } 99 | 100 | $suite_runner->run(); 101 | } 102 | 103 | $this->emit('test_run.complete', array('result_set' => $this->result_set)); 104 | 105 | return $this->result_set; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/Core/Context.php: -------------------------------------------------------------------------------- 1 | block = $block; 42 | } 43 | 44 | public function __get($name) 45 | { 46 | if (isset($this->context[$name])) { 47 | return $this->context[$name]; 48 | } 49 | 50 | foreach (array_reverse($this->block->getContextChain()) as $context) { 51 | if ($context->getImmediate($name) !== null) { 52 | // Cache the value. 53 | $this->context[$name] = $context->getImmediate($name); 54 | return $this->context[$name]; 55 | } 56 | } 57 | 58 | return null; 59 | } 60 | 61 | public function __call($name, $arguments) 62 | { 63 | if (isset($this->context[$name])) { 64 | return call_user_func_array($this->context[$name], $arguments); 65 | } 66 | 67 | foreach (array_reverse($this->block->getContextChain()) as $context) { 68 | if ($context->getImmediate($name) !== null) { 69 | // Cache the value. 70 | $this->context[$name] = $context->getImmediate($name); 71 | return call_user_func_array($this->context[$name], $arguments); 72 | } 73 | } 74 | 75 | throw new \Exception("Method $name does not exist."); 76 | } 77 | 78 | public function getImmediate($key) 79 | { 80 | return array_key_exists($key, $this->context) ? $this->context[$key] : null; 81 | } 82 | /** 83 | * Sets a value. Always on myself and never on member of the context chain. 84 | */ 85 | public function __set($name, $value) 86 | { 87 | $this->context[$name] = $value; 88 | } 89 | 90 | public function getIterator() 91 | { 92 | return new ArrayIterator($this->context); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/Matura.php: -------------------------------------------------------------------------------- 1 | closest('\Matura\Blocks\Suite'); 36 | } 37 | 38 | public function closestDescribe() 39 | { 40 | return $this->closest('\Matura\Blocks\Describe'); 41 | } 42 | 43 | public function closestTest() 44 | { 45 | return $this->closest('\Matura\Blocks\Methods\TestMethod'); 46 | } 47 | 48 | public function closestBlock() 49 | { 50 | return $this->closest('\Matura\Blocks\Block'); 51 | } 52 | 53 | public function closest($name) 54 | { 55 | foreach (array_reverse($this->stack) as $block) { 56 | if (is_a($block, $name)) { 57 | return $block; 58 | } 59 | } 60 | 61 | return null; 62 | } 63 | 64 | public function invoke(Block $block) 65 | { 66 | $this->total_invocations++; 67 | $args = array_slice(func_get_args(), 1); 68 | $this->stack[] = $block; 69 | $result = call_user_func_array(array($block,'invoke'), $args); 70 | array_pop($this->stack); 71 | 72 | return $result; 73 | } 74 | 75 | public function push(Block $block) 76 | { 77 | $this->stack[] = $block; 78 | } 79 | 80 | public function pop() 81 | { 82 | array_pop($this->stack); 83 | } 84 | 85 | public function activeBlock() 86 | { 87 | return end($this->stack) ?: null; 88 | } 89 | 90 | public function activate() 91 | { 92 | static::$active_invocation_context = $this; 93 | static::$contexts[] = $this; 94 | } 95 | 96 | public function deactivate() 97 | { 98 | array_pop(static::$contexts); 99 | static::$active_invocation_context = end(static::$contexts); 100 | } 101 | 102 | public static function getActive() 103 | { 104 | return static::$active_invocation_context; 105 | } 106 | 107 | /** 108 | * Obtains the current active block and asserts that it is a given type. Used 109 | * to enforce block nested rules for the DSL. 110 | */ 111 | public static function getAndAssertActiveBlock($type) 112 | { 113 | $active_block = static::getActive(); 114 | $current = get_class($active_block->activeBlock()); 115 | if ( !is_a($active_block->activeBlock(), $type)) { 116 | throw new Exception("Improperly nested block. Expected a $type, got a $current"); 117 | } 118 | return $active_block; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /test/functional/test_model.php: -------------------------------------------------------------------------------- 1 | suite = suite('Suite', function () { 15 | describe('Fixture', function ($ctx) { 16 | it('TestMethod', function ($ctx) { 17 | }); 18 | 19 | before(function ($ctx) { 20 | 21 | }); 22 | 23 | before_all(function ($ctx) { 24 | 25 | }); 26 | }); 27 | }); 28 | }); 29 | 30 | describe('Suite', function ($ctx) { 31 | before(function ($ctx) { 32 | $ctx->describe = $ctx->suite->find('Suite:Fixture'); 33 | }); 34 | 35 | it('should be a Suite Block', function ($ctx) { 36 | expect($ctx->suite)->to->be->an('Matura\Blocks\Suite'); 37 | }); 38 | 39 | it('should have a name', function ($ctx) { 40 | expect($ctx->suite->getName())->to->eql('Suite'); 41 | }); 42 | 43 | it('should have a path', function ($ctx) { 44 | expect($ctx->suite->path())->to->eql('Suite'); 45 | }); 46 | 47 | it('should not have a parent Suite block', function ($ctx) { 48 | expect($ctx->suite->parentBlock())->to->eql(null); 49 | }); 50 | }); 51 | 52 | describe('Describe', function ($ctx) { 53 | before(function ($ctx) { 54 | $ctx->describe = $ctx->suite->find('Suite:Fixture'); 55 | }); 56 | 57 | it('should be a Describe Block', function ($ctx) { 58 | expect($ctx->describe)->to->be->a('Matura\Blocks\Describe'); 59 | }); 60 | 61 | it('should have the correct parent Block', function ($ctx) { 62 | expect($ctx->describe->parentBlock())->to->be($ctx->suite); 63 | }); 64 | }); 65 | 66 | describe('TestMethod', function ($ctx) { 67 | before(function ($ctx) { 68 | $ctx->test = $ctx->suite->find('Suite:Fixture:TestMethod'); 69 | }); 70 | 71 | it('should be a TestMethod', function ($ctx) { 72 | expect($ctx->test)->to->be->a('Matura\Blocks\Methods\TestMethod'); 73 | }); 74 | 75 | it('should have the correct parent Block', function ($ctx) { 76 | expect($ctx->test->parentBlock())->to->be->a('Matura\Blocks\Describe'); 77 | }); 78 | }); 79 | 80 | describe('BeforeHook', function ($ctx) { 81 | before(function ($ctx) { 82 | $ctx->describe = $ctx->suite->find('Suite:Fixture'); 83 | }); 84 | 85 | it('should have 1 BeforeHook', function ($ctx) { 86 | $befores = $ctx->describe->befores(); 87 | expect($befores)->to->have->length(1); 88 | expect($befores[0])->to->be->a('Matura\Blocks\Methods\BeforeHook'); 89 | }); 90 | }); 91 | 92 | describe('BeforeAllHook', function ($ctx) { 93 | before(function ($ctx) { 94 | $ctx->describe = $ctx->suite->find('Suite:Fixture'); 95 | }); 96 | 97 | it('should have 1 BeforeAllHook', function ($ctx) { 98 | $before_alls = $ctx->describe->beforeAlls(); 99 | expect($before_alls)->to->have->length(1); 100 | expect($before_alls[0])->to->be->a('Matura\Blocks\Methods\BeforeAllHook'); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /lib/Core/Result.php: -------------------------------------------------------------------------------- 1 | owning_block = $owning_block; 38 | $this->invoked_block = $invoked_block; 39 | $this->status = $status; 40 | $this->returned = $returned; 41 | } 42 | 43 | public function getBlock() 44 | { 45 | return $this->owning_block; 46 | } 47 | 48 | /** 49 | * E.g. a before method failure will be owned by it's triggering test. The 50 | * invoked block will still be the before method. 51 | */ 52 | public function getInvokedBlock() 53 | { 54 | return $this->invoked_block; 55 | } 56 | 57 | public function getStatus() 58 | { 59 | return $this->status; 60 | } 61 | 62 | public function getStatusString() 63 | { 64 | switch($this->status) { 65 | case Result::SUCCESS: 66 | return 'success'; 67 | case Result::FAILURE: 68 | return 'failure'; 69 | case Result::SKIPPED: 70 | return 'skipped'; 71 | case Result::INCOMPLETE: 72 | return 'incomplete'; 73 | default: 74 | return null; 75 | } 76 | } 77 | 78 | public function getReturned() 79 | { 80 | return $this->returned; 81 | } 82 | 83 | public function getException() 84 | { 85 | if ($this->returned instanceof \Exception) { 86 | return $this->returned; 87 | } else { 88 | return null; 89 | } 90 | } 91 | 92 | public function isTestMethod() 93 | { 94 | return $this->invoked_block && ($this->invoked_block instanceof TestMethod); 95 | } 96 | 97 | public function totalTests() 98 | { 99 | return $this->isTestMethod() ? 1 : 0; 100 | } 101 | 102 | public function totalAssertions() 103 | { 104 | return $this->invoked_block->getAssertionCount(); 105 | } 106 | 107 | public function totalFailures() 108 | { 109 | return $this->isFailure() ? 1 : 0; 110 | } 111 | 112 | public function totalIncomplete() 113 | { 114 | return $this->isIncomplete() ? 1 : 0; 115 | } 116 | 117 | public function totalSuccesses() 118 | { 119 | return $this->isSuccessful() ? 1 : 0; 120 | } 121 | 122 | public function totalSkipped() 123 | { 124 | return $this->isSkipped() ? 1 : 0; 125 | } 126 | 127 | public function isSuccessful() 128 | { 129 | return $this->status == static::SUCCESS; 130 | } 131 | 132 | public function isFailure() 133 | { 134 | return $this->status == static::FAILURE; 135 | } 136 | 137 | public function isSkipped() 138 | { 139 | return $this->status == static::SKIPPED; 140 | } 141 | 142 | public function isIncomplete() 143 | { 144 | return $this->invoked_block->getAssertionCount() == 0; 145 | } 146 | 147 | public function getFailures() 148 | { 149 | if ($this->isFailure()) { 150 | return array($this); 151 | } else { 152 | return array(); 153 | } 154 | } 155 | 156 | public function getWithFilter($fn) { 157 | if($fn($this)) { 158 | return array($this); 159 | } else { 160 | return array(); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /lib/Core/ResultSet.php: -------------------------------------------------------------------------------- 1 | results[] = $result; 24 | $this->total_tests += $result->totalTests(); 25 | } 26 | 27 | public function getIterator() 28 | { 29 | return new ArrayIterator($this->results); 30 | } 31 | 32 | public function totalAssertions() 33 | { 34 | $sum = 0; 35 | foreach ($this as $result) { 36 | $sum += $result->totalAssertions(); 37 | } 38 | return $sum; 39 | } 40 | 41 | public function totalFailures() 42 | { 43 | return count($this->getWithFilter(function ($result) { 44 | $invoked = $result->getInvokedBlock(); 45 | return $invoked instanceof TestMethod && $result->isFailure(); 46 | })); 47 | } 48 | 49 | public function totalSkipped() 50 | { 51 | return count($this->getWithFilter(function ($result) { 52 | $invoked = $result->getInvokedBlock(); 53 | return $invoked instanceof TestMethod && $result->isSkipped(); 54 | })); 55 | } 56 | 57 | public function totalSuccesses() 58 | { 59 | return count($this->getWithFilter(function ($result) { 60 | $invoked = $result->getInvokedBlock(); 61 | return $invoked instanceof TestMethod && $result->isSuccessful(); 62 | })); 63 | } 64 | 65 | public function totalTests() 66 | { 67 | $sum = 0; 68 | foreach ($this->results as $result) { 69 | $sum += $result->totalTests(); 70 | } 71 | 72 | return $sum; 73 | } 74 | 75 | public function currentTestIndex() 76 | { 77 | return $this->total_tests; 78 | } 79 | 80 | public function isSuccessful() 81 | { 82 | return count($this->getWithFilter(function ($result) { 83 | return $result->isFailure(); 84 | })) == 0; 85 | } 86 | 87 | public function isFailure() 88 | { 89 | return ! $this->isSuccessful(); 90 | } 91 | 92 | public function isSkipped() 93 | { 94 | return $this->totalSkipped() > 0; 95 | } 96 | 97 | public function getFailures() 98 | { 99 | $failures = array(); 100 | foreach ($this->results as $result) { 101 | $failures = array_merge($failures, $result->getFailures()); 102 | } 103 | return $failures; 104 | } 105 | 106 | public function getWithFilter($fn) 107 | { 108 | $collection = array(); 109 | foreach ($this->results as $result) { 110 | $collection = array_merge($collection, $result->getWithFilter($fn)); 111 | } 112 | return $collection; 113 | } 114 | 115 | public function getExceptions() 116 | { 117 | $exceptions = array(); 118 | foreach ($this->results as $result) { 119 | $exceptions = array_merge($exceptions, $result->getExceptions()); 120 | } 121 | return $exceptions; 122 | } 123 | 124 | public function getStatus() 125 | { 126 | if($this->isFailure()) { 127 | return Result::FAILURE; 128 | } else if($this->isSkipped()) { 129 | return Result::SKIPPED; 130 | } else if($this->isSuccessful()) { // isSuccess seems more correct. 131 | return Result::SUCCESS; 132 | } else { 133 | return Result::INCOMPLETE; 134 | } 135 | } 136 | 137 | public function getStatusString() 138 | { 139 | switch($this->getStatus()) { 140 | case Result::SUCCESS: 141 | return 'success'; 142 | case Result::FAILURE: 143 | return 'failure'; 144 | case Result::SKIPPED: 145 | return 'skipped'; 146 | case Result::INCOMPLETE: 147 | return 'incomplete'; 148 | default: 149 | return null; 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /lib/Console/Commands/Test.php: -------------------------------------------------------------------------------- 1 | 7 26 | ); 27 | 28 | protected function configure() 29 | { 30 | $this 31 | ->setName('test') 32 | ->setDescription('Run tests') 33 | ->addArgument( 34 | 'path', 35 | InputArgument::REQUIRED, 36 | 'The path to the file or directory to test.' 37 | ) 38 | ->addOption( 39 | 'grep', 40 | 'g', 41 | InputOption::VALUE_REQUIRED, 42 | 'Filter individual test cases by a description regexp.' 43 | ) 44 | ->addOption( 45 | 'include', 46 | 'i', 47 | InputOption::VALUE_REQUIRED, 48 | 'Include test files by a basename(filename) regexp.' 49 | ) 50 | ->addOption( 51 | 'exclude', 52 | 'x', 53 | InputOption::VALUE_REQUIRED, 54 | 'Exclude test files by a basename(filename) regexp.' 55 | ) 56 | ->addOption( 57 | 'trace_depth', 58 | 'd', 59 | InputOption::VALUE_REQUIRED, 60 | 'Set the depth of printed stack traces.' 61 | ); 62 | } 63 | 64 | protected function execute(InputInterface $input, OutputInterface $output) 65 | { 66 | 67 | $output->getFormatter()->setStyle( 68 | 'success', 69 | new OutputFormatterStyle('green') 70 | ); 71 | 72 | $output->getFormatter()->setStyle( 73 | 'failure', 74 | new OutputFormatterStyle('red') 75 | ); 76 | 77 | $output->getFormatter()->setStyle( 78 | 'info', 79 | new OutputFormatterStyle('blue') 80 | ); 81 | 82 | $output->getFormatter()->setStyle( 83 | 'skipped', 84 | new OutputFormatterStyle('magenta') 85 | ); 86 | 87 | $output->getFormatter()->setStyle( 88 | 'incomplete', 89 | new OutputFormatterStyle('magenta') 90 | ); 91 | 92 | $output->getFormatter()->setStyle( 93 | 'u', 94 | new OutputFormatterStyle(null, null, array('underscore')) 95 | ); 96 | 97 | $output->getFormatter()->setStyle( 98 | 'suite', 99 | new OutputFormatterStyle('yellow', null) 100 | ); 101 | 102 | $output->getFormatter()->setStyle( 103 | 'bold', 104 | new OutputFormatterStyle('blue', null) 105 | ); 106 | 107 | // Argument parsing 108 | // ################ 109 | $path = $input->getArgument('path'); 110 | 111 | $printer_options = array( 112 | 'trace_depth' => $input->getOption('trace_depth') ?: $this->defaults['trace_depth'] 113 | ); 114 | 115 | // Configure Output Modules 116 | // ######################## 117 | $printer = new Printer($printer_options); 118 | 119 | // Stash these for our event handler. 120 | $this->output = $output; 121 | $this->printer = $printer; 122 | 123 | $options = array(); 124 | 125 | if ($include = $input->getOption('include')) { 126 | $options['include'] = "/$include/"; 127 | } 128 | 129 | if ($exclude = $input->getOption('exclude')) { 130 | $options['exclude'] = "/$exclude/"; 131 | } 132 | 133 | if ($grep = $input->getOption('grep')) { 134 | $options['grep'] = "/$grep/i"; 135 | } 136 | 137 | // Bootstrap and Run 138 | // ################# 139 | 140 | $test_runner = new TestRunner($path, $options); 141 | 142 | $test_runner->addListener($this); 143 | 144 | Matura::init(); 145 | $code = $test_runner->run()->isSuccessful() ? 0 : 1; 146 | Matura::cleanup(); 147 | 148 | return $code; 149 | } 150 | 151 | public function onMaturaEvent(Event $event) 152 | { 153 | $output = $this->printer->renderEvent($event); 154 | 155 | if ($output !== null) { 156 | $this->output->writeln($output); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /test/functional/test_ordering.php: -------------------------------------------------------------------------------- 1 | $num) { 25 | foreach (range(1, $num) as $index) { 26 | $generate_test_block($block_method, $path, $index, $spy); 27 | } 28 | } 29 | 30 | if ($depth < $max_depth) { 31 | foreach (range(1, $describes) as $index) { 32 | describe("describe_$index", function () use (&$generate, $depth, $path, $index) { 33 | $generate($depth+1, array_merge($path, array("describe","$index"))); 34 | }); 35 | } 36 | } 37 | }; 38 | 39 | return suite('Root', function ($ctx) use ($generate) { 40 | $generate(1, array()); 41 | }); 42 | } 43 | 44 | describe('Ordering', function ($ctx) { 45 | it('should invoke 1 test and its hooks in the correct order.', function ($ctx) { 46 | $spy = new Spy(); 47 | 48 | $suite = gentree($spy, 1, 1, array( 49 | 'before' => 1, 50 | 'after' => 1, 51 | 'before_all' => 1, 52 | 'after_all' => 1, 53 | 'it' => 1 54 | )); 55 | 56 | $suite_runner = new SuiteRunner($suite, new ResultSet()); 57 | $suite_runner->run(); 58 | 59 | expect($spy->invocations)->to->eql(array( 60 | 'before_all.1', 61 | 'before.1', 62 | 'it.1', 63 | 'after.1', 64 | 'after_all.1' 65 | )); 66 | }); 67 | 68 | it('should invoke 2 tests and its hooks in the correct order.', function ($ctx) { 69 | $spy = new Spy(); 70 | 71 | $suite = gentree($spy, 1, 1, array( 72 | 'before' => 1, 73 | 'after' => 1, 74 | 'before_all' => 1, 75 | 'after_all' => 1, 76 | 'it' => 2 77 | )); 78 | 79 | $suite_runner = new SuiteRunner($suite, new ResultSet()); 80 | $suite_runner->run(); 81 | 82 | expect($spy->invocations)->to->eql(array( 83 | 'before_all.1', 84 | 85 | 'before.1', 86 | 'it.1', 87 | 'after.1', 88 | 89 | 'before.1', 90 | 'it.2', 91 | 'after.1', 92 | 93 | 'after_all.1' 94 | )); 95 | }); 96 | 97 | it('should invoke nested describes and their hooks in the correct order.', function ($ctx) { 98 | $spy = new Spy(); 99 | 100 | $suite = gentree($spy, 2, 2, array( 101 | 'before' => 1, 102 | 'after' => 1, 103 | 'before_all' => 1, 104 | 'after_all' => 1, 105 | 'it' => 2 106 | )); 107 | 108 | $suite_runner = new SuiteRunner($suite, new ResultSet()); 109 | $suite_runner->run(); 110 | 111 | expect($spy->invocations)->to->eql(array( 112 | // suite 113 | 'before_all.1', 114 | 115 | // test 116 | 'before.1', 117 | 'it.1', 118 | 'after.1', 119 | 120 | // test 121 | 'before.1', 122 | 'it.2', 123 | 'after.1', 124 | 125 | // First describe 126 | 'describe.1.before_all.1', 127 | 128 | // test 129 | 'before.1', // This might come as a surprise! 130 | 'describe.1.before.1', 131 | 'describe.1.it.1', 132 | 'describe.1.after.1', 133 | 'after.1', 134 | 135 | // test 136 | 'before.1', 137 | 'describe.1.before.1', 138 | 'describe.1.it.2', 139 | 'describe.1.after.1', 140 | 'after.1', 141 | 142 | 'describe.1.after_all.1', 143 | 144 | // Second describe 145 | 'describe.2.before_all.1', 146 | 147 | // test 148 | 'before.1', 149 | 'describe.2.before.1', 150 | 'describe.2.it.1', 151 | 'describe.2.after.1', 152 | 'after.1', 153 | 154 | // test 155 | 'before.1', 156 | 'describe.2.before.1', 157 | 'describe.2.it.2', 158 | 'describe.2.after.1', 159 | 'after.1', 160 | 161 | 'describe.2.after_all.1', 162 | 163 | 'after_all.1' 164 | )); 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /lib/Core/Builder.php: -------------------------------------------------------------------------------- 1 | closestTest()->addAssertion(); 48 | return $expect_method->invoke(); 49 | } 50 | 51 | /** 52 | * Marks the test skipped and throws a SkippedException. 53 | */ 54 | public static function skip($message = '') 55 | { 56 | throw new SkippedException($message); 57 | } 58 | 59 | /** 60 | * Begins a new 'describe' block. The callback $fn is invoked when the test 61 | * suite is run. 62 | */ 63 | public static function describe($name, $fn) 64 | { 65 | $next = new Describe(InvocationContext::getActive(), $fn, $name); 66 | $next->addToParent(); 67 | return $next; 68 | } 69 | 70 | /** 71 | * Begins a new test suite. The test suite instantiates a new invocation 72 | * context. 73 | */ 74 | public static function suite($name, $fn) 75 | { 76 | $suite = new Suite(new InvocationContext(), $fn, $name); 77 | $suite->build(); 78 | return $suite; 79 | } 80 | 81 | /** 82 | * Begins a new test case within the active block. 83 | */ 84 | public static function it($name, $fn) 85 | { 86 | $active_block = InvocationContext::getAndAssertActiveBlock('Matura\Blocks\Describe'); 87 | $test_method = new TestMethod($active_block, $fn, $name); 88 | $test_method->addToParent(); 89 | return $test_method; 90 | } 91 | 92 | /** 93 | * Adds a before callback to the active block. The active block should be 94 | * a describe block. 95 | */ 96 | public static function before($fn) 97 | { 98 | $test_method = new BeforeHook(InvocationContext::getActive(), $fn); 99 | $test_method->addToParent(); 100 | return $test_method; 101 | } 102 | 103 | /** 104 | * Adds a before_all callback to the active block. The active block should 105 | * generally be a describe block. 106 | */ 107 | public static function before_all($fn) 108 | { 109 | $test_method = new BeforeAllHook(InvocationContext::getActive(), $fn); 110 | $test_method->addToParent(); 111 | return $test_method; 112 | } 113 | 114 | public static function after($fn) 115 | { 116 | $test_method = new AfterHook(InvocationContext::getActive(), $fn); 117 | $test_method->addToParent(); 118 | return $test_method; 119 | } 120 | 121 | public static function after_all($fn) 122 | { 123 | $test_method = new AfterAllHook(InvocationContext::getActive(), $fn); 124 | $test_method->addToParent(); 125 | return $test_method; 126 | } 127 | 128 | /** 129 | * Takes care of our 'x' flag to skip any of the above methods. 130 | * 131 | * @return Block 132 | */ 133 | public static function __callStatic($name, $arguments) 134 | { 135 | list($name, $skip) = self::getNameAndSkipFlag($name); 136 | 137 | $block = call_user_func_array(array('static', $name), $arguments); 138 | 139 | if ($skip) { 140 | $block->skip('x-ed out'); 141 | } 142 | 143 | return $block; 144 | } 145 | 146 | // DSL Utility Methods 147 | // ################### 148 | 149 | /** 150 | * Used to detect skipped versions of methods. 151 | * 152 | * @example 153 | * >>$this->getNameAndSkipFlag('xit'); 154 | * array('it', true); 155 | * 156 | * >>$this->getNameAndSkipFlag('before_all'); 157 | * array('before_all', false); 158 | * 159 | * @return a 2-tuple of a method name and skip flag. 160 | */ 161 | protected static function getNameAndSkipFlag($name) 162 | { 163 | if ($name[0] == 'x') { 164 | return array(substr($name, 1), true); 165 | } else { 166 | return array(self::$method_map[$name], false); 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /lib/Console/Output/Printer.php: -------------------------------------------------------------------------------- 1 | depth() - 1; 14 | return ($level * $per_level); 15 | } 16 | 17 | function indent($lvl, $string, $per_level = 1) 18 | { 19 | if (empty($string)) { 20 | return ''; 21 | } else { 22 | $indent = str_repeat(" ", $lvl*1); 23 | return $indent.implode(explode("\n", $string), "\n".$indent); 24 | } 25 | } 26 | 27 | function tag($tag) 28 | { 29 | $rest = array_slice(func_get_args(), 1); 30 | $text = implode($rest); 31 | return "<$tag>$text"; 32 | } 33 | 34 | function pad_left($length, $string, $char = ' ') 35 | { 36 | return str_pad($string, $length, $char, STR_PAD_LEFT); 37 | } 38 | 39 | function pad_right($length, $string, $char = ' ') 40 | { 41 | return str_pad($string, $length, $char, STR_PAD_RIGHT); 42 | } 43 | 44 | /** 45 | * Contains test rendering methods. 46 | */ 47 | class Printer 48 | { 49 | protected $options = array( 50 | 'trace_depth' => 7, 51 | 'indent' => 3 52 | ); 53 | 54 | protected $test_count = 0; 55 | 56 | public function __construct($options = array()) 57 | { 58 | $this->options = array_merge($this->options, $options); 59 | } 60 | 61 | public function onTestComplete(Event $event) 62 | { 63 | $index = $this->test_count; 64 | 65 | // Via TestMethod 66 | $indent_width = ($event->test->depth() - 1) * 2; 67 | $name = $event->test->getName(); 68 | 69 | // Via Result 70 | $style = $event->result->getStatusString(); 71 | $status = $event->result->getStatus(); 72 | 73 | $icon_map = array( 74 | Result::SUCCESS => '✓', 75 | Result::FAILURE => '✘', 76 | Result::SKIPPED => '○', 77 | Result::INCOMPLETE => '○' 78 | ); 79 | 80 | $icon = $icon_map[$status]; 81 | 82 | $preamble = "$icon " . $index . ') '; 83 | $preamble = pad_right($indent_width, $preamble, " "); 84 | 85 | return tag($style, $preamble) . $name; 86 | } 87 | 88 | public function onTestRunComplete(Event $event) 89 | { 90 | $summary = array( 91 | tag("success", "Passed:"), 92 | "{$event->result_set->totalSuccesses()} of {$event->result_set->totalTests()}", 93 | tag("skipped", "Skipped:"), 94 | "{$event->result_set->totalSkipped()}", 95 | tag("failure", "Failed:"), 96 | "{$event->result_set->totalFailures()}", 97 | tag("bold", "Assertions:"), 98 | "{$event->result_set->totalAssertions()}" 99 | ); 100 | 101 | // The Passed / Failed / Skipped summary 102 | $summary = implode(" ", $summary); 103 | 104 | // Error formatting. 105 | $failures = $event->result_set->getFailures(); 106 | $failure_count = count($failures); 107 | 108 | $index = 0; 109 | $result = array(); 110 | foreach ($failures as $failure) { 111 | $index++; 112 | $result[] = tag("failure", pad_right(4, "$index )")."FAILURE: ". $failure->getBlock()->path()); 113 | $result[] = $this->formatFailure($index, $failure); 114 | $result[] = ""; 115 | } 116 | 117 | $result[] = $summary; 118 | 119 | return implode("\n", $result); 120 | } 121 | 122 | public function onTestStart(Event $event) 123 | { 124 | $this->test_count++; 125 | } 126 | 127 | public function onSuiteStart(Event $event) 128 | { 129 | $label = "Running: ".$event->suite->path(); 130 | 131 | return tag("suite", $label."\n"); 132 | } 133 | 134 | public function onSuiteComplete(Event $event) 135 | { 136 | return ""; 137 | } 138 | 139 | public function onDescribeStart(Event $event) 140 | { 141 | $name = $event->describe->getName(); 142 | $indent_width = ($event->describe->depth() - 1) * $this->options['indent']; 143 | return indent($indent_width, "$name ", $this->options['indent']); 144 | } 145 | 146 | public function onDescribeComplete(Event $event) 147 | { 148 | if ($event->result->isFailure()) { 149 | $name = $event->describe->getName(); 150 | $indent_width = ($event->describe->depth() - 1) * $this->options['indent']; 151 | return indent($indent_width, "Describe $name Failed", $this->options['indent']); 152 | } 153 | } 154 | 155 | // Formatting helpers 156 | // ################## 157 | 158 | protected function formatFailure($index, Result $failure) 159 | { 160 | $exception = $failure->getException(); 161 | $exception_category = $failure->getException()->getCategory(); 162 | 163 | return indent(4, implode( 164 | "\n", 165 | array( 166 | tag("info", $exception_category.': ') . $exception->getMessage(), 167 | tag("info", "Via:"), 168 | $this->formatTrace($exception) 169 | ) 170 | )); 171 | } 172 | 173 | public function formatTrace(MaturaException $exception) 174 | { 175 | $index = 0; 176 | $result = array(); 177 | $sliced_trace = array_slice($exception->originalTrace(), 0, $this->options['trace_depth']); 178 | 179 | foreach ($sliced_trace as $trace) { 180 | $index++; 181 | 182 | $parts = array(pad_right(4, $index.")")); 183 | 184 | if (isset($trace['file'])) { 185 | $parts[] = $trace['file'].':'.$trace['line']; 186 | } 187 | if (isset($trace['function'])) { 188 | $parts[] = $trace['function'].'()'; 189 | } 190 | $result[] = implode(' ', $parts); 191 | } 192 | 193 | return indent(3, implode("\n", $result)); 194 | } 195 | 196 | /** 197 | * Conducts our 'event_group.action' => 'onEventGroupAction delegation' 198 | */ 199 | public function renderEvent(Event $event) 200 | { 201 | $parts = array_map('ucfirst', array_filter(preg_split('/_|\./', $event->name))); 202 | $name = 'on'.implode($parts); 203 | 204 | if (is_callable(array($this, $name))) { 205 | return call_user_func(array($this, $name), $event); 206 | } else { 207 | return null; 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Matura 2 | ====== 3 | [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/jacobstr/matura?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ![Build Status](https://travis-ci.org/jacobstr/matura.svg) 4 | 5 | An RSpec / Mocha inspired testing tool for php. Requires 5.3+. 6 | 7 | --- 8 | 9 | ## Installation 10 | 11 | 1. `composer require "jacobstr/matura ~0.2"` 12 | 13 | ## Features 14 | 15 | - [Esperance](http://github.com/jacobstr/esperance) expectation library: `expect($result)->to->have->length(2)`. 16 | - A succinct DSL for defining tests. 17 | 18 | ```php 19 | describe('Matura', function ($ctx){ 20 | it('should make writing tests fun', function ($ctx) { 21 | expect($are_we_having_fun_yet)->to->eql(true); 22 | }); 23 | }); 24 | ``` 25 | 26 | - Heirarchical blocks to drill down from basic to complex assertions. 27 | 28 | ```php 29 | describe('User', function ($ctx) { 30 | describe('Authorization', function ($ctx){ 31 | describe('OAuth', function ($ctx) {}); 32 | }); 33 | }); 34 | ``` 35 | 36 | - `before`, `before_all`, `after`, `after_all`, hooks with a well-defined ordering. 37 | 38 | ```php 39 | describe('User Database', function ($ctx) { 40 | foreach(range(1,5) as $repetition) { 41 | it('should insert a user', function ($ctx){ 42 | $user = $ctx->db->findOne(array( 43 | 'username' => $ctx->username; 44 | )); 45 | expect($user)->to->have->length(1); 46 | }); 47 | 48 | it('should not accumulate users', function ($ctx){ 49 | $users = $ctx->db->find(); 50 | expect($users)->to->have->length(1); 51 | }); 52 | } 53 | 54 | // Executed once for each describe block. 55 | before_all(function ($ctx){ 56 | $ctx->test_id = uniqid(); 57 | $ctx->test_db = 'DB_'.$ctx->test_id; 58 | $ctx->db = new Database('localhost', $ctx->test_db); 59 | }); 60 | 61 | // Executed prior to each test (including descendants). 62 | before(function ($ctx){ 63 | $ctx->username = 'test_user'.$ctx->test_id.uniqid(); 64 | $ctx->db->insert(array('username' => $ctx->username)); 65 | }); 66 | 67 | // Executed after each test (including descendants); 68 | after(function ($ctx) { 69 | $ctx->db->delete(array('username' => $ctx->username)); 70 | }); 71 | 72 | // Executed once at the very end of this describe block. 73 | after_all(function ($ctx) { 74 | $ctx->db->drop($ctx->test_db); 75 | }); 76 | }); 77 | ``` 78 | 79 | ## Assertions 80 | 81 | As mentioned above, Matura uses Esperance as it's assertion library. Here 82 | are the core examples that you can use: 83 | 84 | ```php 85 | // Deep Equal(===) 86 | expect($object)->to->be($cloned_object); 87 | // Approximately Equal(==) 88 | expect($object)->to->eql(NULL); 89 | // Not Equal/Be/A 90 | expect($object)->to->not->be('Walrus') 91 | // Type Checking 92 | expect($object)->to->be->a('AwesomeObject'); 93 | expect($object)->to->be->an('AwesomeObject'); 94 | // Truthy 95 | expect($success)->to->be->ok(); 96 | // Invokability 97 | expect($example_func)->to->be->invokable(); 98 | // Range Checking 99 | expect($number)->to->be->within($start, $finish); 100 | // Above 101 | expect($number)->to->be->above($floor); 102 | // Below 103 | expect($number)->to->be->below($ceiling); 104 | // Empty 105 | expecy($an_array)->to->be->empty(); 106 | // Grep 107 | expect($greppable)->to->match($regexp); 108 | // Exception/Error Throwing 109 | expect($function)->to->throw($klass, $expected_msg); 110 | // Length 111 | expect('bob')->to->have->length(3); 112 | // Complex Assertions 113 | expect($complex)->to->not->be('Simple')->and->to->be('Complex'); 114 | ``` 115 | 116 | ## The CLI 117 | 118 | 119 | If you run, `bin/mat test test/examples`: 120 | 121 | ![Matura Shell Output](docs/matura_in_action.gif) 122 | 123 | And the documentation for the standard test command: 124 | 125 | Usage: 126 | test [-g|--grep="..."] [-i|--include="..."] [-x|--exclude="..."] [-d|--trace_depth="..."] path 127 | 128 | Arguments: 129 | path The path to the file or directory to test. 130 | 131 | Options: 132 | --grep (-g) Filter individual test cases by a description regexp. 133 | --include (-i) Include test files by a basename(filename) regexp. 134 | --exclude (-x) Exclude test files by a basename(filename) regexp. 135 | --trace_depth (-d) Set the depth of printed stack traces. 136 | --help (-h) Display this help message. 137 | --quiet (-q) Do not output any message. 138 | --verbose (-v|vv|vvv) Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug 139 | --version (-V) Display this application version. 140 | --ansi Force ANSI output. 141 | --no-ansi Disable ANSI output. 142 | --no-interaction (-n) Do not ask any interactive question. 143 | 144 | ## Filtering 145 | 146 | If you wish to filter specific tests within a suite/file, use `--grep`. Matura 147 | will be clever enough to run the requisite before/after hooks. 148 | 149 | ## Test Result Association 150 | 151 | When running before/after hooks Matura will associate any test failures with the currently running test, rather than treating it as a file-level failure. This is particularly useful with Mockery's `close` method, which triggers additional assertions: was a method called, was it called with the right parameters, and so on. 152 | 153 | For before_all / after_all hooks, the failure is associate with the surrounding describe block. 154 | 155 | ## Test Organization 156 | 157 | By default, Matura filters on the file's basename for anything beginning with `test_`. 158 | 159 | I'm using the structure below. I might formalize this some time: 160 | 161 | ├── test // Actual test cases. 162 | │   ├── functional // Functional tests. 163 | │   │   ├── test_context.php 164 | │   │   ├── test_model.php 165 | │   │   └── test_ordering.php 166 | │   ├── integration // More end-to-end flavored tests. 167 | │   │   └── test_test_runner.php 168 | │   └── performance // Tests that serve to benchmark your code. 169 | │   └── test_stress.php 170 | 171 | I keep my fixtures in a top-level `support` folder. I've seen these placed in the 172 | `test` folder but I chose to keep them tucked away to avoid iterating over them 173 | and making the default filter complex. 174 | 175 | ## Authoring Tests 176 | 177 | The one key piece is you want to place your tests in the `Matura\Tests` namespace 178 | if you're not using PHP 5.6. If you're using 5.6 you can import the functions in 179 | Matura\Tests into your namespace. 180 | 181 | ## Further Documentation 182 | 183 | I swear it's not a cop out! Examine the [tests folder](test/functional). 184 | 185 | * [In what order is everything run?](test/functional/test_ordering.php) 186 | * [What is that $ctx parameter?](test/functional/test_context.php) 187 | 188 | ## TODOS 189 | 190 | * There's currently nothing like PHPUnit's backupGlobals. Maybe there shouldn't 191 | be - I feel a better way to find inadvertent coupling / dependencies on global 192 | variables may be to add functionality that randomizes test ordering. 193 | * Backtraces annoyingly include calls internal to the framework. 194 | * I'm a fan of [contract tests](http://c2.com/cgi/wiki?AbstractTestCases). 195 | Class-based tests seem better suited to them, however, so I'm in need of 196 | inspiration wrt to the callback-driven dsl that matura uses. Maybe an invocable 197 | class... 198 | 199 | ## Thanks! 200 | 201 | * [Ben Zittlau](https://github.com/benzittlau) - PHPUsable which brings similar 202 | syntax to PHPUnit. Helped me realize this was a worthwhile diversion. 203 | -------------------------------------------------------------------------------- /lib/Runners/SuiteRunner.php: -------------------------------------------------------------------------------- 1 | suite = $suite; 41 | $this->result_set = $result_set; 42 | $this->options = array_merge(array( 43 | 'grep' => '//', 44 | 'except' => null, 45 | ), $options); 46 | } 47 | 48 | /** 49 | * Runs the Suite from start to finish. 50 | */ 51 | public function run() 52 | { 53 | $this->emit( 54 | 'suite.start', 55 | array( 56 | 'suite' => $this->suite, 57 | 'result_set' => $this->result_set 58 | ) 59 | ); 60 | 61 | $result = $this->captureAround(array($this, 'runGroup'), $this->suite, $this->suite); 62 | 63 | $this->emit( 64 | 'suite.complete', 65 | array( 66 | 'suite' => $this->suite, 67 | 'result' => $result, 68 | 'result_set' => $this->result_set 69 | ) 70 | ); 71 | 72 | if ($result->isFailure()) { 73 | $this->result_set->addResult($result); 74 | } 75 | } 76 | 77 | // Nested Blocks and Tests 78 | // ####################### 79 | 80 | protected function runDescribe(Describe $describe) 81 | { 82 | $this->emit( 83 | 'describe.start', 84 | array( 85 | 'describe' => $describe, 86 | 'result_set' => $this->result_set 87 | ) 88 | ); 89 | 90 | $result = $this->captureAround(array($this, 'runGroup'), $describe, $describe); 91 | 92 | $this->emit( 93 | 'describe.complete', 94 | array( 95 | 'describe' => $describe, 96 | 'result' => $result, 97 | 'result_set' => $this->result_set 98 | ) 99 | ); 100 | 101 | if ($result->isFailure()) { 102 | $this->result_set->addResult($result); 103 | } 104 | } 105 | 106 | protected function runGroup(Block $block) 107 | { 108 | // Check if the block should run by grepping it and descendants. 109 | if ($this->isFiltered($block)) { 110 | return; 111 | } 112 | 113 | foreach ($block->beforeAlls() as $before_all) { 114 | $before_all->invoke(); 115 | } 116 | 117 | foreach ($block->tests() as $test) { 118 | $this->runTest($test); 119 | } 120 | 121 | foreach ($block->describes() as $describe) { 122 | $this->runDescribe($describe); 123 | } 124 | 125 | foreach ($block->afterAlls() as $after_all) { 126 | $after_all->invoke(); 127 | } 128 | } 129 | 130 | protected function runTest(TestMethod $test) 131 | { 132 | // Check grep filter. 133 | if ($this->isFiltered($test)) { 134 | return; 135 | } 136 | 137 | $start_context = array( 138 | 'test' => $test, 139 | 'result_set' => $this->result_set 140 | ); 141 | 142 | $this->emit('test.start', $start_context); 143 | 144 | 145 | $suite_runner = $this; 146 | $test_result_set = new ResultSet(); 147 | $test->aroundEach(function ($block) use ($suite_runner, $test_result_set, $test) { 148 | // Skip once we fail. 149 | if($test_result_set->isFailure()) { 150 | $block->skip('Skipping due to earlier failures.'); 151 | } 152 | 153 | $result = $suite_runner->captureAround(array($block, 'invoke'), $test, $block); 154 | 155 | $test_result_set->addResult($result); 156 | }); 157 | 158 | $this->result_set->addResult($test_result_set); 159 | 160 | $complete_context = array( 161 | 'test' => $test, 162 | 'result' => $test_result_set, 163 | 'result_set' => $this->result_set 164 | ); 165 | 166 | $this->emit('test.complete', $complete_context); 167 | 168 | return $test_result_set; 169 | } 170 | 171 | /** 172 | * @param $owner The Block 'owns' the result of $fn(). E.g. a TestMethod owns 173 | * the results from all of it's before and after hooks. 174 | * 175 | * (before_all and after_all hooks are owned by their encompassing Describe) 176 | * 177 | * public because @bindshim 178 | * 179 | * @return Result 180 | */ 181 | public function captureAround($fn, Block $owner, Block $invoked) 182 | { 183 | try { 184 | $return_value = call_user_func($fn, $owner, $invoked); 185 | $status = Result::SUCCESS; 186 | } catch (EsperanceError $e) { 187 | $status = Result::FAILURE; 188 | $return_value = new AssertionException($e->getMessage(), $e->getCode(), $e); 189 | } catch (SkippedException $e) { 190 | $status = Result::SKIPPED; 191 | $return_value = $e; 192 | } catch (\Exception $e) { 193 | $status = Result::FAILURE; 194 | $return_value = new MaturaException($e->getMessage(), $e->getCode(), $e); 195 | } 196 | 197 | return new Result($owner, $invoked, $status, $return_value); 198 | } 199 | 200 | /** 201 | * Checks if the block or any of it's descendants match our grep filter or 202 | * do not match our except filter. 203 | * 204 | * Descendants are checked in order to retain a test even it's parent block 205 | * path does not match. 206 | */ 207 | protected function isFiltered(Block $block) 208 | { 209 | // Skip filtering on implicit Suite block. 210 | if ($block instanceof Suite) { 211 | return false; 212 | } 213 | 214 | $options = &$this->options; 215 | 216 | $isFiltered = function ($block) use (&$options) { 217 | $filtered = false; 218 | 219 | if ($options['grep']) { 220 | $filtered = $filtered || preg_match($options['grep'], $block->path(0)) === 0; 221 | } 222 | 223 | if ($options['except']) { 224 | $filtered = $filtered || preg_match($options['except'], $block->path(0)) === 1; 225 | } 226 | 227 | return $filtered; 228 | }; 229 | 230 | // Code smell. Consider moving this responsibility to the blocks. 231 | if ($block instanceof TestMethod) { 232 | return $isFiltered($block); 233 | } else { 234 | foreach ($block->tests() as $test) { 235 | if ($isFiltered($test) === false) { 236 | return false; 237 | } 238 | } 239 | 240 | foreach ($block->describes() as $describe) { 241 | if ($this->isFiltered($describe) === false) { 242 | return false; 243 | } 244 | } 245 | 246 | return true; 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /lib/Blocks/Block.php: -------------------------------------------------------------------------------- 1 | invocation_context = $invocation_context; 46 | $this->parent_block = $invocation_context->activeBlock(); 47 | $this->fn = $fn; 48 | $this->name = $name; 49 | } 50 | 51 | /** 52 | * Unless the Block has been skipped elsewhere, this marks the block as 53 | * skipped with the given message. 54 | * 55 | * @param string $message An optional skip message. 56 | * 57 | * @return Block $this 58 | */ 59 | public function skip($message = '') 60 | { 61 | if ($this->skipped !== true) { 62 | $this->skipped = true; 63 | $this->skipped_because = $message; 64 | } 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * Whether this Block has been marked for skipping. 71 | * 72 | * @return bool 73 | */ 74 | public function isSkipped() 75 | { 76 | return $this->skipped; 77 | } 78 | 79 | /** 80 | * Whether this Block or any of it's ancestors have been marked skipped. 81 | * 82 | * @return bool 83 | */ 84 | public function hasSkippedAncestors() 85 | { 86 | foreach($this->ancestors() as $ancestor) { 87 | if ($ancestor->isSkipped()) { 88 | return true; 89 | } 90 | } 91 | return false; 92 | } 93 | 94 | // Test Context Management 95 | // ####################### 96 | 97 | public function createContext() 98 | { 99 | return $this->context = new Context($this); 100 | } 101 | 102 | public function getContext() 103 | { 104 | return $this->context; 105 | } 106 | 107 | /** 108 | * Returns an aray of related contexts, in their intended call order. 109 | * 110 | * @see test_model.php for assertions against various scenarios in order to 111 | * grok the `official` behavior. 112 | * 113 | * @return Context[] 114 | */ 115 | public function getContextChain() 116 | { 117 | $block_chain = array(); 118 | 119 | // This should return all of our before hooks in the order they *should* 120 | // have been invoked. 121 | $this->traversePost(function ($block) use (&$block_chain) { 122 | // Ensure ordering - even if the test defininition interleaves 123 | // before_all with before DSL invocations, we traverse the context 124 | // according to the 'before_alls before befores' convention. 125 | $befores = array_merge($block->beforeAlls(), $block->befores()); 126 | $block_chain = array_merge($block_chain, $befores); 127 | }); 128 | 129 | return array_filter( 130 | array_map( 131 | function ($block) { 132 | return $block->getContext(); 133 | }, 134 | $block_chain 135 | ) 136 | ); 137 | } 138 | 139 | // Invocation Context Management 140 | // ############################# 141 | 142 | /** 143 | * Default external invocation method - calls the block originally passed into 144 | * the constructor along with a new context. 145 | */ 146 | public function invoke() 147 | { 148 | return $this->invokeWithin($this->fn, array($this->createContext())); 149 | } 150 | 151 | /** 152 | * Invokes $fn with $args while managing our internal invocation context 153 | * in order to ensure our view of the test DSL's call graph is accurate. 154 | */ 155 | public function invokeWithin($fn, $args = array()) 156 | { 157 | $this->invocation_context->activate(); 158 | 159 | $this->invocation_context->push($this); 160 | try { 161 | $result = call_user_func_array($fn, $args); 162 | $this->invocation_context->pop(); 163 | $this->invocation_context->deactivate(); 164 | return $result; 165 | } catch (\Exception $e) { 166 | $this->invocation_context->pop(); 167 | $this->invocation_context->deactivate(); 168 | throw $e; 169 | } 170 | } 171 | 172 | public function addAssertion() 173 | { 174 | $this->assertions++; 175 | } 176 | 177 | public function getAssertionCount() 178 | { 179 | return $this->assertions; 180 | } 181 | 182 | /** 183 | * With no arguments, returns the complete path to this block down from it's 184 | * root ancestor. 185 | * 186 | * @param int $offset Used to arary_slice the intermediate array before implosion. 187 | * @param int $length Used to array_slice the intermediate array before implosion. 188 | */ 189 | public function path($offset = null, $length = null) 190 | { 191 | $ancestors = array_map( 192 | function ($ancestor) { 193 | return $ancestor->getName(); 194 | }, 195 | $this->ancestors() 196 | ); 197 | 198 | $ancestors = array_slice(array_reverse($ancestors), $offset, $length); 199 | 200 | $res = implode(":", $ancestors); 201 | 202 | return $res; 203 | } 204 | 205 | public function getName() 206 | { 207 | return $this->name; 208 | } 209 | 210 | // Traversal 211 | // ######### 212 | 213 | public function depth() 214 | { 215 | $total = 0; 216 | $block = $this; 217 | 218 | while ($block->parentBlock()) { 219 | $block = $block->parentBlock(); 220 | $total++; 221 | } 222 | 223 | return $total; 224 | } 225 | 226 | public function ancestors() 227 | { 228 | $ancestors = array(); 229 | $block = $this; 230 | 231 | while ($block) { 232 | $ancestors[] = $block; 233 | $block = $block->parentBlock(); 234 | } 235 | 236 | return $ancestors; 237 | } 238 | 239 | public function parentBlock($parent_block = null) 240 | { 241 | if (func_num_args()) { 242 | $this->parent_block = $parent_block; 243 | return $this; 244 | } else { 245 | return $this->parent_block; 246 | } 247 | } 248 | 249 | public function addToParent() 250 | { 251 | if ($this->parent_block) { 252 | $this->parent_block->addChild($this); 253 | } 254 | } 255 | 256 | public function traversePost($fn) 257 | { 258 | if ($parent_block = $this->parentBlock()) { 259 | $parent_block->traversePost($fn); 260 | } 261 | 262 | $fn($this); 263 | } 264 | 265 | public function traversePre($fn) 266 | { 267 | $fn($this); 268 | 269 | if ($parent_block = $this->parentBlock()) { 270 | $parent_block->traversePre($fn); 271 | } 272 | } 273 | 274 | public function closest($class_name) 275 | { 276 | foreach ($this->ancestors() as $ancestor) { 277 | if (is_a($ancestor, $class_name)) { 278 | return $ancestor; 279 | } 280 | } 281 | return null; 282 | } 283 | 284 | // Traversing Upwards 285 | // ################## 286 | 287 | public function closestTest() 288 | { 289 | return $this->closest('Matura\Blocks\Methods\TestMethod'); 290 | } 291 | 292 | public function closestSuite() 293 | { 294 | return $this->closest('Matura\Blocks\Suite'); 295 | } 296 | 297 | // Retrieving and Filtering Child Blocks 298 | // ##################################### 299 | 300 | public function addChild(Block $block) 301 | { 302 | $type = get_class($block); 303 | if (!isset($this->children[$type])) { 304 | $this->children[$type] = array(); 305 | } 306 | $this->children[$type][] = $block; 307 | } 308 | 309 | public function children($of_type) 310 | { 311 | if (!isset($this->children[$of_type])) { 312 | $this->children[$of_type] = array(); 313 | } 314 | return $this->children[$of_type]; 315 | } 316 | 317 | public function tests() 318 | { 319 | return $this->children('Matura\Blocks\Methods\TestMethod'); 320 | } 321 | 322 | /** 323 | * @var Block[] This Method's nested blocks. 324 | */ 325 | public function describes() 326 | { 327 | return $this->children('Matura\Blocks\Describe'); 328 | } 329 | 330 | /** 331 | * @return HookMethod[] All of our current `after` hooks. 332 | */ 333 | public function afters() 334 | { 335 | return $this->children('Matura\Blocks\Methods\AfterHook'); 336 | } 337 | 338 | /** 339 | * @return HookMethod[] All of our current `before` hooks. 340 | */ 341 | public function befores() 342 | { 343 | return $this->children('Matura\Blocks\Methods\BeforeHook'); 344 | } 345 | 346 | /** 347 | * @return HookMethod[] All of our current `before_all` hooks. 348 | */ 349 | public function beforeAlls() 350 | { 351 | return $this->children('Matura\Blocks\Methods\BeforeAllHook'); 352 | } 353 | 354 | /** 355 | * @return HookMethod[] All of our current `after_all` hooks. 356 | */ 357 | public function afterAlls() 358 | { 359 | return $this->children('Matura\Blocks\Methods\AfterAllHook'); 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /test/integration/test_test_runner.php: -------------------------------------------------------------------------------- 1 | fixture_folder = __DIR__.'/../../support/fixtures/fake_folders/'; 29 | }); 30 | 31 | describe('Unfiltered', function ($ctx) { 32 | before(function ($ctx) { 33 | $ctx->runner = new TestRunner($ctx->fixture_folder); 34 | }); 35 | 36 | it('should include all *.php files if no filter is specified', function ($ctx) { 37 | $files = $ctx->runner->collectFiles(); 38 | expect(iterator_to_array($files))->to->have->length(3); 39 | }); 40 | }); 41 | 42 | describe('Filtered', function ($ctx) { 43 | before(function ($ctx) { 44 | $ctx->runner = new TestRunner($ctx->fixture_folder, array('include' => '/^test_fake/')); 45 | }); 46 | 47 | it('should only include files that start with `fake`.', function ($ctx) { 48 | $files = $ctx->runner->collectFiles(); 49 | expect(iterator_to_array($files))->to->have->length(1); 50 | }); 51 | }); 52 | }); 53 | 54 | describe('Exclusion', function ($ctx) { 55 | before(function ($ctx) { 56 | $ctx->fixture_folder = __DIR__.'/../../support/fixtures/exclude/'; 57 | $ctx->runner = new TestRunner($ctx->fixture_folder, array('exclude' => '/^test_exclude/')); 58 | }); 59 | 60 | it('should only exclude files that start with `test_exclude`.', function ($ctx) { 61 | $files = iterator_to_array($ctx->runner->collectFiles()); 62 | expect($files)->to->have->length(1); 63 | $file = array_pop($files); 64 | expect($file->getBaseName())->to->eql('test_include.php'); 65 | }); 66 | }); 67 | 68 | describe('Grepping', function ($ctx) { 69 | before(function ($ctx) { 70 | $ctx->fixture_folder = __DIR__.'/../../support/fixtures/tests/'; 71 | $ctx->test_file = $ctx->fixture_folder . '/test_dynamically_generated_test.php'; 72 | }); 73 | 74 | describe('Ungrepped', function ($ctx) { 75 | before(function ($ctx) { 76 | $ctx->runner = new TestRunner($ctx->test_file); 77 | }); 78 | 79 | it('should run the correct tests', function ($ctx) { 80 | $result = $ctx->runner->run(); 81 | // Level L1:nested 0 82 | // Level L1:nested 1 83 | // Level L1:Level L2:nested 0 84 | // Level L1:Level L2:nested 1 85 | // Level L1:Level R2:nested 0 86 | // Level L1:Level R2:nested 1 87 | // Level R1:nested 0 88 | // Level R1:nested 1 89 | // Level R1:Level L2:nested 0 90 | // Level R1:Level L2:nested 1 91 | // Level R1:Level R2:nested 0 92 | // Level R1:Level R2:nested 1 93 | expect($result->totalTests())->to->eql(12); 94 | }); 95 | }); 96 | 97 | describe('Grepped `Level L`', function ($ctx) { 98 | before(function ($ctx) { 99 | $ctx->runner = new TestRunner( 100 | $ctx->test_file, 101 | array('grep' => '/Level L1/') 102 | ); 103 | }); 104 | 105 | it('should run the correct tests', function ($ctx) { 106 | $result = $ctx->runner->run(); 107 | // Level L1:nested 0 108 | // Level L1:nested 1 109 | // Level L1:Level L2:nested 0 110 | // Level L1:Level L2:nested 1 111 | // Level L1:Level R2:nested 0 112 | // Level L1:Level R2:nested 1 113 | expect($result->totalTests())->to->eql(6); 114 | }); 115 | }); 116 | 117 | describe('Grepped `Level L1:Level R2`', function ($ctx) { 118 | before(function ($ctx) { 119 | $ctx->runner = new TestRunner( 120 | $ctx->test_file, 121 | array('grep' => '/Level L1:Level R2/') 122 | ); 123 | }); 124 | 125 | it('should run the correct tests', function ($ctx) { 126 | $result = $ctx->runner->run(); 127 | // Level L1:Level R2:nested 0 128 | // Level L1:Level R2:nested 1 129 | expect($result->totalTests())->to->eql(2); 130 | }); 131 | }); 132 | }); 133 | 134 | describe('Error Capture and Reporting', function ($ctx) { 135 | before(function ($ctx) { 136 | $ctx->spy = $spy = Mockery::mock()->shouldIgnoreMissing(); 137 | $ctx->listener = Mockery::mock('Matura\Events\Listener')->shouldIgnoreMissing(); 138 | $ctx->suite = suite('Fixture', function ($inner_ctx) use ($spy, $ctx) { 139 | $ctx->before_all = before_all(array($spy, 'before_all')); 140 | $ctx->after_all = after_all(array($spy, 'after_all')); 141 | $ctx->after = after(array($spy, 'after')); 142 | $ctx->before = before(array($spy, 'before')); 143 | $ctx->describe = describe('Inner', function ($inner_ctx) use ($spy, $ctx) { 144 | $ctx->inner_before_all = before_all(array($spy, 'inner_before_all')); 145 | $ctx->inner_after_all = after_all(array($spy, 'inner_after_all')); 146 | $ctx->inner_after = after(array($spy, 'inner_after')); 147 | $ctx->inner_before = before(array($spy, 'inner_before')); 148 | $ctx->test = it('should have a test case', array($spy,'it')); 149 | }); 150 | }); 151 | $ctx->suite_runner = new SuiteRunner($ctx->suite, new ResultSet()); 152 | $ctx->suite_runner->addListener($ctx->listener); 153 | }); 154 | 155 | describe('At the Suite Level', function ($ctx) { 156 | it('should capture before_all errors', function ($ctx) { 157 | $ctx->spy->shouldReceive('before_all')->once()->andThrow('\Exception'); 158 | $ctx->suite_runner->run(); 159 | $failures = $ctx->suite_runner->getResultSet()->getFailures(); 160 | expect($failures)->to->have->length(1); 161 | expect($failures[0]->getBlock())->to->be($ctx->suite); 162 | }); 163 | 164 | it('should capture after_all errors', function ($ctx) { 165 | $ctx->spy->shouldReceive('after_all')->once()->andThrow('\Exception'); 166 | $ctx->suite_runner->run(); 167 | $failures = $ctx->suite_runner->getResultSet()->getFailures(); 168 | expect($failures)->to->have->length(1); 169 | expect($failures[0]->getBlock())->to->be($ctx->suite); 170 | }); 171 | }); 172 | 173 | describe('At the Describe Level', function ($ctx) { 174 | it('should capture inner before_all errors', function ($ctx) { 175 | $ctx->spy->shouldReceive('inner_before_all')->once()->andThrow('\Exception'); 176 | $ctx->suite_runner->run(); 177 | $failures = $ctx->suite_runner->getResultSet()->getFailures(); 178 | expect($failures)->to->have->length(1); 179 | expect($failures[0]->getBlock())->to->be($ctx->describe); 180 | }); 181 | 182 | it('should capture inner after_all errors', function ($ctx) { 183 | $ctx->spy->shouldReceive('inner_after_all')->once()->andThrow('\Exception'); 184 | $ctx->suite_runner->run(); 185 | $failures = $ctx->suite_runner->getResultSet()->getFailures(); 186 | expect($failures)->to->have->length(1); 187 | expect($failures[0]->getBlock())->to->be($ctx->describe); 188 | }); 189 | }); 190 | 191 | describe('At the Test Level', function ($ctx) { 192 | it('should capture test before errors', function ($ctx) { 193 | $ctx->spy->shouldReceive('inner_before')->once()->andThrow('\Exception'); 194 | $ctx->suite_runner->run(); 195 | $failures = $ctx->suite_runner->getResultSet()->getFailures(); 196 | expect($failures)->to->have->length(1); 197 | expect($failures[0]->getBlock())->to->be($ctx->test); 198 | }); 199 | 200 | it('should capture test after errors', function ($ctx) { 201 | $ctx->spy->shouldReceive('inner_after')->once()->andThrow('\Exception'); 202 | $ctx->suite_runner->run(); 203 | $failures = $ctx->suite_runner->getResultSet()->getFailures(); 204 | expect($failures)->to->have->length(1); 205 | expect($failures[0]->getBlock())->to->be($ctx->test); 206 | }); 207 | }); 208 | 209 | describe('Within Listeners', function ($test) { 210 | it('should capture listener errors somewhere...', function ($ctx) { 211 | $ctx->listener->shouldReceive('onTestComplete')->once()->andThrow('\Exception'); 212 | $ctx->suite_runner->run(); 213 | $failures = $ctx->suite_runner->getResultSet()->getFailures(); 214 | expect($failures)->to->have->length(1); 215 | expect($failures[0]->getBlock())->to->be->a('Matura\Blocks\Block'); 216 | }); 217 | 218 | }); 219 | }); 220 | 221 | describe('End to End', function ($ctx) { 222 | before(function ($ctx) { 223 | $ctx->fixture_folder = __DIR__.'/../../support/fixtures/tests/'; 224 | $ctx->test_file = $ctx->fixture_folder . '/test_failing_and_skipping_test.php'; 225 | $ctx->runner = new TestRunner($ctx->test_file); 226 | $ctx->result = $ctx->runner->run(); 227 | }); 228 | 229 | it('should run all tests', function ($ctx) { 230 | expect($ctx->result->totalTests())->to->eql(8); 231 | }); 232 | 233 | it('should skip 2 tests', function ($ctx) { 234 | expect($ctx->result->totalSkipped())->to->eql(2); 235 | }); 236 | 237 | it('should fail 1 test', function ($ctx) { 238 | expect($ctx->result->totalFailures())->to->eql(1); 239 | }); 240 | 241 | it('will only count executed assertions', function ($ctx) { 242 | expect($ctx->result->totalAssertions())->to->eql(5); 243 | }); 244 | }); 245 | }); 246 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "b91e9d06bba8b995569a72fbe43a65d4", 8 | "packages": [ 9 | { 10 | "name": "evenement/evenement", 11 | "version": "v1.0.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/igorw/evenement.git", 15 | "reference": "fa966683e7df3e5dd5929d984a44abfbd6bafe8d" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/igorw/evenement/zipball/fa966683e7df3e5dd5929d984a44abfbd6bafe8d", 20 | "reference": "fa966683e7df3e5dd5929d984a44abfbd6bafe8d", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=5.3.0" 25 | }, 26 | "type": "library", 27 | "autoload": { 28 | "psr-0": { 29 | "Evenement": "src" 30 | } 31 | }, 32 | "notification-url": "https://packagist.org/downloads/", 33 | "license": [ 34 | "MIT" 35 | ], 36 | "authors": [ 37 | { 38 | "name": "Igor Wiedler", 39 | "email": "igor@wiedler.ch", 40 | "homepage": "http://wiedler.ch/igor/" 41 | } 42 | ], 43 | "description": "Événement is a very simple event dispatching library for PHP 5.3", 44 | "keywords": [ 45 | "event-dispatcher" 46 | ], 47 | "time": "2012-05-30 15:01:08" 48 | }, 49 | { 50 | "name": "jacobstr/dumpling", 51 | "version": "v0.0.3", 52 | "source": { 53 | "type": "git", 54 | "url": "https://github.com/jacobstr/dumpling.git", 55 | "reference": "b66da47438277f641cd570eb8e2079df0c07f2bb" 56 | }, 57 | "dist": { 58 | "type": "zip", 59 | "url": "https://api.github.com/repos/jacobstr/dumpling/zipball/b66da47438277f641cd570eb8e2079df0c07f2bb", 60 | "reference": "b66da47438277f641cd570eb8e2079df0c07f2bb", 61 | "shasum": "" 62 | }, 63 | "require-dev": { 64 | "phpunit/phpunit": "3.7.*" 65 | }, 66 | "type": "library", 67 | "autoload": { 68 | "psr-4": { 69 | "Dumpling\\": "src" 70 | } 71 | }, 72 | "notification-url": "https://packagist.org/downloads/", 73 | "license": [ 74 | "MIT" 75 | ], 76 | "authors": [ 77 | { 78 | "name": "jacobstr", 79 | "email": "jacobstr@gmail.com" 80 | } 81 | ], 82 | "description": "Like print_r, but depth limited and cyclic reference safe.", 83 | "time": "2014-05-10 03:22:52" 84 | }, 85 | { 86 | "name": "jacobstr/esperance", 87 | "version": "v0.1.2", 88 | "source": { 89 | "type": "git", 90 | "url": "https://github.com/jacobstr/esperance.git", 91 | "reference": "c60bba80423badebb0902a1998d8fce7bc1dc500" 92 | }, 93 | "dist": { 94 | "type": "zip", 95 | "url": "https://api.github.com/repos/jacobstr/esperance/zipball/c60bba80423badebb0902a1998d8fce7bc1dc500", 96 | "reference": "c60bba80423badebb0902a1998d8fce7bc1dc500", 97 | "shasum": "" 98 | }, 99 | "require": { 100 | "evenement/evenement": "~1.0", 101 | "jacobstr/dumpling": "~0.0.3", 102 | "php": ">=5.3.2" 103 | }, 104 | "type": "library", 105 | "autoload": { 106 | "psr-0": { 107 | "Esperance": "src" 108 | } 109 | }, 110 | "notification-url": "https://packagist.org/downloads/", 111 | "license": [ 112 | "MIT" 113 | ], 114 | "description": "BDD style assertion library for PHP.", 115 | "keywords": [ 116 | "BDD", 117 | "TDD", 118 | "assertion" 119 | ], 120 | "time": "2014-07-25 00:16:29" 121 | }, 122 | { 123 | "name": "symfony/console", 124 | "version": "v2.5.4", 125 | "target-dir": "Symfony/Component/Console", 126 | "source": { 127 | "type": "git", 128 | "url": "https://github.com/symfony/Console.git", 129 | "reference": "748beed2a1e73179c3f5154d33fe6ae100c1aeb1" 130 | }, 131 | "dist": { 132 | "type": "zip", 133 | "url": "https://api.github.com/repos/symfony/Console/zipball/748beed2a1e73179c3f5154d33fe6ae100c1aeb1", 134 | "reference": "748beed2a1e73179c3f5154d33fe6ae100c1aeb1", 135 | "shasum": "" 136 | }, 137 | "require": { 138 | "php": ">=5.3.3" 139 | }, 140 | "require-dev": { 141 | "psr/log": "~1.0", 142 | "symfony/event-dispatcher": "~2.1" 143 | }, 144 | "suggest": { 145 | "psr/log": "For using the console logger", 146 | "symfony/event-dispatcher": "" 147 | }, 148 | "type": "library", 149 | "extra": { 150 | "branch-alias": { 151 | "dev-master": "2.5-dev" 152 | } 153 | }, 154 | "autoload": { 155 | "psr-0": { 156 | "Symfony\\Component\\Console\\": "" 157 | } 158 | }, 159 | "notification-url": "https://packagist.org/downloads/", 160 | "license": [ 161 | "MIT" 162 | ], 163 | "authors": [ 164 | { 165 | "name": "Symfony Community", 166 | "homepage": "http://symfony.com/contributors" 167 | }, 168 | { 169 | "name": "Fabien Potencier", 170 | "email": "fabien@symfony.com" 171 | } 172 | ], 173 | "description": "Symfony Console Component", 174 | "homepage": "http://symfony.com", 175 | "time": "2014-08-14 16:10:54" 176 | } 177 | ], 178 | "packages-dev": [ 179 | { 180 | "name": "mockery/mockery", 181 | "version": "dev-master", 182 | "source": { 183 | "type": "git", 184 | "url": "https://github.com/padraic/mockery.git", 185 | "reference": "1b3b265a904cab6f28b72684d7d62abade836e25" 186 | }, 187 | "dist": { 188 | "type": "zip", 189 | "url": "https://api.github.com/repos/padraic/mockery/zipball/1b3b265a904cab6f28b72684d7d62abade836e25", 190 | "reference": "1b3b265a904cab6f28b72684d7d62abade836e25", 191 | "shasum": "" 192 | }, 193 | "require": { 194 | "lib-pcre": ">=7.0", 195 | "php": ">=5.3.2" 196 | }, 197 | "require-dev": { 198 | "hamcrest/hamcrest-php": "~1.1", 199 | "phpunit/phpunit": "~4.0", 200 | "satooshi/php-coveralls": "~0.7@dev" 201 | }, 202 | "type": "library", 203 | "extra": { 204 | "branch-alias": { 205 | "dev-master": "0.9.x-dev" 206 | } 207 | }, 208 | "autoload": { 209 | "psr-0": { 210 | "Mockery": "library/" 211 | } 212 | }, 213 | "notification-url": "https://packagist.org/downloads/", 214 | "license": [ 215 | "BSD-3-Clause" 216 | ], 217 | "authors": [ 218 | { 219 | "name": "Pádraic Brady", 220 | "email": "padraic.brady@gmail.com", 221 | "homepage": "http://blog.astrumfutura.com" 222 | }, 223 | { 224 | "name": "Dave Marshall", 225 | "email": "dave.marshall@atstsolutions.co.uk", 226 | "homepage": "http://davedevelopment.co.uk" 227 | } 228 | ], 229 | "description": "Mockery is a simple yet flexible PHP mock object framework for use in unit testing with PHPUnit, PHPSpec or any other testing framework. Its core goal is to offer a test double framework with a succint API capable of clearly defining all possible object operations and interactions using a human readable Domain Specific Language (DSL). Designed as a drop in alternative to PHPUnit's phpunit-mock-objects library, Mockery is easy to integrate with PHPUnit and can operate alongside phpunit-mock-objects without the World ending.", 230 | "homepage": "http://github.com/padraic/mockery", 231 | "keywords": [ 232 | "BDD", 233 | "TDD", 234 | "library", 235 | "mock", 236 | "mock objects", 237 | "mockery", 238 | "stub", 239 | "test", 240 | "test double", 241 | "testing" 242 | ], 243 | "time": "2014-07-23 18:59:52" 244 | }, 245 | { 246 | "name": "mover-io/belt", 247 | "version": "dev-master", 248 | "source": { 249 | "type": "git", 250 | "url": "https://github.com/mover-io/belt.git", 251 | "reference": "6706db677dcbf439cfe36687a0ca71b0fae20f7b" 252 | }, 253 | "dist": { 254 | "type": "zip", 255 | "url": "https://api.github.com/repos/mover-io/belt/zipball/6706db677dcbf439cfe36687a0ca71b0fae20f7b", 256 | "reference": "6706db677dcbf439cfe36687a0ca71b0fae20f7b", 257 | "shasum": "" 258 | }, 259 | "require-dev": { 260 | "mover-io/phpusable-phpunit": "dev-master" 261 | }, 262 | "type": "library", 263 | "autoload": { 264 | "psr-4": { 265 | "Belt\\": "lib/" 266 | } 267 | }, 268 | "notification-url": "https://packagist.org/downloads/", 269 | "license": [ 270 | "MIT" 271 | ], 272 | "authors": [ 273 | { 274 | "name": "Jacob Straszynski", 275 | "homepage": "https://github.com/jacobstr" 276 | } 277 | ], 278 | "description": "A utility belt", 279 | "time": "2014-09-19 19:15:26" 280 | } 281 | ], 282 | "aliases": [], 283 | "minimum-stability": "stable", 284 | "stability-flags": { 285 | "mockery/mockery": 20, 286 | "mover-io/belt": 20 287 | }, 288 | "prefer-stable": false, 289 | "platform": [], 290 | "platform-dev": [] 291 | } 292 | --------------------------------------------------------------------------------