├── .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$tag>";
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 | [](https://gitter.im/jacobstr/matura?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 
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 | 
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 |
--------------------------------------------------------------------------------