diff --git a/README.md b/README.md index 4548f0d72..a663510d1 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,18 @@ includes:
+Do you use mocks in your PHPUnit tests? Enable mocking rules with single parameter: + +```yaml +parameters: + mocks: true +``` + +
+ But at start, make baby steps with one rule at a time: -Jump to: [Symfony-specific rules](#3-symfony-specific-rules), [Doctrine-specific rules](#2-doctrine-specific-rules) or [PHPUnit-specific rules](#4-phpunit-specific-rules). +Jump to: [Symfony-specific rules](#3-symfony-specific-rules), [Doctrine-specific rules](#2-doctrine-specific-rules), [PHPUnit-specific rules](#4-phpunit-specific-rules) or [PHPUnit mock rules](#5-phpunit-mock-rules).
@@ -2601,25 +2610,24 @@ return function (ContainerConfigurator $container) { ## 4. PHPUnit-specific Rules -### NoMockObjectAndRealObjectPropertyRule +### NoAssertFuncCallInTestsRule -Avoid using one property for both real object and mock object. Use separate properties or single type instead +Avoid using assert*() functions in tests, as they can lead to false positives ```yaml rules: - - Symplify\PHPStanRules\Rules\PHPUnit\NoMockObjectAndRealObjectPropertyRule + - Symplify\PHPStanRules\Rules\PHPUnit\NoAssertFuncCallInTestsRule ```
-### NoEntityMockingRule, NoDocumentMockingRule +### PublicStaticDataProviderRule -Instead of entity or document mocking, create object directly to get better type support +PHPUnit data provider method "%s" must be public ```yaml rules: - - Symplify\PHPStanRules\Rules\Doctrine\NoEntityMockingRule - - Symplify\PHPStanRules\Rules\Doctrine\NoDocumentMockingRule + - Symplify\PHPStanRules\Rules\PHPUnit\PublicStaticDataProviderRule ``` ```php @@ -2627,9 +2635,17 @@ use PHPUnit\Framework\TestCase; final class SomeTest extends TestCase { - public function test() + /** + * @dataProvider dataProvider + */ + public function test(): array { - $someEntityMock = $this->createMock(SomeEntity::class); + return []; + } + + protected function dataProvider(): array + { + return []; } } ``` @@ -2643,9 +2659,17 @@ use PHPUnit\Framework\TestCase; final class SomeTest extends TestCase { - public function test() + /** + * @dataProvider dataProvider + */ + public function test(): array { - $someEntityMock = new SomeEntity(); + return []; + } + + public static function dataProvider(): array + { + return []; } } ``` @@ -2654,13 +2678,17 @@ final class SomeTest extends TestCase
-### NoAssertFuncCallInTestsRule +## 5. PHPUnit Mock Rules -Avoid using assert*() functions in tests, as they can lead to false positives +* Do you use extensive mocking in your PHPUnit tests? +* Do you want to keep your tests clean, maintainable and avoid upgrade hell in the future? +* Do you want to have tests that actually test something? + +This set is for you! Enable all mocking rules with single parameter in your `phpstan.neon`: ```yaml -rules: - - Symplify\PHPStanRules\Rules\PHPUnit\NoAssertFuncCallInTestsRule +parameters: + mocks: true ```
@@ -2669,11 +2697,6 @@ rules: Test should have at least one non-mocked property, to test something -```yaml -rules: - - Symplify\PHPStanRules\Rules\PHPUnit\NoMockOnlyTestRule -``` - ```php use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -2717,31 +2740,47 @@ class SomeTest extends TestCase
-### PublicStaticDataProviderRule +### NoMockObjectAndRealObjectPropertyRule -PHPUnit data provider method "%s" must be public +Avoid using one property for both real object and mock object. Use separate properties or single type instead -```yaml -rules: - - Symplify\PHPStanRules\Rules\PHPUnit\PublicStaticDataProviderRule +```php +$this->service = $this->createMock(Service::class); +$this->service = new Service(); ``` +:x: + +
+ +```php +$this->someMock = $this->createMock(AnotherService::class); + +$this->realService = new Service(); +``` + +:+1: + +
+ +### NoDoubleConsecutiveTestMockRule + +Do not use `willReturnOnConsecutiveCalls()` and `willReturnCallback()` on the same mock. Use `willReturnCallback()` only instead to make the test more clear. + ```php use PHPUnit\Framework\TestCase; final class SomeTest extends TestCase { - /** - * @dataProvider dataProvider - */ - public function test(): array - { - return []; - } - - protected function dataProvider(): array + public function test() { - return []; + $this->createMock('SomeClass') + ->expects($this->exactly(2)) + ->method('someMethod') + ->willReturnCallback(function () { + return 'first'; + }) + ->willReturnOnConsecutiveCalls('first'); } } ``` @@ -2755,17 +2794,14 @@ use PHPUnit\Framework\TestCase; final class SomeTest extends TestCase { - /** - * @dataProvider dataProvider - */ - public function test(): array - { - return []; - } - - public static function dataProvider(): array + public function test() { - return []; + $this->createMock('SomeClass') + ->expects($this->exactly(2)) + ->method('someMethod') + ->willReturnCallback(function () { + return 'first'; + }); } } ``` @@ -2776,12 +2812,7 @@ final class SomeTest extends TestCase ### ExplicitExpectsMockMethodRule -PHPUnit mock method is missing explicit `expects()`, e.g. `$this->mock->expects($this->once())->...` - -```yaml -rules: - - Symplify\PHPStanRules\Rules\PHPUnit\ExplicitExpectsMockMethodRule -``` +PHPUnit mock method is missing explicit `expects()`, e.g. `$this->mock->expects($this->once())->...`. This is required since PHPUnit 12 to avoid silent stubs. ```php use PHPUnit\Framework\TestCase; @@ -2820,15 +2851,89 @@ final class SomeTest extends TestCase
-### NoDoubleConsecutiveTestMockRule +### AvoidAnyExpectsRule -Do not use `willReturnOnConsecutiveCalls()` and `willReturnCallback()` on the same mock. Use `willReturnCallback()` only instead to make the test more clear. +Disallow usage of `any()` expectation in mocks to ensure that all mock interactions are explicitly defined and verified. -```yaml -rules: - - Symplify\PHPStanRules\Rules\PHPUnit\NoDoubleConsecutiveTestMockRule +```php +$someMock = $this->createMock(Service::class); +$someMock->expects($this->any()) + ->method('calculate') + ->willReturn(10); +``` + +:x: + +
+ +```php +$someMock = $this->createMock(Service::class); +$someMock->expects($this->once()) + ->method('calculate') + ->willReturn(10); +``` + +:+1: + +
+ +### NoWithOnStubRule + +Disallow `with()` on stubs (mocks without `expects()`). PHPUnit deprecates `with()` on test stubs because they silently swallow argument mismatches. + +```php +$someMock = $this->createMock(Service::class); +$someMock->method('calculate') + ->with(10) + ->willReturn(20); +``` + +:x: + +
+ +```php +$someMock = $this->createMock(Service::class); +$someMock->expects($this->once()) + ->method('calculate') + ->with(10) + ->willReturn(20); ``` +:+1: + +
+ +### RequireAtLeastOneRule + +Disallow `atLeast(0)` on mock expectations, as it matches any number of calls (including zero) and provides no real verification. Require a value of `1` or higher. + +```php +$someMock = $this->createMock(Service::class); +$someMock->expects($this->atLeast(0)) + ->method('calculate') + ->willReturn(10); +``` + +:x: + +
+ +```php +$someMock = $this->createMock(Service::class); +$someMock->expects($this->atLeast(1)) + ->method('calculate') + ->willReturn(10); +``` + +:+1: + +
+ +### NoEntityMockingRule, NoDocumentMockingRule + +Instead of entity or document mocking, create object directly to get better type support + ```php use PHPUnit\Framework\TestCase; @@ -2836,13 +2941,7 @@ final class SomeTest extends TestCase { public function test() { - $this->createMock('SomeClass') - ->expects($this->exactly(2)) - ->method('someMethod') - ->willReturnCallback(function () { - return 'first'; - }) - ->willReturnOnConsecutiveCalls('first'); + $someEntityMock = $this->createMock(SomeEntity::class); } } ``` @@ -2858,12 +2957,7 @@ final class SomeTest extends TestCase { public function test() { - $this->createMock('SomeClass') - ->expects($this->exactly(2)) - ->method('someMethod') - ->willReturnCallback(function () { - return 'first'; - }); + $someEntityMock = new SomeEntity(); } } ``` diff --git a/composer.json b/composer.json index 39a8f78f5..fc1496520 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,8 @@ "phpstan": { "includes": [ "config/services/services.neon", - "config/ctor-rules.neon" + "config/ctor-rules.neon", + "config/mock-rules.neon" ] } } diff --git a/config/mock-rules.neon b/config/mock-rules.neon new file mode 100644 index 000000000..6e3db73d4 --- /dev/null +++ b/config/mock-rules.neon @@ -0,0 +1,48 @@ +# PHPUnit mocking rules, enable in your phpstan.neon with: +# +# parameters: +# mocks: true + +parameters: + mocks: false + +parametersSchema: + mocks: bool() + +conditionalTags: + # mocking + Symplify\PHPStanRules\Rules\PHPUnit\NoMockOnlyTestRule: + phpstan.rules.rule: %mocks% + Symplify\PHPStanRules\Rules\PHPUnit\NoMockObjectAndRealObjectPropertyRule: + phpstan.rules.rule: %mocks% + + # overly complicated + Symplify\PHPStanRules\Rules\PHPUnit\NoDoubleConsecutiveTestMockRule: + phpstan.rules.rule: %mocks% + + # explicit expects() + Symplify\PHPStanRules\Rules\PHPUnit\ExplicitExpectsMockMethodRule: + phpstan.rules.rule: %mocks% + Symplify\PHPStanRules\Rules\PHPUnit\AvoidAnyExpectsRule: + phpstan.rules.rule: %mocks% + Symplify\PHPStanRules\Rules\PHPUnit\NoWithOnStubRule: + phpstan.rules.rule: %mocks% + Symplify\PHPStanRules\Rules\PHPUnit\RequireAtLeastOneRule: + phpstan.rules.rule: %mocks% + + # better alternative than mocks + Symplify\PHPStanRules\Rules\Doctrine\NoDocumentMockingRule: + phpstan.rules.rule: %mocks% + Symplify\PHPStanRules\Rules\Doctrine\NoEntityMockingRule: + phpstan.rules.rule: %mocks% + +services: + - Symplify\PHPStanRules\Rules\PHPUnit\NoMockOnlyTestRule + - Symplify\PHPStanRules\Rules\PHPUnit\NoMockObjectAndRealObjectPropertyRule + - Symplify\PHPStanRules\Rules\PHPUnit\NoDoubleConsecutiveTestMockRule + - Symplify\PHPStanRules\Rules\PHPUnit\ExplicitExpectsMockMethodRule + - Symplify\PHPStanRules\Rules\PHPUnit\AvoidAnyExpectsRule + - Symplify\PHPStanRules\Rules\PHPUnit\NoWithOnStubRule + - Symplify\PHPStanRules\Rules\PHPUnit\RequireAtLeastOneRule + - Symplify\PHPStanRules\Rules\Doctrine\NoDocumentMockingRule + - Symplify\PHPStanRules\Rules\Doctrine\NoEntityMockingRule diff --git a/config/phpunit-rules.neon b/config/phpunit-rules.neon index c0c0ee353..78da5b9e5 100644 --- a/config/phpunit-rules.neon +++ b/config/phpunit-rules.neon @@ -2,13 +2,7 @@ rules: - Symplify\PHPStanRules\Rules\PHPUnit\PublicStaticDataProviderRule - Symplify\PHPStanRules\Rules\PHPUnit\NoAssertFuncCallInTestsRule - # mocking - - Symplify\PHPStanRules\Rules\PHPUnit\NoMockOnlyTestRule - - Symplify\PHPStanRules\Rules\Doctrine\NoDocumentMockingRule - - Symplify\PHPStanRules\Rules\Doctrine\NoEntityMockingRule - - Symplify\PHPStanRules\Rules\PHPUnit\NoMockObjectAndRealObjectPropertyRule - - Symplify\PHPStanRules\Rules\PHPUnit\NoDoubleConsecutiveTestMockRule - - # @todo test first - # ever method() must have expects() call to define how many times it is expected - # - Symplify\PHPStanRules\Rules\PHPUnit\ExplicitExpectsMockMethodRule +# mocking rules moved to mock-rules.neon, enable them with: +# +# parameters: +# mocks: true diff --git a/phpunit.xml b/phpunit.xml index 2dddc2d11..10abff925 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -15,5 +15,10 @@ tests/Rules/PHPUnit/NoMockOnlyTestRule/Fixture/SkipConstraintValidatorTest.php tests/Rules/PHPUnit/ExplicitExpectsMockMethodRule/Fixture/SkipMockWithExpectsTest.php tests/Rules/PHPUnit/ExplicitExpectsMockMethodRule/Fixture/MockWithoutExpectsTest.php + tests/Rules/PHPUnit/AvoidAnyExpectsRule/Fixture/SomeAnyTest.php + tests/Rules/PHPUnit/NoWithOnStubRule/Fixture/StubWithWithTest.php + tests/Rules/PHPUnit/NoWithOnStubRule/Fixture/SkipMockWithExpectsAndWithTest.php + tests/Rules/PHPUnit/RequireAtLeastOneRule/Fixture/AtLeastZeroTest.php + tests/Rules/PHPUnit/RequireAtLeastOneRule/Fixture/SkipAtLeastOneTest.php diff --git a/src/Enum/RuleIdentifier/PHPUnitRuleIdentifier.php b/src/Enum/RuleIdentifier/PHPUnitRuleIdentifier.php index 4e1e2591d..566aad3a0 100644 --- a/src/Enum/RuleIdentifier/PHPUnitRuleIdentifier.php +++ b/src/Enum/RuleIdentifier/PHPUnitRuleIdentifier.php @@ -17,4 +17,12 @@ final class PHPUnitRuleIdentifier public const string NO_ASSERT_FUNC_CALL_IN_TESTS = 'phpunit.noAssertFuncCallInTests'; public const string NO_DOUBLE_CONSECUTIVE_TEST_MOCK = 'phpunit.noDoubleConsecutiveTestMock'; + + public const string EXPLICIT_EXPECTS_MOCK_METHOD = 'phpunit.explicitExpectsMockMethod'; + + public const string AVOID_ANY_EXPECTS = 'phpunit.avoidAnyExpects'; + + public const string NO_WITH_ON_STUB = 'phpunit.noWithOnStub'; + + public const string REQUIRE_AT_LEAST_ONE = 'phpunit.requireAtLeastOne'; } diff --git a/src/Rules/PHPUnit/AvoidAnyExpectsRule.php b/src/Rules/PHPUnit/AvoidAnyExpectsRule.php new file mode 100644 index 000000000..1c499a6ae --- /dev/null +++ b/src/Rules/PHPUnit/AvoidAnyExpectsRule.php @@ -0,0 +1,61 @@ + + * + * @see \Symplify\PHPStanRules\Tests\Rules\PHPUnit\AvoidAnyExpectsRule\AvoidAnyExpectsRuleTest + */ +final class AvoidAnyExpectsRule implements Rule +{ + public const string ERROR_MESSAGE = 'Using $this->any() on mock is ambigous. Use explicit count or change to a stub'; + + public function getNodeType(): string + { + return MethodCall::class; + } + + /** + * @param MethodCall $node + * @return IdentifierRuleError[] + */ + public function processNode(Node $node, Scope $scope): array + { + if (! TestClassDetector::isTestClass($scope)) { + return []; + } + + if (! NamingHelper::isName($node->name, 'expects')) { + return []; + } + + $firstArg = $node->getArgs()[0]; + if (! $firstArg->value instanceof MethodCall) { + return []; + } + + $nestedCall = $firstArg->value; + if (! NamingHelper::isName($nestedCall->name, 'any')) { + return []; + } + + $identifierRuleError = RuleErrorBuilder::message(self::ERROR_MESSAGE) + ->identifier(PHPUnitRuleIdentifier::AVOID_ANY_EXPECTS) + ->build(); + + return [$identifierRuleError]; + } +} diff --git a/src/Rules/PHPUnit/ExplicitExpectsMockMethodRule.php b/src/Rules/PHPUnit/ExplicitExpectsMockMethodRule.php index 1c18d278a..f86805f30 100644 --- a/src/Rules/PHPUnit/ExplicitExpectsMockMethodRule.php +++ b/src/Rules/PHPUnit/ExplicitExpectsMockMethodRule.php @@ -54,7 +54,7 @@ public function processNode(Node $node, Scope $scope): array } $identifierRuleError = RuleErrorBuilder::message(self::ERROR_MESSAGE) - ->identifier(PHPUnitRuleIdentifier::NO_ASSERT_FUNC_CALL_IN_TESTS) + ->identifier(PHPUnitRuleIdentifier::EXPLICIT_EXPECTS_MOCK_METHOD) ->build(); return [$identifierRuleError]; diff --git a/src/Rules/PHPUnit/NoWithOnStubRule.php b/src/Rules/PHPUnit/NoWithOnStubRule.php new file mode 100644 index 000000000..67a82fb43 --- /dev/null +++ b/src/Rules/PHPUnit/NoWithOnStubRule.php @@ -0,0 +1,75 @@ + + * + * @see \Symplify\PHPStanRules\Tests\Rules\PHPUnit\NoWithOnStubRule\NoWithOnStubRuleTest + */ +final class NoWithOnStubRule implements Rule +{ + public const string ERROR_MESSAGE = 'Using with() on a stub is misleading and deprecated by PHPUnit. Use explicit expects() to turn it into a mock, or drop with()'; + + public function getNodeType(): string + { + return MethodCall::class; + } + + /** + * @param MethodCall $node + * @return IdentifierRuleError[] + */ + public function processNode(Node $node, Scope $scope): array + { + if (! NamingHelper::isName($node->name, 'with')) { + return []; + } + + if (! TestClassDetector::isTestClass($scope)) { + return []; + } + + if (! $node->var instanceof MethodCall) { + return []; + } + + $methodCall = $node->var; + if (! NamingHelper::isName($methodCall->name, 'method')) { + return []; + } + + if ($methodCall->var instanceof MethodCall && NamingHelper::isName($methodCall->var->name, 'expects')) { + return []; + } + + if (! $methodCall->var instanceof Variable && ! $methodCall->var instanceof PropertyFetch) { + return []; + } + + $callerType = $scope->getType($methodCall->var); + if (! $callerType->hasMethod('expects')->yes()) { + return []; + } + + $identifierRuleError = RuleErrorBuilder::message(self::ERROR_MESSAGE) + ->identifier(PHPUnitRuleIdentifier::NO_WITH_ON_STUB) + ->build(); + + return [$identifierRuleError]; + } +} diff --git a/src/Rules/PHPUnit/RequireAtLeastOneRule.php b/src/Rules/PHPUnit/RequireAtLeastOneRule.php new file mode 100644 index 000000000..b4a33ecd0 --- /dev/null +++ b/src/Rules/PHPUnit/RequireAtLeastOneRule.php @@ -0,0 +1,66 @@ + + * + * @see \Symplify\PHPStanRules\Tests\Rules\PHPUnit\RequireAtLeastOneRule\RequireAtLeastOneRuleTest + */ +final class RequireAtLeastOneRule implements Rule +{ + public const string ERROR_MESSAGE = 'Using $this->atLeast(0) is meaningless, as it matches any number of calls. Use 1 or higher'; + + public function getNodeType(): string + { + return MethodCall::class; + } + + /** + * @param MethodCall $node + * @return IdentifierRuleError[] + */ + public function processNode(Node $node, Scope $scope): array + { + if (! TestClassDetector::isTestClass($scope)) { + return []; + } + + if (! NamingHelper::isName($node->name, 'atLeast')) { + return []; + } + + $args = $node->getArgs(); + if ($args === []) { + return []; + } + + $firstArgValue = $args[0]->value; + if (! $firstArgValue instanceof Int_) { + return []; + } + + if ($firstArgValue->value >= 1) { + return []; + } + + $identifierRuleError = RuleErrorBuilder::message(self::ERROR_MESSAGE) + ->identifier(PHPUnitRuleIdentifier::REQUIRE_AT_LEAST_ONE) + ->build(); + + return [$identifierRuleError]; + } +} diff --git a/tests/Rules/PHPUnit/AvoidAnyExpectsRule/AvoidAnyExpectsRuleTest.php b/tests/Rules/PHPUnit/AvoidAnyExpectsRule/AvoidAnyExpectsRuleTest.php new file mode 100644 index 000000000..f70c03adc --- /dev/null +++ b/tests/Rules/PHPUnit/AvoidAnyExpectsRule/AvoidAnyExpectsRuleTest.php @@ -0,0 +1,36 @@ +> $expectedErrorsWithLines + */ + #[DataProvider('provideData')] + public function testRule(string $filePath, array $expectedErrorsWithLines): void + { + $this->analyse([$filePath], $expectedErrorsWithLines); + } + + /** + * @return Iterator, mixed>> + */ + public static function provideData(): Iterator + { + yield [__DIR__ . '/Fixture/SomeAnyTest.php', [[AvoidAnyExpectsRule::ERROR_MESSAGE, 13]]]; + } + + protected function getRule(): Rule + { + return new AvoidAnyExpectsRule(); + } +} diff --git a/tests/Rules/PHPUnit/AvoidAnyExpectsRule/Fixture/SomeAnyTest.php b/tests/Rules/PHPUnit/AvoidAnyExpectsRule/Fixture/SomeAnyTest.php new file mode 100644 index 000000000..3c5f8e267 --- /dev/null +++ b/tests/Rules/PHPUnit/AvoidAnyExpectsRule/Fixture/SomeAnyTest.php @@ -0,0 +1,17 @@ +createMock(\stdClass::class); + + $mock->expects($this->any()) + ->method('someMethod') + ->willReturn('value'); + } +} diff --git a/tests/Rules/PHPUnit/NoWithOnStubRule/Fixture/SkipMockWithExpectsAndWithTest.php b/tests/Rules/PHPUnit/NoWithOnStubRule/Fixture/SkipMockWithExpectsAndWithTest.php new file mode 100644 index 000000000..ab53a989f --- /dev/null +++ b/tests/Rules/PHPUnit/NoWithOnStubRule/Fixture/SkipMockWithExpectsAndWithTest.php @@ -0,0 +1,18 @@ +createMock(\stdClass::class); + + $mock->expects($this->once()) + ->method('someMethod') + ->with('arg') + ->willReturn('value'); + } +} diff --git a/tests/Rules/PHPUnit/NoWithOnStubRule/Fixture/StubWithWithTest.php b/tests/Rules/PHPUnit/NoWithOnStubRule/Fixture/StubWithWithTest.php new file mode 100644 index 000000000..cbcf1cc6a --- /dev/null +++ b/tests/Rules/PHPUnit/NoWithOnStubRule/Fixture/StubWithWithTest.php @@ -0,0 +1,17 @@ +createMock(\stdClass::class); + + $mock->method('someMethod') + ->with('arg') + ->willReturn('value'); + } +} diff --git a/tests/Rules/PHPUnit/NoWithOnStubRule/NoWithOnStubRuleTest.php b/tests/Rules/PHPUnit/NoWithOnStubRule/NoWithOnStubRuleTest.php new file mode 100644 index 000000000..d449f17a8 --- /dev/null +++ b/tests/Rules/PHPUnit/NoWithOnStubRule/NoWithOnStubRuleTest.php @@ -0,0 +1,38 @@ +> $expectedErrorsWithLines + */ + #[DataProvider('provideData')] + public function testRule(string $filePath, array $expectedErrorsWithLines): void + { + $this->analyse([$filePath], $expectedErrorsWithLines); + } + + /** + * @return Iterator, mixed>> + */ + public static function provideData(): Iterator + { + yield [__DIR__ . '/Fixture/StubWithWithTest.php', [[NoWithOnStubRule::ERROR_MESSAGE, 13]]]; + + yield [__DIR__ . '/Fixture/SkipMockWithExpectsAndWithTest.php', []]; + } + + protected function getRule(): Rule + { + return new NoWithOnStubRule(); + } +} diff --git a/tests/Rules/PHPUnit/RequireAtLeastOneRule/Fixture/AtLeastZeroTest.php b/tests/Rules/PHPUnit/RequireAtLeastOneRule/Fixture/AtLeastZeroTest.php new file mode 100644 index 000000000..5d23aa939 --- /dev/null +++ b/tests/Rules/PHPUnit/RequireAtLeastOneRule/Fixture/AtLeastZeroTest.php @@ -0,0 +1,17 @@ +createMock(\stdClass::class); + + $mock->expects($this->atLeast(0)) + ->method('someMethod') + ->willReturn('value'); + } +} diff --git a/tests/Rules/PHPUnit/RequireAtLeastOneRule/Fixture/SkipAtLeastOneTest.php b/tests/Rules/PHPUnit/RequireAtLeastOneRule/Fixture/SkipAtLeastOneTest.php new file mode 100644 index 000000000..5ad46249d --- /dev/null +++ b/tests/Rules/PHPUnit/RequireAtLeastOneRule/Fixture/SkipAtLeastOneTest.php @@ -0,0 +1,22 @@ +createMock(\stdClass::class); + + $mock->expects($this->atLeast(1)) + ->method('someMethod') + ->willReturn('value'); + + $anotherMock = $this->createMock(\stdClass::class); + $anotherMock->expects($this->atLeast(3)) + ->method('anotherMethod') + ->willReturn('value'); + } +} diff --git a/tests/Rules/PHPUnit/RequireAtLeastOneRule/RequireAtLeastOneRuleTest.php b/tests/Rules/PHPUnit/RequireAtLeastOneRule/RequireAtLeastOneRuleTest.php new file mode 100644 index 000000000..a7284586e --- /dev/null +++ b/tests/Rules/PHPUnit/RequireAtLeastOneRule/RequireAtLeastOneRuleTest.php @@ -0,0 +1,37 @@ +> $expectedErrorsWithLines + */ + #[DataProvider('provideData')] + public function testRule(string $filePath, array $expectedErrorsWithLines): void + { + $this->analyse([$filePath], $expectedErrorsWithLines); + } + + /** + * @return Iterator, mixed>> + */ + public static function provideData(): Iterator + { + yield [__DIR__ . '/Fixture/AtLeastZeroTest.php', [[RequireAtLeastOneRule::ERROR_MESSAGE, 13]]]; + yield [__DIR__ . '/Fixture/SkipAtLeastOneTest.php', []]; + } + + protected function getRule(): Rule + { + return new RequireAtLeastOneRule(); + } +}