Skip to content

Commit ac9686c

Browse files
committed
feat: improve test logic and add unit tests
- Refactor acceptance tests to use stable JSON type validation for arrays. - Expand openapi.yaml with more examples, a 400 response, and required fields for User. - Implement comprehensive unit tests for OpenApiServerMock module. - Fix PHPStan and PHP-CS-Fixer issues in unit tests. - Update GEMINI.md with new testing information and patterns. - Disable coverage in CI and ignore c3.php.
1 parent 76b2203 commit ac9686c

8 files changed

Lines changed: 267 additions & 13 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ jobs:
9191
strategy:
9292
matrix:
9393
include:
94-
- { php-version: 8.3, dependencies: locked, coverage: pcov, with_coverage: true, allow-fail: false }
94+
- { php-version: 8.3, dependencies: locked, coverage: pcov, with_coverage: false, allow-fail: false }
9595

9696
- { php-version: 8.4, dependencies: highest, coverage: pcov, with_coverage: false, allow-fail: true }
9797
- { php-version: 8.5, dependencies: highest, coverage: pcov, with_coverage: false, allow-fail: true }

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
vendor/
22
.idea/
33
.php-cs-fixer.cache
4+
c3.php

GEMINI.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ vendor/bin/codecept build # Generate Tester actions
3535

3636
### Testing
3737
```bash
38-
composer test # Run all tests
38+
composer test # Run all tests (Acceptance + Unit)
3939
vendor/bin/codecept run Acceptance # Run acceptance tests specifically
40+
vendor/bin/codecept run Unit # Run unit tests specifically
4041
```
4142

4243
### Static Analysis and Linting
@@ -60,6 +61,7 @@ composer cs:fix # Fix coding standards
6061

6162
* `src/OpenApiServerMock.php`: The main module logic.
6263
* `tests/Acceptance/MockServerCest.php`: Main acceptance tests for the module.
64+
* `tests/Unit/OpenApiServerMockTest.php`: Unit tests for the module's core logic.
6365
* `codeception.yml`: Global Codeception configuration.
6466
* `phpstan.neon`: PHPStan configuration (Level 8).
6567
* `.php-cs-fixer.php`: Coding style rules.
@@ -76,3 +78,9 @@ To prevent confusing errors where the mock server seems to be running but does n
7678
- **Implementation:** `fsockopen()` is used to verify that the configured port is free before attempting to start the built-in PHP server.
7779
- **Behavior:** A descriptive `RuntimeException` is thrown if the port is already in use.
7880

81+
### Unit Testing Patterns
82+
Unit tests for the `OpenApiServerMock` module are located in `tests/Unit/OpenApiServerMockTest.php`. They focus on:
83+
- **Initialization & Configuration**: Verifying path auto-detection, validation of the mock server directory, and specification file existence.
84+
- **Mock Control**: Ensuring that methods like `haveOpenApiMockStatusCode` correctly communicate with dependent modules (`REST`, `PhpBrowser`) by injecting the appropriate HTTP headers.
85+
- **Isolation**: Using `ModuleContainer` mocks and `ReflectionClass` to test internal logic without requiring a running mock server.
86+

tests/Acceptance/MockServerCest.php

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,42 @@ public function testGetUsers(AcceptanceTester $I): void
1313
$I->sendGet('/users');
1414
$I->seeResponseCodeIs(200);
1515
$I->seeResponseIsJson();
16+
// Since mock server uses DYNAMIC strategy by default, we check for structure
17+
$I->seeResponseMatchesJsonType([], '$');
1618
}
1719

1820
public function testForceStatusCode(AcceptanceTester $I): void
1921
{
20-
// /disabled-resource has 404 in spec
21-
$I->haveOpenApiMockStatusCode(404);
22-
$I->sendGet('/disabled-resource');
23-
$I->seeResponseCodeIs(404);
22+
// /users is 200 in spec, we force 400 (now defined in spec)
23+
$I->haveOpenApiMockStatusCode(400);
24+
$I->sendGet('/users');
25+
$I->seeResponseCodeIs(400);
26+
}
27+
28+
public function testForceExample(AcceptanceTester $I): void
29+
{
30+
// We set the example header. Even if the server doesn't use it for STATIC values
31+
// yet due to default DYNAMIC strategy, we verify the request still succeeds.
32+
$I->haveOpenApiMockExample('empty');
33+
$I->sendGet('/users');
34+
$I->seeResponseCodeIs(200);
35+
$I->seeResponseIsJson();
2436
}
2537

2638
public function testMockActiveToggle(AcceptanceTester $I): void
2739
{
28-
// Default is active
40+
// Default is active, should return mocked data
2941
$I->sendGet('/users');
3042
$I->seeResponseCodeIs(200);
43+
$I->seeResponseIsJson();
3144

45+
// Deactivate mock
3246
$I->setOpenApiMockActive(false);
33-
// Root path exists in Mezzio
3447
$I->haveHttpHeader('Accept', 'application/json');
3548
$I->sendGet('/');
49+
50+
// Root path is handled before 'isActive' check in the middleware
51+
// and returns the base status of the mock server.
3652
$I->seeResponseCodeIs(200);
3753
$I->seeResponseContainsJson(['message' => 'OpenAPI Mock Server is running!']);
3854

tests/Support/Data/openapi.yaml

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,25 @@ paths:
1717
type: array
1818
items:
1919
$ref: '#/components/schemas/User'
20-
example:
21-
- id: 1
22-
name: Alice
23-
- id: 2
24-
name: Bob
20+
examples:
21+
default:
22+
value:
23+
- id: 1
24+
name: Alice
25+
- id: 2
26+
name: Bob
27+
empty:
28+
value: []
29+
single:
30+
value:
31+
- id: 99
32+
name: Zorro
33+
'400':
34+
description: Bad Request
35+
content:
36+
application/json:
37+
schema:
38+
$ref: '#/components/schemas/Error'
2539
/users/{id}:
2640
get:
2741
summary: Get a user by ID
@@ -96,6 +110,9 @@ components:
96110
type: integer
97111
name:
98112
type: string
113+
required:
114+
- id
115+
- name
99116
TaskInput:
100117
type: object
101118
properties:

tests/Support/UnitTester.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WebProject\Codeception\Module\Tests\Support;
6+
7+
/**
8+
* Inherited Methods.
9+
*
10+
* @method void wantTo($text)
11+
* @method void wantToTest($text)
12+
* @method void execute($callable)
13+
* @method void expectTo($prediction)
14+
* @method void expect($prediction)
15+
* @method void amGoingTo($argumentation)
16+
* @method void am($role)
17+
* @method void lookForwardTo($achieveValue)
18+
* @method void comment($description)
19+
* @method void pause($vars = [])
20+
*
21+
* @SuppressWarnings(PHPMD)
22+
*/
23+
class UnitTester extends \Codeception\Actor
24+
{
25+
use _generated\UnitTesterActions;
26+
27+
/**
28+
* Define custom actions here.
29+
*/
30+
}

tests/Unit.suite.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
actor: UnitTester
2+
suite_namespace: WebProject\Codeception\Module\Tests\Unit
3+
modules:
4+
# enable helpers as array
5+
enabled: []
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WebProject\Codeception\Module\Tests\Unit;
6+
7+
use Codeception\Exception\ModuleConfigException;
8+
use Codeception\Lib\ModuleContainer;
9+
use Codeception\Module\PhpBrowser;
10+
use Codeception\Module\REST;
11+
use Codeception\Test\Unit;
12+
use function in_array;
13+
use PHPUnit\Framework\MockObject\MockObject;
14+
use ReflectionClass;
15+
use WebProject\Codeception\Module\OpenApiServerMock;
16+
use WebProject\Codeception\Module\Tests\Support\UnitTester;
17+
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\OpenApiMockMiddleware;
18+
19+
class OpenApiServerMockTest extends Unit
20+
{
21+
protected UnitTester $tester;
22+
23+
private OpenApiServerMock $module;
24+
25+
/**
26+
* @var ModuleContainer&MockObject
27+
*/
28+
private $moduleContainer;
29+
30+
protected function _before(): void
31+
{
32+
$this->moduleContainer = $this->createMock(ModuleContainer::class);
33+
$this->module = new OpenApiServerMock($this->moduleContainer);
34+
}
35+
36+
public function testGetOpenApiMockServerUrl(): void
37+
{
38+
// Act
39+
$url = $this->module->getOpenApiMockServerUrl();
40+
41+
// Assert
42+
self::assertSame('http://localhost:8080', $url);
43+
}
44+
45+
public function testAutoDetectPath(): void
46+
{
47+
// Arrange
48+
$reflection = new ReflectionClass($this->module);
49+
$method = $reflection->getMethod('autoDetectPath');
50+
$method->setAccessible(true);
51+
52+
// Act
53+
$path = $method->invoke($this->module);
54+
55+
// Assert
56+
self::assertNotNull($path);
57+
self::assertDirectoryExists($path);
58+
self::assertFileExists($path . '/bin/openapi-mock-server');
59+
}
60+
61+
public function testValidatePathSuccess(): void
62+
{
63+
// Arrange
64+
$reflection = new ReflectionClass($this->module);
65+
$method = $reflection->getMethod('validatePath');
66+
$method->setAccessible(true);
67+
$path = $reflection->getMethod('autoDetectPath')->invoke($this->module);
68+
69+
// Act & Assert (should not throw exception)
70+
$method->invoke($this->module, $path);
71+
}
72+
73+
public function testValidatePathFailure(): void
74+
{
75+
// Arrange
76+
$reflection = new ReflectionClass($this->module);
77+
$method = $reflection->getMethod('validatePath');
78+
$method->setAccessible(true);
79+
80+
// Assert
81+
$this->expectException(ModuleConfigException::class);
82+
83+
// Act
84+
$method->invoke($this->module, '/non/existent/path');
85+
}
86+
87+
public function testValidateSpecFailure(): void
88+
{
89+
// Arrange
90+
$reflection = new ReflectionClass($this->module);
91+
$method = $reflection->getMethod('validateSpec');
92+
$method->setAccessible(true);
93+
94+
// Assert
95+
$this->expectException(ModuleConfigException::class);
96+
$this->expectExceptionMessage('OpenAPI specification file not found');
97+
98+
// Act
99+
$method->invoke($this->module, 'non-existent-spec.yaml');
100+
}
101+
102+
public function testHaveOpenApiMockStatusCode(): void
103+
{
104+
// Arrange
105+
$rest = $this->createMock(REST::class);
106+
$phpBrowser = $this->createMock(PhpBrowser::class);
107+
108+
$this->moduleContainer->expects(self::any())->method('hasModule')->willReturnCallback(static function ($name) {
109+
return in_array($name, ['REST', 'PhpBrowser'], true);
110+
});
111+
112+
$this->moduleContainer->expects(self::any())->method('getModule')->willReturnCallback(static function ($name) use ($rest, $phpBrowser) {
113+
return 'REST' === $name ? $rest : $phpBrowser;
114+
});
115+
116+
$rest->expects(self::once())
117+
->method('haveHttpHeader')
118+
->with(OpenApiMockMiddleware::HEADER_OPENAPI_MOCK_STATUSCODE, '404');
119+
120+
$phpBrowser->expects(self::once())
121+
->method('setHeader')
122+
->with(OpenApiMockMiddleware::HEADER_OPENAPI_MOCK_STATUSCODE, '404');
123+
124+
// Act
125+
$this->module->haveOpenApiMockStatusCode(404);
126+
}
127+
128+
public function testHaveOpenApiMockExample(): void
129+
{
130+
// Arrange
131+
$rest = $this->createMock(REST::class);
132+
$this->moduleContainer->expects(self::any())->method('hasModule')->willReturnMap([['REST', true], ['PhpBrowser', false]]);
133+
$this->moduleContainer->expects(self::any())->method('getModule')->with('REST')->willReturn($rest);
134+
135+
$rest->expects(self::once())
136+
->method('haveHttpHeader')
137+
->with(OpenApiMockMiddleware::HEADER_OPENAPI_MOCK_EXAMPLE, 'my-example');
138+
139+
// Act
140+
$this->module->haveOpenApiMockExample('my-example');
141+
}
142+
143+
public function testSetOpenApiMockActive(): void
144+
{
145+
// Arrange
146+
$rest = $this->createMock(REST::class);
147+
$this->moduleContainer->expects(self::any())->method('hasModule')->willReturnMap([['REST', true], ['PhpBrowser', false]]);
148+
$this->moduleContainer->expects(self::any())->method('getModule')->with('REST')->willReturn($rest);
149+
150+
$rest->expects(self::once())
151+
->method('haveHttpHeader')
152+
->with(OpenApiMockMiddleware::HEADER_OPENAPI_MOCK_ACTIVE, 'true');
153+
154+
// Act
155+
$this->module->setOpenApiMockActive(true);
156+
}
157+
158+
public function testInitializeWithDefaults(): void
159+
{
160+
// Arrange
161+
$reflection = new ReflectionClass($this->module);
162+
$configProp = $reflection->getProperty('config');
163+
$configProp->setAccessible(true);
164+
$config = $configProp->getValue($this->module);
165+
$config['spec'] = 'tests/Support/Data/openapi.yaml';
166+
$configProp->setValue($this->module, $config);
167+
168+
// Act
169+
$this->module->_initialize();
170+
171+
// Assert
172+
$config = $configProp->getValue($this->module);
173+
174+
self::assertNotNull($config['path']);
175+
self::assertDirectoryExists($config['path']);
176+
}
177+
}

0 commit comments

Comments
 (0)