Skip to content

Commit 5100e2f

Browse files
authored
Merge pull request #6151 from Catrobat/release/v26.1.6
Release v26.1.6 into main
2 parents 230835c + 65dfffc commit 5100e2f

File tree

409 files changed

+15022
-6996
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

409 files changed

+15022
-6996
lines changed

.claude/CLAUDE.md

Lines changed: 607 additions & 0 deletions
Large diffs are not rendered by default.

.claude/CLAUDE_PHPUNIT_MOCKING.md

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
# PHPUnit Testing Best Practices - Mocks vs Stubs
2+
3+
## Overview
4+
5+
This document explains proper usage of test doubles in PHPUnit tests. PHPUnit 12+ strictly enforces best practices to help developers write clearer, more maintainable tests.
6+
7+
## The Problem
8+
9+
You may see this warning:
10+
11+
```
12+
No expectations were configured for the mock object for ClassName.
13+
Consider refactoring your test code to use a test stub instead.
14+
```
15+
16+
This means you're using `createMock()` but not actually verifying behavior with `expects()`. This is a code smell - you should use `createStub()` instead.
17+
18+
## Mocks vs Stubs - When to Use Which
19+
20+
### Use `createStub()` when:
21+
22+
- You need a dependency to return specific values
23+
- You're **NOT** verifying that methods were called
24+
- The dependency is just providing data for the test
25+
- You only care about **WHAT** is returned, not **HOW** it's called
26+
27+
### Use `createMock()` when:
28+
29+
- You're **verifying behavior** (that specific methods are called)
30+
- You use `expects()` to assert interactions
31+
- The test is about **HOW** the code interacts with dependencies
32+
- You care about **method calls, arguments, and order**
33+
34+
## Common Patterns
35+
36+
### Pattern 1: Repository as Stub (No Behavior Verification)
37+
38+
```php
39+
use PHPUnit\Framework\MockObject\Stub;
40+
41+
class UserServiceTest extends TestCase
42+
{
43+
private Stub|UserRepository $repository;
44+
45+
protected function setUp(): void
46+
{
47+
// GOOD - Using a stub for dependencies that just return data
48+
$this->repository = $this->createStub(UserRepository::class);
49+
$this->repository->method('find')->willReturn(new User());
50+
$this->service = new UserService($this->repository);
51+
}
52+
53+
public function testGetUserName(): void
54+
{
55+
// We don't care HOW the repository was called, just that we get data
56+
$name = $this->service->getUserName(123);
57+
$this->assertEquals('John', $name);
58+
}
59+
}
60+
```
61+
62+
### Pattern 2: Mock When Verifying Behavior
63+
64+
```php
65+
use PHPUnit\Framework\MockObject\MockObject;
66+
67+
class UserServiceTest extends TestCase
68+
{
69+
public function testUserIsSaved(): void
70+
{
71+
// GOOD - Using a mock to verify the method was called correctly
72+
$repository = $this->createMock(UserRepository::class);
73+
$repository->expects($this->once()) // ← This is WHY we use createMock()
74+
->method('save')
75+
->with($this->isInstanceOf(User::class));
76+
77+
$service = new UserService($repository);
78+
$service->createUser('John');
79+
80+
// The assertion happens via expects() - we're testing BEHAVIOR
81+
}
82+
}
83+
```
84+
85+
### Pattern 3: Mixed Approach (Stub by Default, Mock When Needed)
86+
87+
```php
88+
use PHPUnit\Framework\MockObject\Stub;
89+
use PHPUnit\Framework\MockObject\MockObject;
90+
91+
class TranslateClientTest extends TestCase
92+
{
93+
private Stub|TranslateClient $client;
94+
private GoogleTranslateApi $api;
95+
96+
protected function setUp(): void
97+
{
98+
// Stub by default - many tests don't need to verify behavior
99+
$this->client = $this->createStub(TranslateClient::class);
100+
$this->api = new GoogleTranslateApi($this->client, $this->createStub(LoggerInterface::class), 5);
101+
}
102+
103+
public function testTranslatesText(): void
104+
{
105+
// For this test, we need to verify the client is called correctly
106+
$client = $this->createMock(TranslateClient::class);
107+
$client->expects($this->once())
108+
->method('translate')
109+
->with('hello', ['target' => 'fr'])
110+
->willReturn(['text' => 'bonjour']);
111+
112+
$api = new GoogleTranslateApi($client, $this->createStub(LoggerInterface::class), 5);
113+
$result = $api->translate('hello', null, 'fr');
114+
115+
$this->assertEquals('bonjour', $result->translation);
116+
}
117+
118+
public function testGetPreference(): void
119+
{
120+
// This test doesn't call the client at all, so the stub from setUp() is fine
121+
$preference = $this->api->getPreference('test', 'en', 'fr');
122+
$this->assertEquals(1.0, $preference);
123+
}
124+
}
125+
```
126+
127+
### Pattern 4: Type Hints Show Intent
128+
129+
```php
130+
use PHPUnit\Framework\MockObject\MockObject;
131+
use PHPUnit\Framework\MockObject\Stub;
132+
133+
class MyTest extends TestCase
134+
{
135+
// Clear signal: this is a stub (returns values, no behavior verification)
136+
private Stub|UserRepository $userRepository;
137+
private Stub|LoggerInterface $logger;
138+
139+
// Clear signal: these are mocks (behavior will be verified)
140+
private MockObject|EmailService $emailService;
141+
private MockObject|EventDispatcher $eventDispatcher;
142+
143+
protected function setUp(): void
144+
{
145+
// Stubs - just return values
146+
$this->userRepository = $this->createStub(UserRepository::class);
147+
$this->logger = $this->createStub(LoggerInterface::class);
148+
149+
// Mocks - we'll use expects() on these
150+
$this->emailService = $this->createMock(EmailService::class);
151+
$this->eventDispatcher = $this->createMock(EventDispatcher::class);
152+
}
153+
}
154+
```
155+
156+
## What NOT to Do
157+
158+
### ❌ BAD: Creating mocks without expectations
159+
160+
```php
161+
// BAD - createMock() without expects()
162+
protected function setUp(): void
163+
{
164+
$this->logger = $this->createMock(LoggerInterface::class); // ← Wrong!
165+
$this->repository = $this->createMock(UserRepository::class); // ← Wrong!
166+
}
167+
168+
public function testSomething(): void
169+
{
170+
// No expects() anywhere - these should be stubs!
171+
$result = $this->service->doSomething();
172+
$this->assertTrue($result);
173+
}
174+
```
175+
176+
### ❌ BAD: Using opt-out attribute instead of proper refactoring
177+
178+
```php
179+
// BAD - This hides the problem instead of fixing it
180+
#[AllowMockObjectsWithoutExpectations]
181+
public function testSomething(): void
182+
{
183+
$logger = $this->createMock(LoggerInterface::class); // ← Should be createStub()!
184+
// ...
185+
}
186+
```
187+
188+
### ✅ GOOD: Use the right tool for the job
189+
190+
```php
191+
// GOOD - Using stubs for dependencies that just return values
192+
protected function setUp(): void
193+
{
194+
$this->logger = $this->createStub(LoggerInterface::class); // ← Correct!
195+
$this->repository = $this->createStub(UserRepository::class); // ← Correct!
196+
}
197+
198+
public function testSomething(): void
199+
{
200+
$result = $this->service->doSomething();
201+
$this->assertTrue($result);
202+
}
203+
```
204+
205+
## Refactoring Checklist
206+
207+
When you see PHPUnit warnings about mocks without expectations:
208+
209+
1. **Identify the dependency** - What object is triggering the warning?
210+
211+
2. **Search for `expects()` in the test file**
212+
- If NO `expects()` anywhere → Change ALL `createMock()` to `createStub()`
213+
- If `expects()` in SOME tests → Use Pattern 3 (stub by default, mock in specific tests)
214+
215+
3. **Update type hints**
216+
217+
```php
218+
// From:
219+
private MockObject|Logger $logger;
220+
221+
// To:
222+
private Stub|Logger $logger;
223+
```
224+
225+
4. **Update imports** if needed
226+
227+
```php
228+
use PHPUnit\Framework\MockObject\Stub;
229+
```
230+
231+
5. **Run tests** to verify everything still passes
232+
233+
6. **Remove any `#[AllowMockObjectsWithoutExpectations]` attributes**
234+
235+
## Examples from the Codebase
236+
237+
### Example 1: TranslationDelegateTest (Properly Refactored)
238+
239+
**Before:**
240+
241+
```php
242+
private MockObject|ProjectCustomTranslationRepository $repository;
243+
244+
protected function setUp(): void
245+
{
246+
$this->repository = $this->createMock(ProjectCustomTranslationRepository::class);
247+
}
248+
249+
#[AllowMockObjectsWithoutExpectations]
250+
public function testTranslate(): void
251+
{
252+
// No expects() used
253+
$result = $this->delegate->translate('test', 'en', 'fr');
254+
$this->assertNotNull($result);
255+
}
256+
```
257+
258+
**After:**
259+
260+
```php
261+
private Stub|ProjectCustomTranslationRepository $repository;
262+
263+
protected function setUp(): void
264+
{
265+
$this->repository = $this->createStub(ProjectCustomTranslationRepository::class);
266+
}
267+
268+
public function testTranslate(): void
269+
{
270+
// Clean - no warning, clear intent
271+
$result = $this->delegate->translate('test', 'en', 'fr');
272+
$this->assertNotNull($result);
273+
}
274+
275+
public function testAddTranslation(): void
276+
{
277+
// For this test, we need to verify behavior
278+
$repository = $this->createMock(ProjectCustomTranslationRepository::class);
279+
$repository->expects($this->once())
280+
->method('addNameTranslation')
281+
->with($project, 'fr', 'test');
282+
283+
$delegate = new TranslationDelegate($repository, ...);
284+
$delegate->addProjectNameCustomTranslation($project, 'fr', 'test');
285+
}
286+
```
287+
288+
### Example 2: GoogleTranslateApiTest (Properly Refactored)
289+
290+
**Before:**
291+
292+
```php
293+
private MockObject|TranslateClient $client;
294+
295+
protected function setUp(): void
296+
{
297+
$this->client = $this->createMock(TranslateClient::class);
298+
$this->api = new GoogleTranslateApi($this->client, ...);
299+
}
300+
301+
#[AllowMockObjectsWithoutExpectations]
302+
public function testGetPreference(): void
303+
{
304+
// Doesn't use $this->client at all!
305+
$this->assertEquals(1.0, $this->api->getPreference('test', 'en', 'fr'));
306+
}
307+
```
308+
309+
**After:**
310+
311+
```php
312+
private Stub|TranslateClient $client;
313+
314+
protected function setUp(): void
315+
{
316+
$this->client = $this->createStub(TranslateClient::class);
317+
$this->api = new GoogleTranslateApi($this->client, ...);
318+
}
319+
320+
public function testGetPreference(): void
321+
{
322+
// Clean - no warning
323+
$this->assertEquals(1.0, $this->api->getPreference('test', 'en', 'fr'));
324+
}
325+
326+
public function testTranslate(): void
327+
{
328+
// Create a mock when we need to verify behavior
329+
$client = $this->createMock(TranslateClient::class);
330+
$client->expects($this->once())->method('translate')->willReturn(['text' => 'bonjour']);
331+
$api = new GoogleTranslateApi($client, ...);
332+
333+
$result = $api->translate('hello', null, 'fr');
334+
$this->assertEquals('bonjour', $result->translation);
335+
}
336+
```
337+
338+
## Why This Matters
339+
340+
1. **Test Clarity** - Stubs vs mocks makes intent explicit and tests easier to understand
341+
2. **Maintainability** - Future developers immediately understand what's being tested
342+
3. **Best Practices** - Aligns with modern PHP/PHPUnit standards
343+
4. **Faster Tests** - Stubs are lighter weight than mocks
344+
345+
## Summary
346+
347+
- **Stub** = "I need this dependency to return a value"
348+
- **Mock** = "I need to verify this dependency was used correctly"
349+
350+
When in doubt, start with a stub. Only upgrade to a mock when you need to verify behavior with `expects()`.
351+
352+
**Never use `#[AllowMockObjectsWithoutExpectations]` - it's a code smell that hides the real issue.**

.dockerignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
vendor/
22
node_modules/
3-
var/
3+
var/
4+
.git/
5+
.github/
6+
.idea/
7+
tmp/
8+
tests/TestReports/
9+
.phpunit.cache

.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration
1515

1616
# Define the App Environment
17-
APP_VERSION='26.1.5'
17+
APP_VERSION='26.1.6'
1818
APP_ENV=dev
1919
APP_DEBUG=0
2020
APP_NAME="PocketCode Share"

.github/workflows/check_for_new_bricks.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
#
1313
name: Check for new bricks
1414

15+
permissions:
16+
contents: read
17+
1518
#
1619
# This check is independent of a pull request, hence, it is executed once a day on a schedule.
1720
# In case, an additional run is required, a manual dispatch trigger is also enabled.

0 commit comments

Comments
 (0)