├── .gitignore ├── README.md ├── composer.json └── src └── Ozh └── PHPUnit └── Listener └── OverAssertiveTestsListener.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /composer.lock 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # phpunit-overassertive 2 | 3 | Having several assertions in the same test is fine, but when an assertion fails, 4 | the whole test aborts and other assertions in the same test are not tested. 5 | 6 | Depending on what you test and how you coded it, you may want to split some tests 7 | in several sub tests. 8 | 9 | **OverAssertive** is a PHPUnit extension that reports right in the console 10 | which tests have "too many" assertions, where "too many" is what you define, to 11 | help you inspect and maybe refactor some tests. 12 | 13 | ![overassertive](https://cloud.githubusercontent.com/assets/223647/7969423/0d90aaee-0a37-11e5-9f40-a7d29c613017.png) 14 | 15 | ## Usage 16 | 17 | Enable it with all defaults by adding the following to your test suite's `phpunit.xml` file: 18 | 19 | ```xml 20 | 21 | ... 22 | 23 | 24 | 25 | 26 | ``` 27 | 28 | If you're not using an autoloader you can also specify the library location: 29 | 30 | ```xml 31 | 32 | ... 33 | 34 | 35 | 36 | 37 | ``` 38 | 39 | Now run your test suite as normal. OverAssertive will report over assertive tests in the console after the suite completes. 40 | 41 | ## Configuration 42 | 43 | OverAssertive has two configurable parameters: 44 | 45 | * **alertThreshold** - Number of assertions that will make a test over assertive (default: 10 assertions) 46 | * **reportLength** - Number of over assertive tests included in the report (default: 10 tests) 47 | 48 | These configuration parameters are set in `phpunit.xml` when adding the listener: 49 | 50 | ```xml 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 10 60 | 61 | 62 | 10 63 | 64 | 65 | 66 | 67 | 68 | 69 | ``` 70 | 71 | ## Inspiration 72 | 73 | Much thanks to [phpunit-speedtrap](https://github.com/johnkary/phpunit-speedtrap) 74 | 75 | ## License 76 | 77 | Do whatever the hell you want to. 78 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ozh/phpunit-overassertive", 3 | "description": "Find over assertive tests in your PHPUnit test suite", 4 | "keywords": ["PHPUnit", "assertion", "listener"], 5 | "homepage": "https://github.com/ozh/phpunit-overassertive", 6 | "type": "library", 7 | "license": "WTFPL", 8 | "authors": [ 9 | { 10 | "name": "Ozh", 11 | "homepage": "http://ozh.org/" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=7" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": ">=7" 19 | }, 20 | "autoload": { 21 | "psr-0": { 22 | "ozh": "src/" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Ozh/PHPUnit/Listener/OverAssertiveTestsListener.php: -------------------------------------------------------------------------------- 1 | loadOptions($options); 52 | } 53 | 54 | /** 55 | * An error occurred. 56 | * 57 | * @param \PHPUnit\Framework\Test $test 58 | * @param Throwable $e 59 | * @param float $time 60 | */ 61 | public function addError(\PHPUnit\Framework\Test $test, \Throwable $e, $time) :void 62 | { 63 | } 64 | 65 | /** 66 | * A failure occurred. 67 | * 68 | * @param \PHPUnit\Framework\Test $test 69 | * @param \PHPUnit_Framework_AssertionFailedError $e 70 | * @param float $time 71 | */ 72 | public function addFailure(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\AssertionFailedError $e, $time) :void 73 | { 74 | } 75 | 76 | /** 77 | * Incomplete test. 78 | * 79 | * @param \PHPUnit\Framework\Test $test 80 | * @param Throwable $e 81 | * @param float $time 82 | */ 83 | public function addIncompleteTest(\PHPUnit\Framework\Test $test, \Throwable $e, $time) :void 84 | { 85 | } 86 | 87 | /** 88 | * Risky test. 89 | * 90 | * @param \PHPUnit\Framework\Test $test 91 | * @param Throwable $e 92 | * @param float $time 93 | * @since Method available since Release 4.0.0 94 | */ 95 | public function addRiskyTest(\PHPUnit\Framework\Test $test, \Throwable $e, $time) :void 96 | { 97 | } 98 | 99 | /** 100 | * Skipped test. 101 | * 102 | * @param \PHPUnit\Framework\Test $test 103 | * @param Throwable $e 104 | * @param float $time 105 | */ 106 | public function addSkippedTest(\PHPUnit\Framework\Test $test, \Throwable $e, $time) :void 107 | { 108 | } 109 | 110 | /** 111 | * Warning. 112 | * 113 | * @param \PHPUnit\Framework\Test $test 114 | * @param Throwable $e 115 | * @param float $time 116 | */ 117 | public function addWarning(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\Warning $e, $time) :void 118 | { 119 | } 120 | 121 | /** 122 | * A test started. 123 | * 124 | * @param \PHPUnit\Framework\Test $test 125 | */ 126 | public function startTest(\PHPUnit\Framework\Test $test) :void 127 | { 128 | } 129 | 130 | /** 131 | * A test ended. 132 | * 133 | * @param \PHPUnit\Framework\Test $test 134 | */ 135 | public function endTest(\PHPUnit\Framework\Test $test, $time) :void 136 | { 137 | if (!$test instanceof \PHPUnit\Framework\TestCase) return; 138 | 139 | $threshold = $this->alertThreshold; 140 | $assertions = \PHPUnit\Framework\Assert::getCount(); 141 | 142 | if ($assertions > $this->alertThreshold) { 143 | $this->addAssertiveTest($test, $assertions); 144 | } 145 | 146 | } 147 | 148 | /** 149 | * A test suite started. 150 | * 151 | * @param \PHPUnit\Framework\TestSuite $suite 152 | */ 153 | public function startTestSuite(\PHPUnit\Framework\TestSuite $suite) :void 154 | { 155 | $this->suites++; 156 | } 157 | 158 | /** 159 | * A test suite ended. 160 | * 161 | * @param \PHPUnit\Framework\TestSuite $suite 162 | */ 163 | public function endTestSuite(\PHPUnit\Framework\TestSuite $suite) :void 164 | { 165 | $this->suites--; 166 | 167 | if (0 === $this->suites && $this->hasAssertiveTest()) { 168 | arsort($this->assertive); // Sort most assertive tests to the top 169 | 170 | $this->renderHeader(); 171 | $this->renderBody(); 172 | $this->renderFooter(); 173 | } 174 | } 175 | 176 | /** 177 | * Stores a test as over assertive. 178 | * 179 | * @param \PHPUnit\Framework\TestCase $test 180 | * @param int $assertions Number of assertions 181 | */ 182 | protected function addAssertiveTest(\PHPUnit\Framework\TestCase $test, $assertions) :void 183 | { 184 | $label = $this->makeLabel($test); 185 | $this->assertive[$label] = $assertions; 186 | } 187 | 188 | /** 189 | * Whether at least one test has been considered over assertive. 190 | * 191 | * @return bool 192 | */ 193 | protected function hasAssertiveTest() 194 | { 195 | return !empty($this->assertive); 196 | } 197 | 198 | /** 199 | * Label for describing a test. 200 | * 201 | * @param \PHPUnit\Framework\TestCase $test 202 | * @return string 203 | */ 204 | protected function makeLabel(\PHPUnit\Framework\TestCase $test) 205 | { 206 | return sprintf('%s:%s', get_class($test), $test->getName()); 207 | } 208 | 209 | /** 210 | * Calculate number of over assertive tests to report about. 211 | * 212 | * @return int 213 | */ 214 | protected function getReportLength() 215 | { 216 | return min(count($this->assertive), $this->reportLength); 217 | } 218 | 219 | /** 220 | * Find how many over assertive tests occurred that won't be shown due to list length. 221 | * 222 | * @return int Number of hidden over assertive tests 223 | */ 224 | protected function getHiddenCount() 225 | { 226 | $total = count($this->assertive); 227 | $showing = $this->getReportLength($this->assertive); 228 | 229 | $hidden = 0; 230 | if ($total > $showing) { 231 | $hidden = $total - $showing; 232 | } 233 | 234 | return $hidden; 235 | } 236 | 237 | /** 238 | * Pluralize "assertion" if needed 239 | */ 240 | protected function pluralizeAssertion($count) 241 | { 242 | return $count <= 1 ? 'assertion' : 'assertions'; 243 | } 244 | 245 | 246 | /** 247 | * Renders test report header. 248 | */ 249 | protected function renderHeader() 250 | { 251 | echo sprintf("\n\n%s more than %s %s:\n", count($this->assertive) == 1 ? 'This test has' : 'These tests have', $this->alertThreshold, $this->pluralizeAssertion($this->alertThreshold)); 252 | } 253 | 254 | /** 255 | * Renders test report body. 256 | */ 257 | protected function renderBody() 258 | { 259 | $assertive = $this->assertive; 260 | $max = strlen(max($assertive)); 261 | 262 | $length = $this->getReportLength($assertive); 263 | for ($i = 1; $i <= $length; ++$i) { 264 | $label = key($assertive); 265 | $assertions = array_shift($assertive); 266 | $display = str_pad($assertions, $max, " ", STR_PAD_LEFT); 267 | $line = str_pad($i, strlen($length), " ", STR_PAD_LEFT); 268 | 269 | echo sprintf(" %s. %s %s in test %s\n", $line, $display, $this->pluralizeAssertion($assertions), $label); 270 | } 271 | } 272 | 273 | /** 274 | * Renders test report footer. 275 | */ 276 | protected function renderFooter() 277 | { 278 | if ($hidden = $this->getHiddenCount($this->assertive)) { 279 | echo sprintf("...and there %s %s more above your threshold hidden from view", $hidden == 1 ? 'is' : 'are', $hidden); 280 | } 281 | } 282 | 283 | /** 284 | * Populate options into class internals. 285 | * 286 | * @param array $options 287 | */ 288 | protected function loadOptions(array $options) 289 | { 290 | $this->alertThreshold = isset($options['alertThreshold']) ? $options['alertThreshold'] : 10; 291 | $this->reportLength = isset($options['reportLength']) ? $options['reportLength'] : 10; 292 | } 293 | 294 | } 295 | --------------------------------------------------------------------------------