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();
+ }
+}