Compare commits

...

211 Commits

Author SHA1 Message Date
58cff003d8 release: v1.19.0 2021-09-20 19:30:58 +01:00
ae997e6eee No need for --ignore-platform-req=php 2021-09-20 19:28:43 +01:00
e6c7d68def Adds php-81 support 2021-09-20 19:26:47 +01:00
2f0cd7a4e3 Adds Fathom Analytics as sponsor 2021-09-15 10:03:05 +01:00
447af55e7c Merge pull request #391 from gabbanaesteban/master
Add `toHaveProperties`
2021-09-03 09:52:39 +01:00
253e9d10c8 Fix types 2021-09-03 04:44:10 -04:00
536ce1eca0 Update snapshots 2021-09-03 04:40:48 -04:00
4331b2aaf6 Add toHaveProperties 2021-09-03 04:26:57 -04:00
8d99cacc95 Update README.md 2021-09-02 23:36:02 +01:00
3c38facc8a Update README.md 2021-09-02 23:35:51 +01:00
ed389d35d0 Adds Auth0 as sponsor 2021-09-02 23:35:40 +01:00
60c0636523 release: v1.18.0 2021-08-30 00:05:26 +01:00
16b6f96b47 Merge pull request #389 from dansysanalyst/toHaveLength-mblen
Use mb_strlen instead of grapheme_strlen
2021-08-29 12:18:38 +01:00
042f2ec3f3 Use mb_strlen instead of grapheme_strlen
Due to inconsistent behave, mb_strlen will be used.
2021-08-29 12:55:37 +02:00
851ce36010 Merge pull request #386 from dansysanalyst/expectation-toHaveLength
Adds toHaveLength() Expectation & Tests
2021-08-29 00:45:39 +01:00
4f386894bd Revert "Use toHaveCount"
This reverts commit 2289adade2.
2021-08-28 18:14:55 +02:00
2289adade2 Use toHaveCount
changes to toHaveCount
2021-08-28 17:54:29 +02:00
29e21e3814 Merge pull request #385 from dansysanalyst/snapshot-contrib
Update snapshots
2021-08-28 16:47:25 +01:00
8367af22e7 Adds toHaveLength() Expectation & Tests
Adds toHaveLength() Expectation and its tests. toHaveLength checks if the given value has the informed length. Works with strings, array, object and collections.
2021-08-28 17:02:09 +02:00
e3d678dc04 Update snapshots
Adds update:snapshots to CONTRIBUTING
2021-08-28 16:30:17 +02:00
4ae482c707 feat: adds support for nunomaduro/collision:^6.0 2021-08-27 22:32:18 +01:00
075c31bc78 release: v1.17.0 2021-08-26 21:17:03 +01:00
2125bf9668 chore: adjusts tests 2021-08-26 21:14:56 +01:00
dbf3c0a8cf Merge pull request #361 from kbond/throw-expectation
Add `toThrow` expectation
2021-08-26 21:02:38 +01:00
eca5f89e59 release: v1.16.0 2021-08-19 17:07:45 +01:00
0b0beac122 Script update 2021-08-16 22:12:13 +01:00
578e97123d Changes -P to -p 2021-08-13 22:09:55 +01:00
01d672d563 Merge pull request #369 from pestphp/parallel
Parallel testing support
2021-08-13 16:00:26 +01:00
490b2d66e5 Fix 2021-08-13 13:41:00 +01:00
0368c4846f Adds a composer test:parallel script 2021-08-13 13:40:07 +01:00
4dfc02c5da Refactor 2021-08-13 11:19:03 +01:00
5c84b0c6d3 The getFilenames method now respects only calls. 2021-08-13 11:07:52 +01:00
b6c06e8c30 Bugfix 2021-08-13 10:46:07 +01:00
c6435d5606 Adds a helpful message for users trying to run parallel without the plugin 2021-08-13 10:44:11 +01:00
2887d212e3 CS fix 2021-08-13 10:30:26 +01:00
cadae52d5d Updates snapshots 2021-08-13 10:29:21 +01:00
d9749ca65b Adds a method for getting all filenames 2021-08-13 10:26:38 +01:00
c2070cd99d Refactor 2021-08-13 10:19:10 +01:00
03d34e9a10 Removes isInParallel 2021-08-13 10:14:01 +01:00
45e76a6df6 Typehint updates 2021-08-13 09:41:45 +01:00
28dd3c2a03 Adds -P as a parallel shortcut 2021-08-13 09:39:56 +01:00
5de981d923 Updates composer.json 2021-08-13 09:12:56 +01:00
a55b31e7c3 Update FUNDING.yml 2021-08-12 21:41:42 +01:00
5f0bd8180e Update FUNDING.yml 2021-08-12 21:41:05 +01:00
e1f1fcccbe Removes unneeded dependency 2021-08-11 21:25:41 +01:00
ab04aef561 Refactors addOutput 2021-08-11 20:59:19 +01:00
79ddb1f58e Composer change 2021-08-11 15:57:49 +01:00
c7a2e68941 Fixes a problem with the PhpUnit logger 2021-08-10 14:57:36 +01:00
5c592928d4 Adds a new method, isInParallel, to the Testable trait to allow a test to determine its parallel status. 2021-08-10 14:34:10 +01:00
bcab4224fb Skips a test that doesn't support parallel. 2021-08-10 14:24:34 +01:00
892f70b5b5 Refactor 2021-08-10 13:17:11 +01:00
5c7de5ad75 Refactor 2021-08-10 12:04:15 +01:00
995088b522 Refactor 2021-08-10 11:32:43 +01:00
ef503646ee Removes parallel classes. 2021-08-10 11:30:48 +01:00
a760470e48 Adds Pest output to parallel. 2021-08-09 18:57:20 +01:00
31d1b1b91d Adds a little more spacing above the coverage output. 2021-08-06 15:15:09 +01:00
7524c80af6 Merge pull request #370 from nuernbergerA/parallel
Parallel additions
2021-08-06 15:04:51 +01:00
721d5134b7 replace str_starts_with to support pre php 8.0 2021-08-06 16:00:03 +02:00
0b5321fdd7 only check for coverage driver if option is present 2021-08-06 15:48:38 +02:00
c86058fed1 use support class 2021-08-06 15:39:19 +02:00
8b295b5e9d remove debug statements 2021-08-06 15:38:59 +02:00
221248e691 introduced argument mapping, added pest coverage 2021-08-06 15:25:28 +02:00
7621247bb7 Adds workflows for parallel 2021-08-05 17:45:50 +01:00
463a50ebd4 Investigating bug fix 2021-08-05 17:39:23 +01:00
62aabc6ae1 Bugfix. The TestCase is now aware of if it is running in parallel or not 😎 2021-08-05 17:09:45 +01:00
1ca9aa5ca6 Further cleanup 2021-08-05 16:43:09 +01:00
7a76f8dce2 Further cleanup 2021-08-05 16:21:06 +01:00
beca27599c Further cleanup 2021-08-05 16:18:47 +01:00
256b167eaf Improvements 2021-08-05 15:31:09 +01:00
5526d4c24d Cleanup 2021-08-05 13:51:55 +01:00
7ea138c640 CS fixes 2021-08-05 13:41:04 +01:00
c4a659c3b5 Composer changes 2021-08-05 13:16:29 +01:00
0a3991c314 Initial working draft 2021-08-05 13:13:53 +01:00
d1a9e0bbe3 release: v1.15.0 2021-08-04 23:02:18 +01:00
17eacfdf95 Update RELEASE.md 2021-08-04 23:01:40 +01:00
9ec0762d41 tests: refactors toBeTruthy and toBeFalsy 2021-08-04 22:56:18 +01:00
30f39f1850 Merge pull request #367 from gabbanaesteban/add-to-be-falsy-and-to-be-truthy
Add `toBeTruthy` and `ToBeFalsy`
2021-08-04 22:45:39 +01:00
8ee07330b3 Add toBeFalsy 2021-08-03 20:55:07 -04:00
cffde4564d Add toBeTruthy 2021-08-03 20:54:56 -04:00
ce7a7649a2 chore: removes scripts folder 2021-08-03 18:59:36 +01:00
eeed7e6a0a chore: removes scripts folder 2021-08-03 18:57:41 +01:00
d6844f5239 Delete compile.php 2021-08-03 18:55:53 +01:00
06c4019e81 cs: removes comment 2021-08-03 18:22:15 +01:00
7785a8cc58 chore: reverts changes regarding PHP 8.1 2021-08-03 18:00:42 +01:00
663516c1e3 Merge pull request #366 from jeroenvanrensen/patch-1
Fix typography
2021-08-03 14:32:50 +01:00
e83667a20b docs: update changelog 2021-08-03 14:01:53 +01:00
aa96d75fb9 Merge pull request #364 from pestphp/bound-datasets
Datasets can now access the test case
2021-08-03 13:58:35 +01:00
a5af4bc5ed Fix typography 2021-08-03 15:04:03 +03:00
57161ba5ad Merges master 2021-08-02 14:03:33 +01:00
b1a9254fc1 Merge branch 'master' into bound-datasets
# Conflicts:
#	tests/.snapshots/success.txt
2021-08-02 14:02:49 +01:00
e56e818659 Splits 8.1 OS jobs 2021-08-02 13:06:41 +01:00
79de0d5875 Splits 8.1 OS jobs 2021-08-02 13:05:55 +01:00
f6f8140ebc Splits 8.1 OS jobs 2021-08-02 13:05:29 +01:00
eed221af46 Adds continue-on-error for php 8.1 2021-08-02 13:03:13 +01:00
524457a4e6 Adds continue-on-error for php 8.1 2021-08-02 13:00:07 +01:00
8e289b7a7d Adds continue-on-error for php 8.1 2021-08-02 12:59:07 +01:00
172b69cf15 Adds continue-on-error for php 8.1 2021-08-02 12:57:57 +01:00
475279a4fa Adds continue-on-error for php 8.1 2021-08-02 12:56:29 +01:00
4b236bf9ff docs: update changelog 2021-08-02 12:50:44 +01:00
50e2d10029 docs: update changelog 2021-08-02 12:50:28 +01:00
fa8a57f1ab Merge pull request #350 from pestphp/teamcity-styling
Teamcity styling
2021-08-02 12:47:54 +01:00
715f8b420b Updates phpunit dependency version 2021-08-02 12:41:23 +01:00
c776bcf86d add toThrow expectation 2021-08-01 12:33:09 -04:00
a0637d86ff Refactors 2021-08-01 17:00:01 +01:00
1a941d7f92 Refactors 2021-08-01 16:45:06 +01:00
b5959aa3fa Refactors 2021-08-01 15:42:52 +01:00
8861dd2401 Merges with master 2021-08-01 15:40:34 +01:00
da73f4b395 Merge branch 'master' into teamcity-styling
# Conflicts:
#	tests/.snapshots/success.txt
2021-08-01 15:36:53 +01:00
d5097d0fe5 Merge conflicts 2021-07-31 23:28:23 +01:00
022ad4be0d Merge branch 'master' into bound-datasets
# Conflicts:
#	tests/.snapshots/success.txt
2021-07-31 23:27:38 +01:00
2d2a83e9e8 chore: run tests against php 8.1 2021-07-31 22:56:33 +01:00
67c7bee4fa Merge pull request #365 from def-studio/expect-toContain-with-multiple-needles
implements multiple needles in expect()->toContain()
2021-07-31 20:34:33 +01:00
675b0f1ec8 implements multiple needles in expect()->toContain() 2021-07-31 18:29:03 +02:00
c2b86c3ab3 Removes pest from stack traces 2021-07-30 16:34:30 +01:00
46337b8085 Removes pest from stack traces 2021-07-30 16:32:00 +01:00
df172d8eed Adds new lines 2021-07-30 15:36:38 +01:00
24b9160b79 Removes new line from event printing 2021-07-30 15:34:31 +01:00
a7860b0b8e CS 2021-07-30 12:41:57 +01:00
7471c224fa Output improvements. 2021-07-30 12:39:20 +01:00
2996135155 Output improvements. 2021-07-30 12:38:13 +01:00
1abab8d440 Output improvements. 2021-07-30 12:36:52 +01:00
e52c83e5be Output improvements. 2021-07-30 12:35:20 +01:00
5ed4545737 Output improvements. 2021-07-30 12:34:10 +01:00
8b39df68ce Output improvements. 2021-07-30 12:32:27 +01:00
04d8a3762b Output improvements. 2021-07-30 12:31:03 +01:00
0f7c8d00d6 Output improvements. 2021-07-30 12:29:39 +01:00
12fb4f8639 Output improvements. 2021-07-30 12:24:23 +01:00
6a84b825e6 Output improvements. 2021-07-30 12:23:24 +01:00
e8595c56b3 Merge branch 'master' into teamcity-styling 2021-07-30 11:46:57 +01:00
4a9fb2fa74 Snapshot update 2021-07-28 10:41:47 +01:00
d8fae6d689 Datasets can now access the test case and are executed after the setup method has run. 2021-07-28 10:39:39 +01:00
252f9a0e46 Merge pull request #321 from jordanbrauer/fix-missing-dataset-errors
Add user-friendly exception message for missing test inputs
2021-07-28 09:57:39 +01:00
8d24b4a217 chore: replace prop set/check with method call 2021-07-27 23:39:26 -05:00
43920f79a9 feat: add count method for checking message type presence 2021-07-27 23:36:09 -05:00
55bfc5856b Merge branch 'master' into fix-missing-dataset-errors 2021-07-27 23:02:12 -05:00
cd9d4acbc2 release: v1.13.0 2021-07-28 02:03:24 +01:00
2b5355419a chore: fixes types 2021-07-28 01:59:47 +01:00
22b822ce87 fix: skip with a false condition being ignored 2021-07-28 01:57:13 +01:00
b2c298b926 Merge pull request #363 from freekmurze/add-to-be-in
Add `toBeIn` expectation
2021-07-28 00:09:54 +01:00
671f3df115 fix tests 2021-07-28 01:00:19 +02:00
2dd77001b7 static analysis fix 2021-07-28 00:42:54 +02:00
5f574ded81 add toBeIn 2021-07-28 00:36:43 +02:00
6309e6818d Update README.md 2021-07-26 23:19:51 +01:00
4813ab6ffb Add sponsor 2021-07-26 23:18:55 +01:00
4ebba1298a release: v1.12.0 2021-07-26 22:46:22 +01:00
2e0d1bb5a0 Merge branch 'master' of https://github.com/pestphp/pest 2021-07-26 22:45:08 +01:00
09d2b16767 docs: updates changelog 2021-07-26 22:45:03 +01:00
f56556eb73 Merge pull request #357 from jordanbrauer/fix-static-hooks
Fix static hooks
2021-07-26 22:33:58 +01:00
f387ca8624 Merge pull request #353 from rezaamini-ir/master
Add --force flag to pest:test command
2021-07-26 22:27:08 +01:00
ca9d783cf9 chore: makes phpunit/phpunit requirement above ^9.3.7 2021-07-26 22:02:57 +01:00
863ddea50b style: split assignment into two lines clarity 2021-07-22 17:54:22 -05:00
00b4bb6305 Merge branch 'master' into fix-missing-dataset-errors 2021-07-21 21:26:33 -05:00
d217503a6a test: use consistent test descriptors 2021-07-21 20:47:48 -05:00
b6012862c4 style: run linter 2021-07-21 20:43:21 -05:00
60c0ad006f fix: prevent the global hooks from piling up (#351)
this happens due to the global `*All()` hooks (before & after) being
defined as static. We should be a good citizen and we need to clean up
our mess for the next person in the test instance constructor
2021-07-21 20:38:20 -05:00
79ff332afe test: global "all" hooks must only run once (#351)
in other words, they can only run once per file
2021-07-21 20:32:39 -05:00
595bbe32a4 Add bool to the condition 2021-07-22 01:41:12 +04:30
328427bfdb docs: update changelog 2021-07-21 12:59:45 +01:00
51f556799c Merge pull request #352 from pestphp/higher-order-callable-datasets
Higher order callable datasets
2021-07-21 12:04:35 +01:00
5e0a0855ea Add a better description 2021-07-21 14:38:11 +04:30
7ec3460d73 Add --force flag to pest:test command 2021-07-21 13:02:09 +04:30
4c8c42cd20 Refactor 2021-07-21 08:47:55 +01:00
09682dd393 Alters test to prove order doesn't matter 2021-07-21 08:16:54 +01:00
82c18d3848 Type fixes 2021-07-21 07:58:05 +01:00
3bdba9210d Adds another test 2021-07-21 07:44:05 +01:00
371620d161 Adds support for receiving datasets in higher order tests 2021-07-21 07:40:19 +01:00
d10281f851 Adds test cases for loggers and removes use of str_starts_with. 2021-07-19 12:59:18 +01:00
47ceb2419b Update README.md 2021-07-16 22:12:31 +01:00
771b5b2e53 Adds Spatie 2021-07-16 22:12:19 +01:00
05f72f9b6d Aligns test case name with test names 2021-07-16 17:59:07 +01:00
f9de1b9c00 Refactor 2021-07-16 17:51:12 +01:00
9516e56242 Improves test case name styling 2021-07-16 17:50:18 +01:00
5f315fc899 Updates warning color 2021-07-16 17:46:30 +01:00
e59606818d More work on output 2021-07-16 17:26:48 +01:00
e16104350e More work on output 2021-07-16 16:45:35 +01:00
df31191f4e Merge branch 'master' into teamcity-assertion-fix 2021-07-16 16:45:16 +01:00
5b310f6f93 More work on output 2021-07-16 15:58:44 +01:00
0200f90e9a Start of work on better TeamCity output 2021-07-16 15:06:10 +01:00
027e69e48f Merge pull request #349 from pestphp/teamcity-assertion-fix
Fixes the assertion output showing as 0 in the TeamCity logger
2021-07-16 14:58:30 +01:00
33e01e3805 Merge pull request #348 from jdanino/stubfix
Fix the unit test stub
2021-07-16 14:26:01 +01:00
13781dcd14 Fixes the assertion output showing as 0 in the TeamCity logger. 2021-07-16 13:17:33 +01:00
164bad437a Fixes the assertion output showing as 0 in the TeamCity logger. 2021-07-16 13:10:30 +01:00
7ddcc03ad9 Update stubs/Unit.php
Co-authored-by: Owen Voke <development@voke.dev>
2021-07-16 13:00:32 +01:00
bbe4445257 Fix the unit test stub 2021-07-16 12:40:57 +02:00
d90ddf889c docs: update changelog 2021-07-12 11:54:40 +01:00
5907164749 Merge pull request #341 from pestphp/hot-hoe
Adds support for Higher Order Expectations in Higher Order Tests
2021-07-12 11:52:04 +01:00
eb6de433b7 docs: update changelog 2021-07-11 09:08:31 +01:00
32f72cdf55 Merge pull request #344 from pestphp/callable-expect-fix
Forces higher order test callable expects to be closures
2021-07-11 09:04:59 +01:00
c160d97428 Forces higher order test callable expects to be closures. 2021-07-11 07:56:02 +01:00
2a8de0565f Adds support for Higher Order Expectations in Higher Order Tests 2021-07-09 16:50:15 +01:00
5ab3c6e2d7 Merge pull request #340 from pestphp/better-tap-type-hinting
Better tap type hinting
2021-07-09 16:34:18 +01:00
50ece576a7 Improves type-hinting for target in HigherOrderTapProxy 2021-07-09 16:09:53 +01:00
9c202fa2d7 Improves type-hinting for tap method 2021-07-09 16:07:37 +01:00
e4e9cb09e4 docs: update changelog 2021-07-09 09:15:22 +01:00
6a7597c01a Merge pull request #339 from pestphp/exception-improvements
Allows you to just specify an exception message when calling `throws`.
2021-07-09 09:08:05 +01:00
dd05452edd Renames a property to be more inclusive. 2021-07-08 18:39:09 +01:00
99ea9f42e5 Allows you to just specify an exception message when calling throws. 2021-07-08 18:32:02 +01:00
dc75b34deb refactor: move logic into boolean method in TestCaseFactory 2021-06-30 09:51:49 -05:00
8e22803797 Merge branch 'master' into fix-missing-dataset-errors 2021-06-30 09:04:20 -05:00
82bd836ae9 Merge branch 'master' into fix-missing-dataset-errors 2021-06-21 19:50:15 -05:00
e64856c664 test: use throws method instead of assert 2021-06-17 13:33:11 -05:00
de86598c0d Merge branch 'master' into fix-missing-dataset-errors 2021-06-16 21:36:42 -05:00
553b45306f feat: handle unions (PHP 8) 2021-06-16 21:29:08 -05:00
2e7192ab95 chore: static analysis adjustments - deprecation RE: ReflectionType::__toString 2021-06-16 14:42:51 -05:00
e21e3080e0 test: add new exception check & update snapshots 2021-06-16 14:04:50 -05:00
6a7ee90ff5 feat: throw user-friendly exception for missing argument data 2021-06-16 01:01:34 -05:00
9d66893d5a feat: add boolean property to signal dependency proxy calls 2021-06-16 01:00:23 -05:00
64e780cf72 fix: types 2021-06-16 00:58:46 -05:00
9bf141f698 style: formatting & linting 2021-06-16 00:54:22 -05:00
9904094590 feat: add new exception for missing datasets on tests with arguments 2021-06-16 00:53:17 -05:00
9e5b779abc feat: add helper function for mapping a function's arguments 2021-06-16 00:52:31 -05:00
55 changed files with 1337 additions and 286 deletions

View File

@ -8,10 +8,11 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
php: ['7.3', '7.4', '8.0']
php: ['7.3', '7.4', '8.0', '8.1']
dependency-version: [prefer-lowest, prefer-stable]
parallel: ['', '--parallel']
name: PHP ${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }}
name: PHP ${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }} - ${{ matrix.parallel }}
steps:
- name: Checkout
@ -29,16 +30,11 @@ jobs:
echo "::add-matcher::${{ runner.tool_cache }}/php.json"
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Install PHP 7 dependencies
- name: Install PHP dependencies
run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress
if: "matrix.php < 8"
- name: Install PHP 8 dependencies
run: composer update --${{ matrix.dependency-version }} --ignore-platform-req=php --no-interaction --no-progress
if: "matrix.php >= 8"
- name: Unit Tests
run: php bin/pest --colors=always --exclude-group=integration
run: php bin/pest --colors=always --exclude-group=integration ${{ matrix.parallel }}
- name: Integration Tests
run: php bin/pest --colors=always --group=integration

View File

@ -3,7 +3,6 @@
$finder = PhpCsFixer\Finder::create()
->in(__DIR__ . DIRECTORY_SEPARATOR . 'tests')
->in(__DIR__ . DIRECTORY_SEPARATOR . 'bin')
->in(__DIR__ . DIRECTORY_SEPARATOR . 'scripts')
->in(__DIR__ . DIRECTORY_SEPARATOR . 'stubs')
->in(__DIR__ . DIRECTORY_SEPARATOR . 'src')
->append(['.php-cs-fixer.dist.php']);

View File

@ -4,6 +4,69 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [v1.19.0 (2021-09-20)](https://github.com/pestphp/pest/compare/v1.18.0...v1.19.0)
### Added
- PHP 8.1 support ([e6c7d68](https://github.com/pestphp/pest/commit/e6c7d68defaec8efe01e71e15dd8d8c45b0cf60f))
## [v1.18.0 (2021-08-30)](https://github.com/pestphp/pest/compare/v1.17.0...v1.18.0)
### Added
- `toHaveLength` expectation ([#386](https://github.com/pestphp/pest/pull/386))
- `nunomaduro/collision:^6.0` support ([4ae482c](https://github.com/pestphp/pest/commit/4ae482c7073fb77782b8a4b5738ef1fcea0f82ab))
## [v1.17.0 (2021-08-26)](https://github.com/pestphp/pest/compare/v1.16.0...v1.17.0)
### Added
- `toThrow` expectation ([#361](https://github.com/pestphp/pest/pull/361))
## [v1.16.0 (2021-08-19)](https://github.com/pestphp/pest/compare/v1.15.0...v1.16.0)
### Added
- Support for new parallel options ([#369](https://github.com/pestphp/pest/pull/369))
## [v1.15.0 (2021-08-04)](https://github.com/pestphp/pest/compare/v1.14.0...v1.15.0)
### Added
- `toBeTruthy` and `toBeFalsy` ([#367](https://github.com/pestphp/pest/pull/367))
## [v1.14.0 (2021-08-03)](https://github.com/pestphp/pest/compare/v1.13.0...v1.14.0)
### Added
- A new bound closure that allows you to access the test case in Datasets ([#364](https://github.com/pestphp/pest/pull/364))
## [v1.13.0 (2021-08-02)](https://github.com/pestphp/pest/compare/v1.12.0...v1.13.0)
### Added
- A cleaner output when running the Pest runner in PhpStorm ([#350](https://github.com/pestphp/pest/pull/350))
- `toBeIn` expectation ([#363](https://github.com/pestphp/pest/pull/363))
### Fixed
- `skip` with false condition marking test as skipped ([22b822c](https://github.com/pestphp/pest/commit/22b822ce87a3d19d84960fa5c93eb286820b525d))
## [v1.12.0 (2021-07-26)](https://github.com/pestphp/pest/compare/v1.11.0...v1.12.0)
### Added
- `--force` option to override tests in `pest:test` artisan command ([#353](https://github.com/pestphp/pest/pull/353))
- Support for PHPUnit `^9.3.7` ([ca9d783](https://github.com/pestphp/pest/commit/ca9d783cf942a2caabc85ff7a728c7f28350c67a))
### Fixed
- `beforeAll` and `afterAll` behind called multiple times per test ([#357](https://github.com/pestphp/pest/pull/357))
## [v1.11.0 (2021-07-21)](https://github.com/pestphp/pest/compare/v1.10.0...v1.11.0)
### Added
- Support for interacting with datasets in higher order tests ([#352](https://github.com/pestphp/pest/pull/352))
### Changed
- The unit test stub now uses the expectation API ([#348](https://github.com/pestphp/pest/pull/348))
### Fixed
- PhpStorm will no longer show 0 assertions in the output ([#349](https://github.com/pestphp/pest/pull/349))
## [v1.10.0 (2021-07-12)](https://github.com/pestphp/pest/compare/v1.9.1...v1.10.0)
### Added
- The ability to use higher order expectations inside higher order tests ([#341](https://github.com/pestphp/pest/pull/341))
## [v1.9.1 (2021-07-11)](https://github.com/pestphp/pest/compare/v1.9.0...v1.9.1)
### Fixed
- Callable `expect` values in higher order tests failing if the value was an existing method name ([#334](https://github.com/pestphp/pest/pull/344))
## [v1.9.0 (2021-07-09)](https://github.com/pestphp/pest/compare/v1.8.0...v1.9.0)
### Changed
- You may now pass just an exception message when using the `throws` method ([#339](https://github.com/pestphp/pest/pull/339))
## [v1.8.0 (2021-07-08)](https://github.com/pestphp/pest/compare/v1.7.1...v1.8.0)
### Added
- A new `tap` and test case aware `expect` methods for higher order tests ([#331](https://github.com/pestphp/pest/pull/331))

View File

@ -31,6 +31,10 @@ composer lint
```
## Tests
Update the snapshots:
```bash
composer update:snapshots
```
Run all tests:
```bash
composer test

View File

@ -21,8 +21,12 @@ We would like to extend our thanks to the following sponsors for funding Pest de
### Premium Sponsors
- **[Scout APM](https://scoutapm.com)**
- **[Akaunting](https://akaunting.com)**
- **[Meema](https://meema.io/)**
- **[Auth0](https://auth0.com)**
- **[Codecourse](https://codecourse.com/)**
- **[Fathom Analytics](https://usefathom.com/)**
- **[Meema](https://meema.io)**
- **[Scout APM](https://scoutapm.com)**
- **[Spatie](https://spatie.be)**
Pest was created by **[Nuno Maduro](https://twitter.com/enunomaduro)** under the **[Sponsorware license](https://github.com/sponsorware/docs)**. It got open-sourced and is now licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.
Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.

View File

@ -6,7 +6,7 @@ When releasing a new version of Pest there are some checks and updates that need
- On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...master](https://github.com/pestphp/pest/compare/{latest_version}...master) and update the [changelog](CHANGELOG.md) file with the main changes for this release
- Update the version number in [src/Pest.php](src/Pest.php)
- Run the tests locally using: `composer test`
- Commit the CHANGELOG and Pest file with the message: `git commit -m "docs: update changelog"`
- Commit the CHANGELOG and Pest file with the message: `git commit -m "release: vX.X.X"`
- Push the changes to GitHub
- Check that the CI is passing as expected: [github.com/pestphp/pest/actions](https://github.com/pestphp/pest/actions)
- Tag and push the tag with `git tag vX.X.X && git push --tags`

View File

@ -3,9 +3,9 @@
use NunoMaduro\Collision\Provider;
use Pest\Actions\ValidatesEnvironment;
use Pest\Console\Command;
use Pest\Support\Container;
use Pest\TestSuite;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
@ -27,7 +27,7 @@ use Symfony\Component\Console\Output\OutputInterface;
(new Provider())->register();
// get $rootPath based on $autoloadPath
// Get $rootPath based on $autoloadPath
$rootPath = dirname($autoloadPath, 2);
$argv = new ArgvInput();
@ -42,14 +42,22 @@ use Symfony\Component\Console\Output\OutputInterface;
ValidatesEnvironment::in($testSuite);
// lets remove any arguments that PHPUnit does not understand
$args = $_SERVER['argv'];
// Let's remove any arguments that PHPUnit does not understand
if ($argv->hasParameterOption('--test-directory')) {
foreach ($_SERVER['argv'] as $key => $value) {
foreach ($args as $key => $value) {
if (strpos($value, '--test-directory') !== false) {
unset($_SERVER['argv'][$key]);
unset($args[$key]);
}
}
}
exit($container->get(Command::class)->run($_SERVER['argv']));
if (($runInParallel = $argv->hasParameterOption(['--parallel', '-p'])) && !class_exists(\Pest\Parallel\Command::class)) {
$output->writeln("Parallel support requires the Pest Parallel plugin. Run <fg=yellow;options=bold>`composer require --dev pestphp/pest-plugin-parallel`</> first.");
exit(Command::FAILURE);
}
$command = $runInParallel ? \Pest\Parallel\Command::class : \Pest\Console\Command::class;
exit($container->get($command)->run($args));
})();

View File

@ -18,9 +18,9 @@
],
"require": {
"php": "^7.3 || ^8.0",
"nunomaduro/collision": "^5.4.0",
"nunomaduro/collision": "^5.4.0|^6.0",
"pestphp/pest-plugin": "^1.0.0",
"phpunit/phpunit": ">= 9.3.7 <= 9.5.6"
"phpunit/phpunit": "^9.5.5"
},
"autoload": {
"psr-4": {
@ -43,7 +43,8 @@
"illuminate/console": "^8.47.0",
"illuminate/support": "^8.47.0",
"laravel/dusk": "^6.15.0",
"pestphp/pest-dev-tools": "dev-master"
"pestphp/pest-dev-tools": "dev-master",
"pestphp/pest-plugin-parallel": "^1.0"
},
"minimum-stability": "dev",
"prefer-stable": true,
@ -57,14 +58,16 @@
"scripts": {
"lint": "php-cs-fixer fix -v",
"test:lint": "php-cs-fixer fix -v --dry-run",
"test:types": "phpstan analyse --ansi --memory-limit=0",
"test:types": "phpstan analyse --ansi --memory-limit=-1",
"test:unit": "php bin/pest --colors=always --exclude-group=integration",
"test:parallel": "php bin/pest -p --colors=always --exclude-group=integration",
"test:integration": "php bin/pest --colors=always --group=integration",
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always",
"test": [
"@test:lint",
"@test:types",
"@test:unit",
"@test:parallel",
"@test:integration"
]
},

View File

@ -7,7 +7,6 @@ parameters:
level: max
paths:
- src
- scripts
checkMissingIterableValueType: true
checkGenericClassInNonGenericObjectType: false

View File

@ -1,37 +0,0 @@
<?php
declare(strict_types=1);
$globalsFilePath = implode(DIRECTORY_SEPARATOR, [
dirname(__DIR__),
'vendor',
'phpunit',
'phpunit',
'src',
'Framework',
'Assert',
'Functions.php',
]);
$compiledFilePath = implode(DIRECTORY_SEPARATOR, [dirname(__DIR__), 'compiled', 'globals.php']);
/* @phpstan-ignore-next-line */
@unlink($compiledFilePath);
$replace = function ($contents, $string, $by) {
return str_replace($string, $by, $contents);
};
$remove = function ($contents, $string) {
return str_replace($string, '', $contents);
};
$contents = file_get_contents($globalsFilePath);
$contents = $replace($contents, 'namespace PHPUnit\Framework;', 'use PHPUnit\Framework\Assert;');
$contents = $remove($contents, 'use ArrayAccess;');
$contents = $remove($contents, 'use Countable;');
$contents = $remove($contents, 'use DOMDocument;');
$contents = $remove($contents, 'use DOMElement;');
$contents = $remove($contents, 'use Throwable;');
file_put_contents(implode(DIRECTORY_SEPARATOR, [dirname(__DIR__), 'compiled', 'globals.php']), $contents);

View File

@ -30,7 +30,7 @@ final class AddsDefaults
}
if ($arguments[self::PRINTER] === \PHPUnit\Util\Log\TeamCity::class) {
$arguments[self::PRINTER] = new TeamCity($arguments['verbose'] ?? false, $arguments['colors'] ?? DefaultResultPrinter::COLOR_ALWAYS);
$arguments[self::PRINTER] = new TeamCity(null, $arguments['verbose'] ?? false, $arguments['colors'] ?? DefaultResultPrinter::COLOR_ALWAYS);
}
// Load our junit logger instead.

View File

@ -22,8 +22,6 @@ final class AddsTests
{
self::removeTestClosureWarnings($testSuite);
// @todo refactor this...
$testSuites = [];
$pestTestSuite->tests->build($pestTestSuite, function (TestCase $testCase) use (&$testSuites): void {
$testCaseClass = get_class($testCase);

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Pest\Actions;
use Pest\Contracts\Plugins\AddsOutput;
use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Plugin\Loader;
/**
* @internal
*/
final class InteractsWithPlugins
{
/**
* Transform the input arguments by passing it to the relevant plugins.
*
* @param array<int, string> $argv
*
* @return array<int, string>
*/
public static function handleArguments(array $argv): array
{
$plugins = Loader::getPlugins(HandlesArguments::class);
/** @var HandlesArguments $plugin */
foreach ($plugins as $plugin) {
$argv = $plugin->handleArguments($argv);
}
return $argv;
}
/**
* Provides an opportunity for any plugins that want
* to provide additional output after test execution.
*/
public static function addOutput(int $result): int
{
$plugins = Loader::getPlugins(AddsOutput::class);
/** @var AddsOutput $plugin */
foreach ($plugins as $plugin) {
$result = $plugin->addOutput($result);
}
return $result;
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Pest\Concerns\Logging;
/**
* @internal
*/
trait WritesToConsole
{
private function writeSuccess(string $message): void
{
$this->writePestTestOutput($message, 'fg-green, bold', '✓');
}
private function writeError(string $message): void
{
$this->writePestTestOutput($message, 'fg-red, bold', '');
}
private function writeWarning(string $message): void
{
$this->writePestTestOutput($message, 'fg-yellow, bold', '-');
}
private function writePestTestOutput(string $message, string $color, string $symbol): void
{
$this->writeWithColor($color, "$symbol ", false);
$this->write($message);
$this->writeNewLine();
}
}

View File

@ -73,6 +73,8 @@ trait Testable
{
$this->__test = $test;
$this->__description = $description;
self::$beforeAll = null;
self::$afterAll = null;
parent::__construct('__test', $data);
}
@ -271,7 +273,19 @@ trait Testable
*/
public function __test()
{
return $this->__callClosure($this->__test, func_get_args());
return $this->__callClosure($this->__test, $this->resolveTestArguments(func_get_args()));
}
/**
* Resolve the passed arguments. Any Closures will be bound to the testcase and resolved.
*
* @throws Throwable
*/
private function resolveTestArguments(array $arguments): array
{
return array_map(function ($data) {
return $data instanceof Closure ? $this->__callClosure($data, []) : $data;
}, $arguments);
}
/**

View File

@ -6,11 +6,9 @@ namespace Pest\Console;
use Pest\Actions\AddsDefaults;
use Pest\Actions\AddsTests;
use Pest\Actions\InteractsWithPlugins;
use Pest\Actions\LoadStructure;
use Pest\Actions\ValidatesConfiguration;
use Pest\Contracts\Plugins\AddsOutput;
use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Plugin\Loader;
use Pest\Plugins\Version;
use Pest\Support\Container;
use Pest\TestSuite;
@ -57,23 +55,12 @@ final class Command extends BaseCommand
*/
protected function handleArguments(array $argv): void
{
/*
* First, let's call all plugins that want to handle arguments
*/
$plugins = Loader::getPlugins(HandlesArguments::class);
$argv = InteractsWithPlugins::handleArguments($argv);
/** @var HandlesArguments $plugin */
foreach ($plugins as $plugin) {
$argv = $plugin->handleArguments($argv);
}
/*
* Next, as usual, let's send the console arguments to PHPUnit.
*/
parent::handleArguments($argv);
/*
* Finally, let's validate the configuration. Making
* Let's validate the configuration. Making
* sure all options are yet supported by Pest.
*/
ValidatesConfiguration::in($this->arguments);
@ -128,16 +115,7 @@ final class Command extends BaseCommand
LoadStructure::in($this->testSuite->rootPath);
$result = parent::run($argv, false);
/*
* Let's call all plugins that want to add output after test execution
*/
$plugins = Loader::getPlugins(AddsOutput::class);
/** @var AddsOutput $plugin */
foreach ($plugins as $plugin) {
$result = $plugin->addOutput($result);
}
$result = InteractsWithPlugins::addOutput($result);
exit($result);
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use BadFunctionCallException;
use NunoMaduro\Collision\Contracts\RenderlessEditor;
use NunoMaduro\Collision\Contracts\RenderlessTrace;
use Symfony\Component\Console\Exception\ExceptionInterface;
/**
* Creates a new instance of dataset is not present for test that has arguments.
*
* @internal
*/
final class DatasetMissing extends BadFunctionCallException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
/**
* Create new exception instance.
*
* @param array<string, string> $args A map of argument names to their typee
*/
public function __construct(string $file, string $name, array $args)
{
parent::__construct(sprintf(
"A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s",
$name,
count($args),
implode(', ', array_map(static function (string $arg, string $type): string {
return sprintf('%s $%s', $type, $arg);
}, array_keys($args), $args)),
$file,
));
}
}

View File

@ -5,13 +5,19 @@ declare(strict_types=1);
namespace Pest;
use BadMethodCallException;
use Closure;
use InvalidArgumentException;
use Pest\Concerns\Extendable;
use Pest\Concerns\RetrievesValues;
use Pest\Support\Arr;
use Pest\Support\NullClosure;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\Constraint\Constraint;
use PHPUnit\Framework\ExpectationFailedException;
use ReflectionFunction;
use ReflectionNamedType;
use SebastianBergmann\Exporter\Exporter;
use Throwable;
/**
* @internal
@ -205,6 +211,16 @@ final class Expectation
return $this;
}
/**
* Asserts that the value is truthy.
*/
public function toBeTruthy(): Expectation
{
Assert::assertTrue((bool) $this->value);
return $this;
}
/**
* Asserts that the value is false.
*/
@ -215,6 +231,16 @@ final class Expectation
return $this;
}
/**
* Asserts that the value is falsy.
*/
public function toBeFalsy(): Expectation
{
Assert::assertFalse((bool) $this->value);
return $this;
}
/**
* Asserts that the value is greater than $expected.
*
@ -266,14 +292,16 @@ final class Expectation
/**
* Asserts that $needle is an element of the value.
*
* @param mixed $needle
* @param mixed $needles
*/
public function toContain($needle): Expectation
public function toContain(...$needles): Expectation
{
if (is_string($this->value)) {
Assert::assertStringContainsString($needle, $this->value);
} else {
Assert::assertContains($needle, $this->value);
foreach ($needles as $needle) {
if (is_string($this->value)) {
Assert::assertStringContainsString($needle, $this->value);
} else {
Assert::assertContains($needle, $this->value);
}
}
return $this;
@ -299,6 +327,36 @@ final class Expectation
return $this;
}
/**
* Asserts that $number matches value's Length.
*/
public function toHaveLength(int $number): Expectation
{
if (is_string($this->value)) {
Assert::assertEquals($number, mb_strlen($this->value));
return $this;
}
if (is_iterable($this->value)) {
return $this->toHaveCount($number);
}
if (is_object($this->value)) {
if (method_exists($this->value, 'toArray')) {
$array = $this->value->toArray();
} else {
$array = (array) $this->value;
}
Assert::assertCount($number, $array);
return $this;
}
throw new BadMethodCallException('Expectation value length is not countable.');
}
/**
* Asserts that $count matches the number of elements of the value.
*/
@ -328,6 +386,20 @@ final class Expectation
return $this;
}
/**
* Asserts that the value contains the provided properties $names.
*
* @param iterable<array-key, string> $names
*/
public function toHaveProperties(iterable $names): Expectation
{
foreach ($names as $name) {
$this->toHaveProperty($name);
}
return $this;
}
/**
* Asserts that two variables have the same value.
*
@ -371,6 +443,18 @@ final class Expectation
return $this;
}
/**
* Asserts that the value is one of the given values.
*
* @param iterable<int|string, mixed> $values
*/
public function toBeIn(iterable $values): Expectation
{
Assert::assertContains($this->value, $values);
return $this;
}
/**
* Asserts that the value is infinite.
*/
@ -715,6 +799,56 @@ final class Expectation
return $this;
}
/**
* Asserts that executing value throws an exception.
*
* @param (Closure(Throwable): mixed)|string $exception
*/
public function toThrow($exception, string $exceptionMessage = null): Expectation
{
$callback = NullClosure::create();
if ($exception instanceof Closure) {
$callback = $exception;
$parameters = (new ReflectionFunction($exception))->getParameters();
if (1 !== count($parameters)) {
throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.');
}
if (!($type = $parameters[0]->getType()) instanceof ReflectionNamedType) {
throw new InvalidArgumentException('The given closure\'s parameter must be type-hinted as the class string.');
}
$exception = $type->getName();
}
try {
($this->value)();
} catch (Throwable $e) { // @phpstan-ignore-line
if (!class_exists($exception)) {
Assert::assertStringContainsString($exception, $e->getMessage());
return $this;
}
if ($exceptionMessage !== null) {
Assert::assertStringContainsString($exceptionMessage, $e->getMessage());
}
Assert::assertInstanceOf($exception, $e);
$callback($e);
return $this;
}
if (!class_exists($exception)) {
throw new ExpectationFailedException("Exception with message \"{$exception}\" not thrown.");
}
throw new ExpectationFailedException("Exception \"{$exception}\" not thrown.");
}
/**
* Exports the given value.
*

View File

@ -228,4 +228,13 @@ final class TestCaseFactory
return $classFQN;
}
/**
* Determine if the test case will receive argument input from Pest, or not.
*/
public function receivesArguments(): bool
{
return count($this->datasets) > 0
|| $this->factoryProxies->count('addDependencies') > 0;
}
}

View File

@ -21,7 +21,7 @@ final class PestTestCommand extends Command
*
* @var string
*/
protected $signature = 'pest:test {name : The name of the file} {--unit : Create a unit test} {--dusk : Create a Dusk test} {--test-directory=tests : The name of the tests directory}';
protected $signature = 'pest:test {name : The name of the file} {--unit : Create a unit test} {--dusk : Create a Dusk test} {--test-directory=tests : The name of the tests directory} {--force : Overwrite the existing test file with the same name}';
/**
* The console command description.
@ -56,7 +56,7 @@ final class PestTestCommand extends Command
File::makeDirectory(dirname($target), 0777, true, true);
}
if (File::exists($target)) {
if (File::exists($target) && !(bool) $this->option('force')) {
throw new InvalidConsoleArgument(sprintf('%s already exist', $target));
}

View File

@ -5,25 +5,34 @@ declare(strict_types=1);
namespace Pest\Logging;
use function getmypid;
use Pest\Concerns\Logging\WritesToConsole;
use Pest\Concerns\Testable;
use Pest\Support\ExceptionTrace;
use function Pest\version;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestResult;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Framework\Warning;
use PHPUnit\TextUI\DefaultResultPrinter;
use function round;
use function str_replace;
use function strlen;
use Throwable;
final class TeamCity extends DefaultResultPrinter
{
use WritesToConsole;
private const PROTOCOL = 'pest_qn://';
private const NAME = 'name';
private const LOCATION_HINT = 'locationHint';
private const DURATION = 'duration';
private const TEST_SUITE_STARTED = 'testSuiteStarted';
private const TEST_SUITE_FINISHED = 'testSuiteFinished';
private const TEST_COUNT = 'testCount';
private const TEST_STARTED = 'testStarted';
private const TEST_FINISHED = 'testFinished';
/** @var int */
private $flowId;
@ -34,148 +43,105 @@ final class TeamCity extends DefaultResultPrinter
/** @var \PHPUnit\Util\Log\TeamCity */
private $phpunitTeamCity;
public function __construct(bool $verbose, string $colors)
/**
* @param resource|string|null $out
*/
public function __construct($out, bool $verbose, string $colors)
{
parent::__construct(null, $verbose, $colors, false, 80, false);
$this->phpunitTeamCity = new \PHPUnit\Util\Log\TeamCity(
null,
$verbose,
$colors,
false,
80,
false
);
parent::__construct($out, $verbose, $colors);
$this->phpunitTeamCity = new \PHPUnit\Util\Log\TeamCity($out, $verbose, $colors);
$this->logo();
}
private function logo(): void
{
$this->writeNewLine();
$this->write('Pest ' . version());
$this->writeNewLine();
}
public function printResult(TestResult $result): void
{
$this->printHeader($result);
$this->printFooter($result);
$this->write('Tests: ');
$results = [
'failed' => ['count' => $result->errorCount() + $result->failureCount(), 'color' => 'fg-red'],
'skipped' => ['count' => $result->skippedCount(), 'color' => 'fg-yellow'],
'warned' => ['count' => $result->warningCount(), 'color' => 'fg-yellow'],
'risked' => ['count' => $result->riskyCount(), 'color' => 'fg-yellow'],
'incomplete' => ['count' => $result->notImplementedCount(), 'color' => 'fg-yellow'],
'passed' => ['count' => $this->successfulTestCount($result), 'color' => 'fg-green'],
];
$filteredResults = array_filter($results, function ($item): bool {
return $item['count'] > 0;
});
foreach ($filteredResults as $key => $info) {
$this->writeWithColor($info['color'], $info['count'] . " $key", false);
if ($key !== array_reverse(array_keys($filteredResults))[0]) {
$this->write(', ');
}
}
$this->writeNewLine();
$this->write("Assertions: $this->numAssertions");
$this->writeNewLine();
$this->write("Time: {$result->time()}s");
$this->writeNewLine();
}
private function successfulTestCount(TestResult $result): int
{
return $result->count()
- $result->failureCount()
- $result->errorCount()
- $result->skippedCount()
- $result->warningCount()
- $result->notImplementedCount()
- $result->riskyCount();
}
/** @phpstan-ignore-next-line */
public function startTestSuite(TestSuite $suite): void
{
$suiteName = $suite->getName();
if (static::isCompoundTestSuite($suite)) {
$this->writeWithColor('bold', ' ' . $suiteName);
} elseif (static::isPestTestSuite($suite)) {
$this->writeWithColor('fg-white, bold', ' ' . substr_replace($suiteName, '', 0, 2) . ' ');
} else {
$this->writeWithColor('fg-white, bold', ' ' . $suiteName);
}
$this->writeNewLine();
$this->flowId = (int) getmypid();
if (!$this->isSummaryTestCountPrinted) {
$this->printEvent(
'testCount',
['count' => $suite->count()]
);
$this->printEvent(self::TEST_COUNT, [
'count' => $suite->count(),
]);
$this->isSummaryTestCountPrinted = true;
}
$suiteName = $suite->getName();
if (file_exists($suiteName) || !method_exists($suiteName, '__getFileName')) {
$this->printEvent(
self::TEST_SUITE_STARTED, [
self::NAME => $suiteName,
self::LOCATION_HINT => self::PROTOCOL . $suiteName,
]);
return;
}
$fileName = $suiteName::__getFileName();
$this->printEvent(
self::TEST_SUITE_STARTED, [
self::NAME => substr($suiteName, 2),
self::LOCATION_HINT => self::PROTOCOL . $fileName,
$this->printEvent(self::TEST_SUITE_STARTED, [
self::NAME => static::isCompoundTestSuite($suite) ? $suiteName : substr($suiteName, 2),
self::LOCATION_HINT => self::PROTOCOL . (static::isCompoundTestSuite($suite) ? $suiteName : $suiteName::__getFileName()),
]);
}
/** @phpstan-ignore-next-line */
public function endTestSuite(TestSuite $suite): void
{
$suiteName = $suite->getName();
if (file_exists($suiteName) || !method_exists($suiteName, '__getFileName')) {
$this->printEvent(
self::TEST_SUITE_FINISHED, [
self::NAME => $suiteName,
self::LOCATION_HINT => self::PROTOCOL . $suiteName,
]);
return;
}
$this->printEvent(
self::TEST_SUITE_FINISHED, [
self::NAME => substr($suiteName, 2),
]);
}
/**
* @param Test|Testable $test
*/
public function startTest(Test $test): void
{
if (!TeamCity::isPestTest($test)) {
$this->phpunitTeamCity->startTest($test);
return;
}
$this->printEvent('testStarted', [
self::NAME => $test->getName(),
// @phpstan-ignore-next-line
self::LOCATION_HINT => self::PROTOCOL . $test->toString(),
]);
}
/**
* @param Test|Testable $test
*/
public function endTest(Test $test, float $time): void
{
if (!TeamCity::isPestTest($test)) {
$this->phpunitTeamCity->endTest($test, $time);
return;
}
$this->printEvent('testFinished', [
self::NAME => $test->getName(),
self::DURATION => self::toMilliseconds($time),
]);
}
/**
* @param Test|Testable $test
*/
public function addError(Test $test, Throwable $t, float $time): void
{
$this->phpunitTeamCity->addError($test, $t, $time);
}
/**
* @phpstan-ignore-next-line
*
* @param Test|Testable $test
*/
public function addWarning(Test $test, Warning $e, float $time): void
{
$this->phpunitTeamCity->addWarning($test, $e, $time);
}
public function addFailure(Test $test, AssertionFailedError $e, float $time): void
{
$this->phpunitTeamCity->addFailure($test, $e, $time);
}
protected function writeProgress(string $progress): void
{
}
/**
* @param array<string, string|int> $params
*/
private function printEvent(string $eventName, array $params = []): void
{
$this->write("\n##teamcity[{$eventName}");
$this->write("##teamcity[{$eventName}");
if ($this->flowId !== 0) {
$params['flowId'] = $this->flowId;
@ -198,9 +164,56 @@ final class TeamCity extends DefaultResultPrinter
);
}
private static function toMilliseconds(float $time): int
/** @phpstan-ignore-next-line */
public function endTestSuite(TestSuite $suite): void
{
return (int) round($time * 1000);
$suiteName = $suite->getName();
$this->writeNewLine();
$this->writeNewLine();
$this->printEvent(self::TEST_SUITE_FINISHED, [
self::NAME => static::isCompoundTestSuite($suite) ? $suiteName : substr($suiteName, 2),
self::LOCATION_HINT => self::PROTOCOL . (static::isCompoundTestSuite($suite) ? $suiteName : $suiteName::__getFileName()),
]);
}
/**
* @param Test|Testable $test
*/
public function startTest(Test $test): void
{
if (!TeamCity::isPestTest($test)) {
$this->phpunitTeamCity->startTest($test);
return;
}
$this->printEvent(self::TEST_STARTED, [
self::NAME => $test->getName(),
// @phpstan-ignore-next-line
self::LOCATION_HINT => self::PROTOCOL . $test->toString(),
]);
}
/**
* Verify that the given test suite is a valid Pest suite.
*
* @param TestSuite<Test> $suite
*/
private static function isPestTestSuite(TestSuite $suite): bool
{
return strncmp($suite->getName(), 'P\\', strlen('P\\')) === 0;
}
/**
* Determine if the test suite is made up of multiple smaller test suites.
*
* @param TestSuite<Test> $suite
*/
private static function isCompoundTestSuite(TestSuite $suite): bool
{
return file_exists($suite->getName()) || !method_exists($suite->getName(), '__getFileName');
}
public static function isPestTest(Test $test): bool
@ -210,4 +223,75 @@ final class TeamCity extends DefaultResultPrinter
return in_array(Testable::class, $uses, true);
}
/**
* @param Test|Testable $test
*/
public function endTest(Test $test, float $time): void
{
$this->printEvent(self::TEST_FINISHED, [
self::NAME => $test->getName(),
self::DURATION => self::toMilliseconds($time),
]);
if (!$this->lastTestFailed) {
$this->writeSuccess($test->getName());
}
$this->numAssertions += $test instanceof TestCase ? $test->getNumAssertions() : 1;
$this->lastTestFailed = false;
}
private static function toMilliseconds(float $time): int
{
return (int) round($time * 1000);
}
public function addError(Test $test, Throwable $t, float $time): void
{
$this->markAsFailure($t);
$this->writeError($test->getName());
$this->phpunitTeamCity->addError($test, $t, $time);
}
public function addFailure(Test $test, AssertionFailedError $e, float $time): void
{
$this->markAsFailure($e);
$this->writeError($test->getName());
$this->phpunitTeamCity->addFailure($test, $e, $time);
}
public function addWarning(Test $test, Warning $e, float $time): void
{
$this->markAsFailure($e);
$this->writeWarning($test->getName());
$this->phpunitTeamCity->addWarning($test, $e, $time);
}
public function addIncompleteTest(Test $test, Throwable $t, float $time): void
{
$this->markAsFailure($t);
$this->writeWarning($test->getName());
$this->phpunitTeamCity->addIncompleteTest($test, $t, $time);
}
public function addRiskyTest(Test $test, Throwable $t, float $time): void
{
$this->markAsFailure($t);
$this->writeWarning($test->getName());
$this->phpunitTeamCity->addRiskyTest($test, $t, $time);
}
public function addSkippedTest(Test $test, Throwable $t, float $time): void
{
$this->markAsFailure($t);
$this->writeWarning($test->getName());
$this->phpunitTeamCity->printIgnoredTest($test->getName(), $t, $time);
}
private function markAsFailure(Throwable $t): void
{
$this->lastTestFailed = true;
ExceptionTrace::removePestReferences($t);
}
}

View File

@ -59,11 +59,15 @@ final class TestCall
/**
* Asserts that the test throws the given `$exceptionClass` when called.
*/
public function throws(string $exceptionClass, string $exceptionMessage = null): TestCall
public function throws(string $exception, string $exceptionMessage = null): TestCall
{
$this->testCaseFactory
->proxies
->add(Backtrace::file(), Backtrace::line(), 'expectException', [$exceptionClass]);
if (class_exists($exception)) {
$this->testCaseFactory
->proxies
->add(Backtrace::file(), Backtrace::line(), 'expectException', [$exception]);
} else {
$exceptionMessage = $exception;
}
if (is_string($exceptionMessage)) {
$this->testCaseFactory
@ -144,6 +148,9 @@ final class TestCall
? $conditionOrMessage
: $message;
/** @var callable(): bool $condition */
$condition = $condition->bindTo(null);
$this->testCaseFactory
->chains
->addWhen($condition, Backtrace::file(), Backtrace::line(), 'markTestSkipped', [$message]);
@ -151,12 +158,30 @@ final class TestCall
return $this;
}
/**
* Saves the property accessors to be used on the target.
*/
public function __get(string $name): self
{
return $this->addChain($name);
}
/**
* Saves the calls to be used on the target.
*
* @param array<int, mixed> $arguments
*/
public function __call(string $name, array $arguments): self
{
return $this->addChain($name, $arguments);
}
/**
* Add a chain to the test case factory. Omitting the arguments will treat it as a property accessor.
*
* @param array<int, mixed>|null $arguments
*/
private function addChain(string $name, array $arguments = null): self
{
$this->testCaseFactory
->chains
@ -167,7 +192,9 @@ final class TestCall
if ($this->testCaseFactory->description !== null) {
$this->testCaseFactory->description .= ' → ';
}
$this->testCaseFactory->description .= sprintf('%s %s', $name, $exporter->shortenedRecursiveExport($arguments));
$this->testCaseFactory->description .= $arguments === null
? $name
: sprintf('%s %s', $name, $exporter->shortenedRecursiveExport($arguments));
}
return $this;

View File

@ -6,7 +6,7 @@ namespace Pest;
function version(): string
{
return '1.8.0';
return '1.18.0';
}
function testDirectory(string $file = ''): string

View File

@ -5,11 +5,13 @@ declare(strict_types=1);
namespace Pest\Repositories;
use Closure;
use Pest\Exceptions\DatasetMissing;
use Pest\Exceptions\ShouldNotHappen;
use Pest\Exceptions\TestAlreadyExist;
use Pest\Exceptions\TestCaseAlreadyInUse;
use Pest\Exceptions\TestCaseClassOrTraitNotFound;
use Pest\Factories\TestCaseFactory;
use Pest\Support\Reflection;
use Pest\Support\Str;
use Pest\TestSuite;
use PHPUnit\Framework\TestCase;
@ -20,7 +22,7 @@ use PHPUnit\Framework\TestCase;
final class TestRepository
{
/**
* @var string
* @var non-empty-string
*/
private const SEPARATOR = '>>>';
@ -42,6 +44,20 @@ final class TestRepository
return count($this->state);
}
/**
* Returns the filename of each test that should be executed in the suite.
*
* @return array<int, string>
*/
public function getFilenames(): array
{
$testsWithOnly = $this->testsUsingOnly();
return array_values(array_map(function (TestCaseFactory $factory): string {
return $factory->filename;
}, count($testsWithOnly) > 0 ? $testsWithOnly : $this->state));
}
/**
* Calls the given callable foreach test case.
*/
@ -83,9 +99,7 @@ final class TestRepository
}
}
$onlyState = array_filter($this->state, function ($testFactory): bool {
return $testFactory->only;
});
$onlyState = $this->testsUsingOnly();
$state = count($onlyState) > 0 ? $onlyState : $this->state;
@ -98,6 +112,18 @@ final class TestRepository
}
}
/**
* Return all tests that have called the only method.
*
* @return array<TestCaseFactory>
*/
private function testsUsingOnly(): array
{
return array_filter($this->state, function ($testFactory): bool {
return $testFactory->only;
});
}
/**
* Uses the given `$testCaseClass` on the given `$paths`.
*
@ -140,6 +166,14 @@ final class TestRepository
throw new TestAlreadyExist($test->filename, $test->description);
}
if (!$test->receivesArguments()) {
$arguments = Reflection::getFunctionArguments($test->test);
if (count($arguments) > 0) {
throw new DatasetMissing($test->filename, $test->description, $arguments);
}
}
$this->state[sprintf('%s%s%s', $test->filename, self::SEPARATOR, $test->description)] = $test;
}
}

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Pest\Support;
use Closure;
use ReflectionProperty;
use Throwable;
/**
@ -36,4 +37,30 @@ final class ExceptionTrace
throw $throwable;
}
}
/**
* Removes any item from the stack trace referencing Pest so as not to
* crowd the error log for the end user.
*/
public static function removePestReferences(Throwable $t): void
{
if (!property_exists($t, 'serializableTrace')) {
return;
}
$property = new ReflectionProperty($t, 'serializableTrace');
$property->setAccessible(true);
$trace = $property->getValue($t);
$cleanedTrace = [];
foreach ($trace as $item) {
if (key_exists('file', $item) && mb_strpos($item['file'], 'vendor/pestphp/pest/') > 0) {
continue;
}
$cleanedTrace[] = $item;
}
$property->setValue($t, $cleanedTrace);
}
}

View File

@ -4,7 +4,10 @@ declare(strict_types=1);
namespace Pest\Support;
use Closure;
use Pest\Expectation;
use Pest\PendingObjects\TestCall;
use PHPUnit\Framework\TestCase;
/**
* @internal
@ -32,7 +35,7 @@ final class HigherOrderCallables
*/
public function expect($value)
{
return new Expectation(is_callable($value) ? Reflection::bindCallable($value) : $value);
return new Expectation($value instanceof Closure ? Reflection::bindCallableWithData($value) : $value);
}
/**
@ -50,15 +53,13 @@ final class HigherOrderCallables
}
/**
* @template TValue
* Tap into the test case to perform an action and return the test case.
*
* @param callable(): TValue $callable
*
* @return TValue|object
* @return TestCall|TestCase|object
*/
public function tap(callable $callable)
{
Reflection::bindCallable($callable);
Reflection::bindCallableWithData($callable);
return $this->target;
}

View File

@ -34,18 +34,18 @@ final class HigherOrderMessage
public $line;
/**
* The method name.
* The method or property name to access.
*
* @readonly
*
* @var string
*/
public $methodName;
public $name;
/**
* The arguments.
*
* @var array<int, mixed>
* @var array<int, mixed>|null
*
* @readonly
*/
@ -61,13 +61,13 @@ final class HigherOrderMessage
/**
* Creates a new higher order message.
*
* @param array<int, mixed> $arguments
* @param array<int, mixed>|null $arguments
*/
public function __construct(string $filename, int $line, string $methodName, array $arguments)
public function __construct(string $filename, int $line, string $methodName, $arguments)
{
$this->filename = $filename;
$this->line = $line;
$this->methodName = $methodName;
$this->name = $methodName;
$this->arguments = $arguments;
}
@ -85,21 +85,23 @@ final class HigherOrderMessage
if ($this->hasHigherOrderCallable()) {
/* @phpstan-ignore-next-line */
return (new HigherOrderCallables($target))->{$this->methodName}(...$this->arguments);
return (new HigherOrderCallables($target))->{$this->name}(...$this->arguments);
}
try {
return Reflection::call($target, $this->methodName, $this->arguments);
return is_array($this->arguments)
? Reflection::call($target, $this->name, $this->arguments)
: $target->{$this->name}; /* @phpstan-ignore-line */
} catch (Throwable $throwable) {
Reflection::setPropertyValue($throwable, 'file', $this->filename);
Reflection::setPropertyValue($throwable, 'line', $this->line);
if ($throwable->getMessage() === self::getUndefinedMethodMessage($target, $this->methodName)) {
if ($throwable->getMessage() === self::getUndefinedMethodMessage($target, $this->name)) {
/** @var ReflectionClass $reflection */
$reflection = new ReflectionClass($target);
/* @phpstan-ignore-next-line */
$reflection = $reflection->getParentClass() ?: $reflection;
Reflection::setPropertyValue($throwable, 'message', sprintf('Call to undefined method %s::%s()', $reflection->getName(), $this->methodName));
Reflection::setPropertyValue($throwable, 'message', sprintf('Call to undefined method %s::%s()', $reflection->getName(), $this->name));
}
throw $throwable;
@ -125,7 +127,7 @@ final class HigherOrderMessage
*/
private function hasHigherOrderCallable()
{
return in_array($this->methodName, get_class_methods(HigherOrderCallables::class), true);
return in_array($this->name, get_class_methods(HigherOrderCallables::class), true);
}
private static function getUndefinedMethodMessage(object $target, string $methodName): string

View File

@ -17,21 +17,21 @@ final class HigherOrderMessageCollection
/**
* Adds a new higher order message to the collection.
*
* @param array<int, mixed> $arguments
* @param array<int, mixed>|null $arguments
*/
public function add(string $filename, int $line, string $methodName, array $arguments): void
public function add(string $filename, int $line, string $name, array $arguments = null): void
{
$this->messages[] = new HigherOrderMessage($filename, $line, $methodName, $arguments);
$this->messages[] = new HigherOrderMessage($filename, $line, $name, $arguments);
}
/**
* Adds a new higher order message to the collection if the callable condition is does not return false.
*
* @param array<int, mixed> $arguments
* @param array<int, mixed>|null $arguments
*/
public function addWhen(callable $condition, string $filename, int $line, string $methodName, array $arguments): void
public function addWhen(callable $condition, string $filename, int $line, string $name, array $arguments = null): void
{
$this->messages[] = (new HigherOrderMessage($filename, $line, $methodName, $arguments))->when($condition);
$this->messages[] = (new HigherOrderMessage($filename, $line, $name, $arguments))->when($condition);
}
/**
@ -53,4 +53,20 @@ final class HigherOrderMessageCollection
$message->call($target);
}
}
/**
* Count the number of messages with the given name.
*
* @param string $name A higher order message name (usually a method name)
*/
public function count(string $name): int
{
return array_reduce(
$this->messages,
static function (int $total, HigherOrderMessage $message) use ($name): int {
return $total + (int) ($name === $message->name);
},
0,
);
}
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Pest\Support;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use Throwable;
@ -17,16 +18,14 @@ final class HigherOrderTapProxy
/**
* The target being tapped.
*
* @var mixed
* @var TestCase
*/
public $target;
/**
* Create a new tap proxy instance.
*
* @param mixed $target
*/
public function __construct($target)
public function __construct(TestCase $target)
{
$this->target = $target;
}

View File

@ -12,6 +12,7 @@ use ReflectionException;
use ReflectionFunction;
use ReflectionNamedType;
use ReflectionParameter;
use ReflectionUnionType;
/**
* @internal
@ -60,6 +61,21 @@ final class Reflection
return Closure::fromCallable($callable)->bindTo(TestSuite::getInstance()->test)(...$args);
}
/**
* Bind a callable to the TestCase and return the result,
* passing in the current dataset values as arguments.
*
* @return mixed
*/
public static function bindCallableWithData(callable $callable)
{
$test = TestSuite::getInstance()->test;
return $test === null
? static::bindCallable($callable)
: Closure::fromCallable($callable)->bindTo($test)(...$test->getProvidedData());
}
/**
* Infers the file name from the given closure.
*/
@ -94,10 +110,6 @@ final class Reflection
}
}
if ($reflectionProperty === null) {
throw ShouldNotHappen::fromMessage('Reflection property not found.');
}
$reflectionProperty->setAccessible(true);
return $reflectionProperty->getValue($object);
@ -128,10 +140,6 @@ final class Reflection
}
}
if ($reflectionProperty === null) {
throw ShouldNotHappen::fromMessage('Reflection property not found.');
}
$reflectionProperty->setAccessible(true);
$reflectionProperty->setValue($object, $value);
}
@ -163,4 +171,37 @@ final class Reflection
return $name;
}
/**
* Receive a map of function argument names to their types.
*
* @return array<string, string>
*/
public static function getFunctionArguments(Closure $function): array
{
$parameters = (new ReflectionFunction($function))->getParameters();
$arguments = [];
foreach ($parameters as $parameter) {
/** @var ReflectionNamedType|ReflectionUnionType|null $types */
$types = ($parameter->hasType()) ? $parameter->getType() : null;
if (is_null($types)) {
$arguments[$parameter->getName()] = 'mixed';
continue;
}
$arguments[$parameter->getName()] = implode('|', array_map(
static function (ReflectionNamedType $type): string {
return $type->getName();
},
($types instanceof ReflectionNamedType)
? [$types] // NOTE: normalize as list of to handle unions
: $types->getTypes(),
));
}
return $arguments;
}
}

View File

@ -1,5 +1,5 @@
<?php
test('{name}', function () {
assertTrue(true);
expect(true)->toBeTrue();
});

View File

@ -96,11 +96,17 @@
✓ more than two datasets with (2) / (4) / (5)
✓ more than two datasets with (2) / (4) / (6)
✓ more than two datasets did the job right
✓ it can resolve a dataset after the test case is available with (Closure Object (...))
✓ it can resolve a dataset after the test case is available with shared yield sets with (Closure Object (...)) #1
✓ it can resolve a dataset after the test case is available with shared yield sets with (Closure Object (...)) #2
✓ it can resolve a dataset after the test case is available with shared array sets with (Closure Object (...)) #1
✓ it can resolve a dataset after the test case is available with shared array sets with (Closure Object (...)) #2
PASS Tests\Features\Exceptions
✓ it gives access the the underlying expectException
✓ it catch exceptions
✓ it catch exceptions and messages
✓ it can just define the message
PASS Tests\Features\Expect\HigherOrder\methods
✓ it can access methods
@ -112,11 +118,14 @@
✓ it works with sequence
✓ it can compose complex expectations
✓ it can handle nested method calls
✓ it works with higher order tests
PASS Tests\Features\Expect\HigherOrder\methodsAndProperties
✓ it can access methods and properties
✓ it can handle nested methods and properties
✓ it works with higher order tests
✓ it can start a new higher order expectation using the and syntax
✓ it can start a new higher order expectation using the and syntax in higher order tests
PASS Tests\Features\Expect\HigherOrder\properties
✓ it allows properties to be accessed from the value
@ -128,6 +137,7 @@
✓ it can compose complex expectations
✓ it works with objects
✓ it works with nested properties
✓ it works with higher order tests
PASS Tests\Features\Expect\each
✓ an exception is thrown if the the type is not iterable
@ -196,6 +206,20 @@
PASS Tests\Features\Expect\toBeFalse
✓ strict comparisons
✓ failures
✓ not failures
PASS Tests\Features\Expect\toBeFalsy
✓ passes as falsy with (false)
✓ passes as falsy with ('')
✓ passes as falsy with (null)
✓ passes as falsy with (0)
✓ passes as falsy with ('0')
✓ passes as not falsy with (true)
✓ passes as not falsy with (1) #1
✓ passes as not falsy with ('false')
✓ passes as not falsy with (1) #2
✓ passes as not falsy with (-1)
✓ failures
✓ not failures
PASS Tests\Features\Expect\toBeFile
@ -216,6 +240,11 @@
PASS Tests\Features\Expect\toBeGreatherThanOrEqual
✓ passes
✓ failures
✓ not failures
PASS Tests\Features\Expect\toBeIn
✓ passes
✓ failures
✓ not failures
PASS Tests\Features\Expect\toBeInfinite
@ -301,6 +330,20 @@
PASS Tests\Features\Expect\toBeTrue
✓ strict comparisons
✓ failures
✓ not failures
PASS Tests\Features\Expect\toBeTruthy
✓ passes as truthy with (true)
✓ passes as truthy with (1) #1
✓ passes as truthy with ('false')
✓ passes as truthy with (1) #2
✓ passes as truthy with (-1)
✓ passes as not truthy with (false)
✓ passes as not truthy with ('')
✓ passes as not truthy with (null)
✓ passes as not truthy with (0)
✓ passes as not truthy with ('0')
✓ failures
✓ not failures
PASS Tests\Features\Expect\toBeWritableDirectory
@ -315,9 +358,16 @@
PASS Tests\Features\Expect\toContain
✓ passes strings
✓ passes strings with multiple needles
✓ passes arrays
✓ passes arrays with multiple needles
✓ passes with array needles
✓ failures
✓ failures with multiple needles (all failing)
✓ failures with multiple needles (some failing)
✓ not failures
✓ not failures with multiple needles (all failing)
✓ not failures with multiple needles (some failing)
PASS Tests\Features\Expect\toEndWith
✓ pass
@ -367,6 +417,25 @@
PASS Tests\Features\Expect\toHaveKeys
✓ pass
✓ failures
✓ not failures
PASS Tests\Features\Expect\toHaveLength
✓ it passes with ('Fortaleza')
✓ it passes with ('Sollefteå')
✓ it passes with ('Ιεράπετρα')
✓ it passes with (stdClass Object (...))
✓ it passes with (Illuminate\Support\Collection Object (...))
✓ it passes with array
✓ it passes with *not*
✓ it properly fails with *not*
✓ it fails with (1)
✓ it fails with (1.5)
✓ it fails with (true)
✓ it fails with (null)
PASS Tests\Features\Expect\toHaveProperties
✓ pass
✓ failures
✓ not failures
PASS Tests\Features\Expect\toHaveProperty
@ -399,6 +468,19 @@
✓ failures
✓ not failures
PASS Tests\Features\Expect\toThrow
✓ passes
✓ failures 1
✓ failures 2
✓ failures 3
✓ failures 4
✓ failures 5
✓ failures 6
✓ failures 7
✓ not failures
✓ closure missing parameter
✓ closure missing type-hint
PASS Tests\Features\Helpers
✓ it can set/get properties on $this
✓ it throws error if property do not exist
@ -411,7 +493,12 @@
✓ it proxies calls to object
✓ it is capable doing multiple assertions
✓ it resolves expect callables correctly
✓ does not treat method names as callables
✓ it can tap into the test
✓ it can pass datasets into the expect callables with (1, 2, 3)
✓ it can pass datasets into the tap callable with (1, 2, 3)
✓ it can pass shared datasets into callables with (1)
✓ it can pass shared datasets into callables with (2)
WARN Tests\Features\Incompleted
… incompleted
@ -444,6 +531,8 @@
✓ it do not skips with falsy closure condition
- it skips with condition and message → skipped because foo
- it skips when skip after assertion
- it can use something in the test case as a condition → This test was skipped
- it can user higher order callables and skip
PASS Tests\Features\Test
✓ a test
@ -457,12 +546,14 @@
PASS Tests\Hooks\AfterAllTest
✓ global afterAll execution order
✓ it only gets called once per file
PASS Tests\Hooks\AfterEachTest
✓ global afterEach execution order
PASS Tests\Hooks\BeforeAllTest
✓ global beforeAll execution order
✓ it only gets called once per file
PASS Tests\Hooks\BeforeEachTest
✓ global beforeEach execution order
@ -554,10 +645,16 @@
PASS Tests\Unit\TestSuite
✓ it does not allow to add the same test description twice
✓ it alerts users about tests with arguments but no input
✓ it can return an array of all test suite filenames
✓ it can filter the test suite filenames to those with the only method
PASS Tests\Visual\Help
✓ visual snapshot of help command output
PASS Tests\Visual\JUnit
✓ it is can successfully call all public methods
PASS Tests\Visual\SingleTestOrDirectory
✓ allows to run a single test
✓ allows to run a directory
@ -567,6 +664,9 @@
WARN Tests\Visual\Success
- visual snapshot of test suite on success
PASS Tests\Visual\TeamCity
✓ it is can successfully call all public methods
PASS Tests\Features\Depends
✓ first
✓ second
@ -581,5 +681,5 @@
✓ it is a test
✓ it uses correct parent class
Tests: 4 incompleted, 7 skipped, 365 passed
Tests: 4 incompleted, 9 skipped, 447 passed

11
tests/Datasets/Bound.php Normal file
View File

@ -0,0 +1,11 @@
<?php
dataset('bound.closure', function () {
yield function () { return 1; };
yield function () { return 2; };
});
dataset('bound.array', [
function () { return 1; },
function () { return 2; },
]);

View File

@ -5,6 +5,10 @@ use Pest\Exceptions\DatasetAlreadyExist;
use Pest\Exceptions\DatasetDoesNotExist;
use Pest\Plugin;
beforeEach(function () {
$this->foo = 'bar';
});
it('throws exception if dataset does not exist', function () {
$this->expectException(DatasetDoesNotExist::class);
$this->expectExceptionMessage("A dataset with the name `first` does not exist. You can create it using `dataset('first', ['a', 'b']);`.");
@ -223,3 +227,17 @@ test('more than two datasets', function ($text_a, $text_b, $text_c) use ($state,
test('more than two datasets did the job right', function () use ($state) {
expect($state->text)->toBe('121212121212131423241314232411122122111221221112212213142324135136145146235236245246');
});
it('can resolve a dataset after the test case is available', function ($result) {
expect($result)->toBe('bar');
})->with([
function () { return $this->foo; },
]);
it('can resolve a dataset after the test case is available with shared yield sets', function ($result) {
expect($result)->toBeInt()->toBeLessThan(3);
})->with('bound.closure');
it('can resolve a dataset after the test case is available with shared array sets', function ($result) {
expect($result)->toBeInt()->toBeLessThan(3);
})->with('bound.array');

View File

@ -13,3 +13,7 @@ it('catch exceptions', function () {
it('catch exceptions and messages', function () {
throw new Exception('Something bad happened');
})->throws(Exception::class, 'Something bad happened');
it('can just define the message', function () {
throw new Exception('Something bad happened');
})->throws('Something bad happened');

View File

@ -67,6 +67,13 @@ it('can handle nested method calls', function () {
->books()->each->toBeArray();
});
it('works with higher order tests')
->expect(new HasMethods())
->newInstance()->newInstance()->name()->toEqual('Has Methods')->toBeString()
->newInstance()->name()->toEqual('Has Methods')->not->toBeArray
->name()->toEqual('Has Methods')
->books()->each->toBeArray;
class HasMethods
{
public function name()

View File

@ -22,6 +22,13 @@ it('can handle nested methods and properties', function () {
->newInstance()->books()->toBeArray();
});
it('works with higher order tests')
->expect(new HasMethodsAndProperties())
->meta->foo->bar->toBeString()->toEqual('baz')->not->toBeInt
->newInstance()->meta->foo->toBeArray
->newInstance()->multiply(2, 2)->toEqual(4)->not->toEqual(5)
->newInstance()->books()->toBeArray();
it('can start a new higher order expectation using the and syntax', function () {
expect(new HasMethodsAndProperties())
->toBeInstanceOf(HasMethodsAndProperties::class)
@ -33,6 +40,14 @@ it('can start a new higher order expectation using the and syntax', function ()
expect(static::getCount())->toEqual(4);
});
it('can start a new higher order expectation using the and syntax in higher order tests')
->expect(new HasMethodsAndProperties())
->toBeInstanceOf(HasMethodsAndProperties::class)
->meta->toBeArray
->and(['foo' => 'bar'])
->toBeArray()
->foo->toEqual('bar');
class HasMethodsAndProperties
{
public $name = 'Has Methods and Properties';

View File

@ -64,6 +64,11 @@ it('works with nested properties', function () {
->posts->toBeArray()->toHaveCount(2);
});
it('works with higher order tests')
->expect(new HasProperties())
->nested->foo->bar->toBeString()->toEqual('baz')
->posts->toBeArray()->toHaveCount(2);
class HasProperties
{
public $name = 'foo';

View File

@ -0,0 +1,19 @@
<?php
use PHPUnit\Framework\ExpectationFailedException;
test('passes as falsy', function ($value) {
expect($value)->toBeFalsy();
})->with([false, '', null, 0, '0']);
test('passes as not falsy', function ($value) {
expect($value)->not->toBeFalsy();
})->with([true, [1], 'false', 1, -1]);
test('failures', function () {
expect(1)->toBeFalsy();
})->throws(ExpectationFailedException::class);
test('not failures', function () {
expect(null)->not->toBeFalsy();
})->throws(ExpectationFailedException::class);

View File

@ -0,0 +1,16 @@
<?php
use PHPUnit\Framework\ExpectationFailedException;
test('passes', function () {
expect('a')->toBeIn(['a', 'b', 'c']);
expect('d')->not->toBeIn(['a', 'b', 'c']);
});
test('failures', function () {
expect('d')->toBeIn(['a', 'b', 'c']);
})->throws(ExpectationFailedException::class);
test('not failures', function () {
expect('a')->not->toBeIn(['a', 'b', 'c']);
})->throws(ExpectationFailedException::class);

View File

@ -0,0 +1,19 @@
<?php
use PHPUnit\Framework\ExpectationFailedException;
test('passes as truthy', function ($value) {
expect($value)->toBeTruthy();
})->with([true, [1], 'false', 1, -1]);
test('passes as not truthy', function ($value) {
expect($value)->not->toBeTruthy();
})->with([false, '', null, 0, '0']);
test('failures', function () {
expect(null)->toBeTruthy();
})->throws(ExpectationFailedException::class);
test('not failures', function () {
expect(1)->not->toBeTruthy();
})->throws(ExpectationFailedException::class);

View File

@ -3,17 +3,45 @@
use PHPUnit\Framework\ExpectationFailedException;
test('passes strings', function () {
expect([1, 2, 42])->toContain(42);
expect('Nuno')->toContain('Nu');
});
test('passes strings with multiple needles', function () {
expect('Nuno')->toContain('Nu', 'no');
});
test('passes arrays', function () {
expect('Nuno')->toContain('Nu');
expect([1, 2, 42])->toContain(42);
});
test('passes arrays with multiple needles', function () {
expect([1, 2, 42])->toContain(42, 2);
});
test('passes with array needles', function () {
expect([[1, 2, 3], 2, 42])->toContain(42, [1, 2, 3]);
});
test('failures', function () {
expect([1, 2, 42])->toContain(3);
})->throws(ExpectationFailedException::class);
test('failures with multiple needles (all failing)', function () {
expect([1, 2, 42])->toContain(3, 4);
})->throws(ExpectationFailedException::class);
test('failures with multiple needles (some failing)', function () {
expect([1, 2, 42])->toContain(1, 3, 4);
})->throws(ExpectationFailedException::class);
test('not failures', function () {
expect([1, 2, 42])->not->toContain(42);
})->throws(ExpectationFailedException::class);
test('not failures with multiple needles (all failing)', function () {
expect([1, 2, 42])->not->toContain(42, 2);
})->throws(ExpectationFailedException::class);
test('not failures with multiple needles (some failing)', function () {
expect([1, 2, 42])->not->toContain(42, 1);
})->throws(ExpectationFailedException::class);

View File

@ -0,0 +1,27 @@
<?php
use PHPUnit\Framework\ExpectationFailedException;
it('passes', function ($value) {
expect($value)->toHaveLength(9);
})->with([
'Fortaleza', 'Sollefteå', 'Ιεράπετρα',
(object) [1, 2, 3, 4, 5, 6, 7, 8, 9],
collect([1, 2, 3, 4, 5, 6, 7, 8, 9]),
]);
it('passes with array', function () {
expect([1, 2, 3])->toHaveLength(3);
});
it('passes with *not*', function () {
expect('')->not->toHaveLength(1);
});
it('properly fails with *not*', function () {
expect('pest')->not->toHaveLength(4);
})->throws(ExpectationFailedException::class);
it('fails', function ($value) {
expect($value)->toHaveLength(1);
})->with([1, 1.5, true, null])->throws(BadMethodCallException::class);

View File

@ -0,0 +1,26 @@
<?php
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function () {
$object = new stdClass();
$object->name = 'Jhon';
$object->age = 21;
expect($object)->toHaveProperties(['name', 'age']);
});
test('failures', function () {
$object = new stdClass();
$object->name = 'Jhon';
expect($object)->toHaveProperties(['name', 'age']);
})->throws(ExpectationFailedException::class);
test('not failures', function () {
$object = new stdClass();
$object->name = 'Jhon';
$object->age = 21;
expect($object)->not->toHaveProperties(['name', 'age']);
})->throws(ExpectationFailedException::class);

View File

@ -0,0 +1,60 @@
<?php
use PHPUnit\Framework\ExpectationFailedException;
test('passes', function () {
expect(function () { throw new RuntimeException(); })->toThrow(RuntimeException::class);
expect(function () { throw new RuntimeException(); })->toThrow(Exception::class);
expect(function () { throw new RuntimeException(); })->toThrow(function (RuntimeException $e) {});
expect(function () { throw new RuntimeException('actual message'); })->toThrow(function (Exception $e) {
expect($e->getMessage())->toBe('actual message');
});
expect(function () {})->not->toThrow(Exception::class);
expect(function () { throw new RuntimeException('actual message'); })->toThrow('actual message');
expect(function () { throw new Exception(); })->not->toThrow(RuntimeException::class);
expect(function () { throw new RuntimeException('actual message'); })->toThrow(RuntimeException::class, 'actual message');
expect(function () { throw new RuntimeException('actual message'); })->toThrow(function (RuntimeException $e) {}, 'actual message');
});
test('failures 1', function () {
expect(function () {})->toThrow(RuntimeException::class);
})->throws(ExpectationFailedException::class, 'Exception "' . RuntimeException::class . '" not thrown.');
test('failures 2', function () {
expect(function () {})->toThrow(function (RuntimeException $e) {});
})->throws(ExpectationFailedException::class, 'Exception "' . RuntimeException::class . '" not thrown.');
test('failures 3', function () {
expect(function () { throw new Exception(); })->toThrow(function (RuntimeException $e) {});
})->throws(ExpectationFailedException::class, 'Failed asserting that Exception Object');
test('failures 4', function () {
expect(function () { throw new Exception('actual message'); })
->toThrow(function (Exception $e) {
expect($e->getMessage())->toBe('expected message');
});
})->throws(ExpectationFailedException::class, 'Failed asserting that two strings are identical');
test('failures 5', function () {
expect(function () { throw new Exception('actual message'); })->toThrow('expected message');
})->throws(ExpectationFailedException::class, 'Failed asserting that \'actual message\' contains "expected message".');
test('failures 6', function () {
expect(function () {})->toThrow('actual message');
})->throws(ExpectationFailedException::class, 'Exception with message "actual message" not thrown');
test('failures 7', function () {
expect(function () { throw new RuntimeException('actual message'); })->toThrow(RuntimeException::class, 'expected message');
})->throws(ExpectationFailedException::class);
test('not failures', function () {
expect(function () { throw new RuntimeException(); })->not->toThrow(RuntimeException::class);
})->throws(ExpectationFailedException::class);
test('closure missing parameter', function () {
expect(function () {})->toThrow(function () {});
})->throws(InvalidArgumentException::class, 'The given closure must have a single parameter type-hinted as the class string.');
test('closure missing type-hint', function () {
expect(function () {})->toThrow(function ($e) {});
})->throws(InvalidArgumentException::class, 'The given closure\'s parameter must be type-hinted as the class string.');

View File

@ -18,10 +18,29 @@ it('resolves expect callables correctly')
->toBeString()
->toBe('bar');
test('does not treat method names as callables')
->expect('it')->toBeString();
it('can tap into the test')
->expect('foo')->toBeString()
->tap(function () { expect($this)->toBeInstanceOf(TestCase::class); })
->toBe('foo')
->and('hello world')->toBeString();
it('can pass datasets into the expect callables')
->with([[1, 2, 3]])
->expect(function (...$numbers) { return $numbers; })->toBe([1, 2, 3])
->and(function (...$numbers) { return $numbers; })->toBe([1, 2, 3]);
it('can pass datasets into the tap callable')
->with([[1, 2, 3]])
->tap(function (...$numbers) { expect($numbers)->toBe([1, 2, 3]); });
it('can pass shared datasets into callables')
->with('numbers.closure.wrapped')
->expect(function ($value) { return $value; })
->and(function ($value) { return $value; })
->tap(function ($value) { expect($value)->toBeInt(); })
->toBeInt();
afterEach()->assertTrue(true);

View File

@ -2,26 +2,50 @@
global $globalHook;
// NOTE: this test does not have a $globalHook->calls offset since it is first
// in the directory and thus will always run before the others. See also the
// BeforeAllTest.php for details.
uses()->afterAll(function () use ($globalHook) {
expect($globalHook)
->toHaveProperty('afterAll')
->and($globalHook->afterAll)
->toBe(0);
->toBe(0)
->and($globalHook->calls)
->afterAll
->toBe(1);
$globalHook->afterAll = 1;
$globalHook->calls->afterAll++;
});
afterAll(function () use ($globalHook) {
expect($globalHook)
->toHaveProperty('afterAll')
->and($globalHook->afterAll)
->toBe(1);
->toBe(1)
->and($globalHook->calls)
->afterAll
->toBe(2);
$globalHook->afterAll = 2;
$globalHook->calls->afterAll++;
});
test('global afterAll execution order', function () use ($globalHook) {
expect($globalHook)
->not()
->toHaveProperty('afterAll');
->toHaveProperty('afterAll')
->and($globalHook->calls)
->afterAll
->toBe(0);
});
it('only gets called once per file', function () use ($globalHook) {
expect($globalHook)
->not()
->toHaveProperty('afterAll')
->and($globalHook->calls)
->afterAll
->toBe(0);
});

View File

@ -1,28 +1,56 @@
<?php
use Pest\Support\Str;
global $globalHook;
uses()->beforeAll(function () use ($globalHook) {
// HACK: we have to determine our $globalHook->calls baseline. This is because
// two other tests are executed before this one due to filename ordering.
$args = $_SERVER['argv'] ?? [];
$single = (isset($args[1]) && Str::endsWith(__FILE__, $args[1])) || ($_SERVER['PEST_PARALLEL'] ?? false);
$offset = $single ? 0 : 2;
uses()->beforeAll(function () use ($globalHook, $offset) {
expect($globalHook)
->toHaveProperty('beforeAll')
->and($globalHook->beforeAll)
->toBe(0);
->toBe(0)
->and($globalHook->calls)
->beforeAll
->toBe(1 + $offset);
$globalHook->beforeAll = 1;
$globalHook->calls->beforeAll++;
});
beforeAll(function () use ($globalHook) {
beforeAll(function () use ($globalHook, $offset) {
expect($globalHook)
->toHaveProperty('beforeAll')
->and($globalHook->beforeAll)
->toBe(1);
->toBe(1)
->and($globalHook->calls)
->beforeAll
->toBe(2 + $offset);
$globalHook->beforeAll = 2;
$globalHook->calls->beforeAll++;
});
test('global beforeAll execution order', function () use ($globalHook) {
test('global beforeAll execution order', function () use ($globalHook, $offset) {
expect($globalHook)
->toHaveProperty('beforeAll')
->and($globalHook->beforeAll)
->toBe(2);
->toBe(2)
->and($globalHook->calls)
->beforeAll
->toBe(3 + $offset);
});
it('only gets called once per file', function () use ($globalHook, $offset) {
expect($globalHook)
->beforeAll
->toBe(2)
->and($globalHook->calls)
->beforeAll
->toBe(3 + $offset);
});

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Tests\SubFolder\SubFolder\SubFolder;
namespace Tests\CustomTestCaseInSubFolders\SubFolder\SubFolder;
use PHPUnit\Framework\TestCase;

View File

@ -1,3 +0,0 @@
<?php
uses(Tests\SubFolder\SubFolder\SubFolder\CustomTestCaseInSubFolder::class)->in('CustomTestCaseInSubFolders/SubFolder');

View File

@ -1,8 +1,13 @@
<?php
use Tests\CustomTestCaseInSubFolders\SubFolder\SubFolder\CustomTestCaseInSubFolder;
uses(CustomTestCaseInSubFolder::class)->in('PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder');
uses()->group('integration')->in('Visual');
$globalHook = (object) []; // NOTE: global test value container to be mutated and checked across files, as needed
// NOTE: global test value container to be mutated and checked across files, as needed
$globalHook = (object) ['calls' => (object) ['beforeAll' => 0, 'afterAll' => 0]];
uses()
->beforeEach(function () {
@ -10,11 +15,13 @@ uses()
})
->beforeAll(function () use ($globalHook) {
$globalHook->beforeAll = 0;
$globalHook->calls->beforeAll++;
})
->afterEach(function () {
$this->ith = 0;
})
->afterAll(function () use ($globalHook) {
$globalHook->afterAll = 0;
$globalHook->calls->afterAll++;
})
->in('Hooks');

View File

@ -1,5 +1,6 @@
<?php
use Pest\Exceptions\DatasetMissing;
use Pest\Exceptions\TestAlreadyExist;
use Pest\TestSuite;
@ -7,7 +8,44 @@ it('does not allow to add the same test description twice', function () {
$testSuite = new TestSuite(getcwd(), 'tests');
$test = function () {};
$testSuite->tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test));
$this->expectException(TestAlreadyExist::class);
$this->expectExceptionMessage(sprintf('A test with the description `%s` already exist in the filename `%s`.', 'foo', __FILE__));
$testSuite->tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test));
})->throws(
TestAlreadyExist::class,
sprintf('A test with the description `%s` already exist in the filename `%s`.', 'foo', __FILE__),
);
it('alerts users about tests with arguments but no input', function () {
$testSuite = new TestSuite(getcwd(), 'tests');
$test = function (int $arg) {};
$testSuite->tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test));
})->throws(
DatasetMissing::class,
sprintf("A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s", 'foo', 1, 'int $arg', __FILE__),
);
it('can return an array of all test suite filenames', function () {
$testSuite = new TestSuite(getcwd(), 'tests');
$test = function () {};
$testSuite->tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test));
$testSuite->tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'bar', $test));
expect($testSuite->tests->getFilenames())->toEqual([
__FILE__,
__FILE__,
]);
});
it('can filter the test suite filenames to those with the only method', function () {
$testSuite = new TestSuite(getcwd(), 'tests');
$test = function () {};
$testWithOnly = new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test);
$testWithOnly->only = true;
$testSuite->tests->set($testWithOnly);
$testSuite->tests->set(new \Pest\Factories\TestCaseFactory('Baz/Bar/Boo.php', 'bar', $test));
expect($testSuite->tests->getFilenames())->toEqual([
__FILE__,
]);
});

29
tests/Visual/JUnit.php Normal file
View File

@ -0,0 +1,29 @@
<?php
use Pest\Logging\JUnit;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Framework\Warning;
beforeEach(function () {
file_put_contents(__DIR__ . '/junit.html', '');
});
it('is can successfully call all public methods', function () {
$junit = new JUnit(__DIR__ . '/junit.html');
$junit->startTestSuite(new TestSuite());
$junit->startTest($this);
$junit->addError($this, new Exception(), 0);
$junit->addFailure($this, new AssertionFailedError(), 0);
$junit->addWarning($this, new Warning(), 0);
$junit->addIncompleteTest($this, new Exception(), 0);
$junit->addRiskyTest($this, new Exception(), 0);
$junit->addSkippedTest($this, new Exception(), 0);
$junit->endTest($this, 0);
$junit->endTestSuite(new TestSuite());
$this->expectNotToPerformAssertions();
});
afterEach(function () {
unlink(__DIR__ . '/junit.html');
});

View File

@ -9,7 +9,7 @@ test('visual snapshot of test suite on success', function () {
]);
$output = function () use ($testsPath) {
$process = (new Symfony\Component\Process\Process(['php', 'bin/pest'], dirname($testsPath), ['EXCLUDE' => 'integration', 'REBUILD_SNAPSHOTS' => false]));
$process = (new Symfony\Component\Process\Process(['php', 'bin/pest'], dirname($testsPath), ['EXCLUDE' => 'integration', 'REBUILD_SNAPSHOTS' => false, 'PARATEST' => 0]));
$process->run();

32
tests/Visual/TeamCity.php Normal file
View File

@ -0,0 +1,32 @@
<?php
use Pest\Logging\TeamCity;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\TestResult;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Framework\Warning;
use PHPUnit\TextUI\DefaultResultPrinter;
beforeEach(function () {
file_put_contents(__DIR__ . '/output.txt', '');
});
it('is can successfully call all public methods', function () {
$teamCity = new TeamCity(__DIR__ . '/output.txt', false, DefaultResultPrinter::COLOR_ALWAYS);
expect($teamCity::isPestTest($this))->toBeTrue();
$teamCity->startTestSuite(new TestSuite());
$teamCity->startTest($this);
$teamCity->addError($this, new Exception('Don\'t worry about this error. Its purposeful.'), 0);
$teamCity->addFailure($this, new AssertionFailedError('Don\'t worry about this error. Its purposeful.'), 0);
$teamCity->addWarning($this, new Warning(), 0);
$teamCity->addIncompleteTest($this, new Exception(), 0);
$teamCity->addRiskyTest($this, new Exception(), 0);
$teamCity->addSkippedTest($this, new Exception(), 0);
$teamCity->endTest($this, 0);
$teamCity->printResult(new TestResult());
$teamCity->endTestSuite(new TestSuite());
});
afterEach(function () {
unlink(__DIR__ . '/output.txt');
});