From a3f1a53f9cd9eef7a8f17fbdbdffa9c06dc615d6 Mon Sep 17 00:00:00 2001 From: Kaz WATANABE Date: Tue, 6 May 2025 14:02:01 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[WIP]=20Cursor=E3=81=A7UnitTest=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=81=97=E3=81=A6=E3=81=BF=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 12 +- tests/Fixture/TestTablesFixture.php | 46 +++++ .../Command/OpenApiControllerCommandTest.php | 83 ++++++++- .../Command/OpenApiModelCommandTest.php | 161 ++++++++++++++++++ tests/bootstrap.php | 95 +++++++++-- tests/config/app.php | 57 +++++++ tests/config/bootstrap.php | 1 + .../Controller/ArticlesController.php | 14 ++ tests/test_app/config/app.php | 28 +++ tests/test_app/config/app_local.php | 34 ++++ tests/test_app/config/bootstrap.php | 6 + tests/test_app/src/Application.php | 32 ++++ 12 files changed, 548 insertions(+), 21 deletions(-) create mode 100644 tests/Fixture/TestTablesFixture.php create mode 100644 tests/TestCase/Command/OpenApiModelCommandTest.php create mode 100644 tests/config/app.php create mode 100644 tests/config/bootstrap.php create mode 100644 tests/test_app/Controller/ArticlesController.php create mode 100644 tests/test_app/config/app.php create mode 100644 tests/test_app/config/app_local.php create mode 100644 tests/test_app/config/bootstrap.php create mode 100644 tests/test_app/src/Application.php diff --git a/composer.json b/composer.json index b1502bf..5388d17 100644 --- a/composer.json +++ b/composer.json @@ -7,10 +7,14 @@ "php": ">=8.3", "cakephp/cakephp": "^5.0 || ^5.1", "cakephp/bake": "^3.1", - "zircote/swagger-php": "^4.9" + "zircote/swagger-php": "^4.9", + "cakephp/twig-view": "^2.0.0" }, "require-dev": { - "phpunit/phpunit": "^10.1.0" + "phpunit/phpunit": "^10.1.0", + "cakephp/chronos": "^3.0", + "cakephp/migrations": "^4.0", + "cakephp/plugin-installer": "^2.0" }, "autoload": { "psr-4": { @@ -20,6 +24,7 @@ "autoload-dev": { "psr-4": { "OpenApiTheme\\Test\\": "tests/", + "TestApp\\": "tests/test_app/src/", "Cake\\Test\\": "vendor/cakephp/cakephp/tests/" } }, @@ -27,7 +32,8 @@ "bin/build-swagger-json" ], "scripts": { - "build:swagger": "build-swagger-json" + "build:swagger": "build-swagger-json", + "test": "phpunit" }, "config": { "allow-plugins": { diff --git a/tests/Fixture/TestTablesFixture.php b/tests/Fixture/TestTablesFixture.php new file mode 100644 index 0000000..8be1d3d --- /dev/null +++ b/tests/Fixture/TestTablesFixture.php @@ -0,0 +1,46 @@ + + */ + public array $fields = [ + 'id' => ['type' => 'integer', 'length' => 11, 'unsigned' => false, 'null' => false, 'default' => null, 'comment' => 'Primary key', 'autoIncrement' => true], + 'name' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => 'Name field', 'collate' => 'utf8mb4_general_ci'], + 'created' => ['type' => 'datetime', 'length' => null, 'precision' => null, 'null' => false, 'default' => null, 'comment' => ''], + 'modified' => ['type' => 'datetime', 'length' => null, 'precision' => null, 'null' => false, 'default' => null, 'comment' => ''], + '_constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], + ], + '_options' => [ + 'engine' => 'InnoDB', + 'collation' => 'utf8mb4_general_ci' + ], + ]; + + /** + * Init method + * + * @return void + */ + public function init(): void + { + $this->records = [ + [ + 'id' => 1, + 'name' => 'Test Record', + 'created' => '2024-01-01 00:00:00', + 'modified' => '2024-01-01 00:00:00' + ], + ]; + parent::init(); + } +} \ No newline at end of file diff --git a/tests/TestCase/Command/OpenApiControllerCommandTest.php b/tests/TestCase/Command/OpenApiControllerCommandTest.php index 026edcb..4b9d8dd 100644 --- a/tests/TestCase/Command/OpenApiControllerCommandTest.php +++ b/tests/TestCase/Command/OpenApiControllerCommandTest.php @@ -3,9 +3,12 @@ namespace OpenApiTheme\Test\TestCase\Command; -use Cake\TestSuite\ConsoleIntegrationTestTrait; +use Cake\Console\TestSuite\ConsoleIntegrationTestTrait; use Cake\TestSuite\TestCase; use OpenApiTheme\Command\OpenApiControllerCommand; +use Cake\Core\Configure; +use Cake\Core\Plugin; +use Cake\Filesystem\File; /** * OpenApiTheme\Command\OpenApiControllerCommand Test Case @@ -24,25 +27,89 @@ class OpenApiControllerCommandTest extends TestCase public function setUp(): void { parent::setUp(); - $this->useCommandRunner(); + $this->setAppNamespace('TestApp'); + + Configure::write('App.namespace', 'TestApp'); + Plugin::getCollection()->add(new \OpenApiTheme\Plugin()); + + // TwigViewの設定 + if (!defined('Cake\TwigView\View\CACHE')) { + define('Cake\TwigView\View\CACHE', TMP . 'twig' . DS); + } + + // テスト用のコントローラファイルを削除 + $paths = [ + TEST_APP . 'Controller' . DS . 'ArticlesController.php', + TEST_APP . 'Controller' . DS . 'Admin' . DS . 'ArticlesController.php', + ]; + foreach ($paths as $path) { + if (file_exists($path)) { + unlink($path); + } + // Adminディレクトリが存在する場合は、空のディレクトリも削除 + $adminDir = dirname($path); + if (basename($adminDir) === 'Admin' && is_dir($adminDir) && count(glob($adminDir . '/*')) === 0) { + rmdir($adminDir); + } + } + } + + public function tearDown(): void + { + parent::tearDown(); + Configure::delete('App.namespace'); + Plugin::getCollection()->remove('OpenApiTheme'); + } + + /** + * Test basic controller baking + * + * @return void + */ + public function testBasicBaking(): void + { + $this->exec('bake controller Articles --connection default --theme OpenApiTheme --no-test'); + $this->assertExitSuccess(); + $this->assertOutputContains('Baking controller class for Articles'); + $this->assertOutputContains('Wrote'); + } + + /** + * Test baking with prefix + * + * @return void + */ + public function testBakeWithPrefix(): void + { + $this->exec('bake controller Articles --prefix Admin --connection default --theme OpenApiTheme --no-test'); + $this->assertExitSuccess(); + $this->assertOutputContains('Baking controller class for Articles'); + $this->assertOutputContains('Wrote'); } + /** - * Test buildOptionParser method + * Test baking with actions * * @return void */ - public function testBuildOptionParser(): void + public function testBakeWithActions(): void { - $this->markTestIncomplete('Not implemented yet.'); + $this->exec('bake controller Articles --actions index,view,add --connection default --theme OpenApiTheme --no-test'); + $this->assertExitSuccess(); + $this->assertOutputContains('Baking controller class for Articles'); + $this->assertOutputContains('Wrote'); } /** - * Test execute method + * Test baking with no actions * * @return void */ - public function testExecute(): void + public function testBakeWithNoActions(): void { - $this->markTestIncomplete('Not implemented yet.'); + $this->exec('bake controller Articles --no-actions --connection default --theme OpenApiTheme --no-test'); + $this->assertExitSuccess(); + $this->assertOutputContains('Baking controller class for Articles'); + $this->assertOutputContains('Wrote'); } } diff --git a/tests/TestCase/Command/OpenApiModelCommandTest.php b/tests/TestCase/Command/OpenApiModelCommandTest.php new file mode 100644 index 0000000..bd09fe0 --- /dev/null +++ b/tests/TestCase/Command/OpenApiModelCommandTest.php @@ -0,0 +1,161 @@ +command = new OpenApiModelCommand(); + } + + /** + * tearDown method + * + * @return void + */ + public function tearDown(): void + { + unset($this->command); + parent::tearDown(); + } + + /** + * Test getEntityPropertySchema method + * + * @return void + */ + public function testGetEntityPropertySchema(): void + { + /** @var Table&MockObject $table */ + $table = $this->getMockBuilder(Table::class) + ->onlyMethods(['getSchema', 'associations']) + ->getMock(); + + $schema = new TableSchema('test_table', [ + 'id' => ['type' => 'integer', 'null' => false, 'comment' => 'Primary key'], + 'name' => ['type' => 'string', 'null' => false, 'comment' => 'Name field'], + ]); + + $associationCollection = new AssociationCollection(); + + $table->expects($this->once()) + ->method('getSchema') + ->willReturn($schema); + + $table->expects($this->once()) + ->method('associations') + ->willReturn($associationCollection); + + $result = $this->command->getEntityPropertySchema($table); + + $this->assertIsArray($result); + $this->assertArrayHasKey('id', $result); + $this->assertArrayHasKey('name', $result); + + $this->assertEquals('column', $result['id']['kind']); + $this->assertEquals('integer', $result['id']['type']); + $this->assertEquals(false, $result['id']['null']); + $this->assertEquals('Primary key', $result['id']['comment']); + + $this->assertEquals('column', $result['name']['kind']); + $this->assertEquals('string', $result['name']['type']); + $this->assertEquals(false, $result['name']['null']); + $this->assertEquals('Name field', $result['name']['comment']); + } + + /** + * Test getEntityPropertySchema method with associations + * + * @return void + */ + public function testGetEntityPropertySchemaWithAssociations(): void + { + /** @var Table&MockObject $table */ + $table = $this->getMockBuilder(Table::class) + ->onlyMethods(['getSchema', 'associations']) + ->getMock(); + + $schema = new TableSchema('test_table', [ + 'id' => ['type' => 'integer', 'null' => false], + ]); + + /** @var BelongsTo&MockObject $association */ + $association = $this->getMockBuilder(BelongsTo::class) + ->disableOriginalConstructor() + ->onlyMethods(['getProperty', 'getTarget']) + ->getMock(); + + /** @var Table&MockObject $targetTable */ + $targetTable = $this->getMockBuilder(Table::class) + ->onlyMethods(['getEntityClass', 'getRegistryAlias', 'getAlias']) + ->getMock(); + + $association->expects($this->any()) + ->method('getProperty') + ->willReturn('associated'); + + $association->expects($this->any()) + ->method('getTarget') + ->willReturn($targetTable); + + $targetTable->expects($this->any()) + ->method('getEntityClass') + ->willReturn('App\Model\Entity\Associated'); + + $targetTable->expects($this->any()) + ->method('getRegistryAlias') + ->willReturn('Associated'); + + $targetTable->expects($this->any()) + ->method('getAlias') + ->willReturn('Associated'); + + $associationCollection = new AssociationCollection(); + $associationCollection->add('Associated', $association); + + $table->expects($this->once()) + ->method('getSchema') + ->willReturn($schema); + + $table->expects($this->once()) + ->method('associations') + ->willReturn($associationCollection); + + $result = $this->command->getEntityPropertySchema($table); + + $this->assertIsArray($result); + $this->assertArrayHasKey('id', $result); + $this->assertArrayHasKey('associated', $result); + + $this->assertEquals('column', $result['id']['kind']); + $this->assertEquals('integer', $result['id']['type']); + + $this->assertEquals('association', $result['associated']['kind']); + $this->assertEquals('\App\Model\Entity\Associated', $result['associated']['type']); + } +} \ No newline at end of file diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 1575f86..d9db754 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -24,17 +24,92 @@ chdir($root); +if (!defined('DS')) { + define('DS', DIRECTORY_SEPARATOR); +} +define('ROOT', $root); +define('APP_DIR', 'TestApp'); +define('APP', ROOT . DS . 'tests' . DS . 'test_app' . DS); +define('TMP', sys_get_temp_dir() . DS); +define('CONFIG', ROOT . DS . 'tests' . DS . 'config' . DS); +define('CAKE_CORE_INCLUDE_PATH', ROOT . DS . 'vendor' . DS . 'cakephp' . DS . 'cakephp'); +define('CORE_PATH', CAKE_CORE_INCLUDE_PATH . DS); +define('CAKE', CORE_PATH . 'src' . DS); +define('TESTS', ROOT . DS . 'tests' . DS); +define('TEST_APP', TESTS . 'test_app' . DS); +define('WWW_ROOT', TEST_APP . 'webroot' . DS); + require_once $root . '/vendor/autoload.php'; +require_once CORE_PATH . 'config/bootstrap.php'; -/** - * Define fallback values for required constants and configuration. - * To customize constants and configuration remove this require - * and define the data required by your plugin here. - */ -require_once $root . '/vendor/cakephp/cakephp/tests/bootstrap.php'; +use Cake\Cache\Cache; +use Cake\Core\Configure; +use Cake\Core\Configure\Engine\PhpConfig; +use Cake\Database\Connection; +use Cake\Database\Driver\Mysql; +use Cake\Datasource\ConnectionManager; +use Cake\Log\Log; +use Cake\Utility\Security; -if (file_exists($root . '/config/bootstrap.php')) { - require $root . '/config/bootstrap.php'; +Configure::write('debug', true); +Security::setSalt('dummy-salt-for-tests'); - return; -} +// Setup test database configuration +ConnectionManager::setConfig('default', [ + 'className' => Connection::class, + 'driver' => Mysql::class, + 'database' => 'test_openapi_theme', + 'username' => 'root', + 'password' => '', + 'encoding' => 'utf8mb4', + 'timezone' => 'UTC', + 'cacheMetadata' => true, + 'quoteIdentifiers' => false, + 'log' => false, +]); + +ConnectionManager::setConfig('test', [ + 'className' => Connection::class, + 'driver' => Mysql::class, + 'database' => 'test_openapi_theme_test', + 'username' => 'root', + 'password' => '', + 'encoding' => 'utf8mb4', + 'timezone' => 'UTC', + 'cacheMetadata' => true, + 'quoteIdentifiers' => false, + 'log' => false, +]); + +Cache::setConfig([ + '_cake_core_' => [ + 'engine' => 'File', + 'prefix' => 'cake_core_', + 'serialize' => true, + 'path' => TMP, + ], + '_cake_model_' => [ + 'engine' => 'File', + 'prefix' => 'cake_model_', + 'serialize' => true, + 'path' => TMP, + ], +]); + +Log::setConfig([ + 'debug' => [ + 'engine' => 'Cake\Log\Engine\FileLog', + 'path' => TMP . 'logs/', + 'file' => 'debug', + 'levels' => ['notice', 'info', 'debug'], + ], + 'error' => [ + 'engine' => 'Cake\Log\Engine\FileLog', + 'path' => TMP . 'logs/', + 'file' => 'error', + 'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'], + ], +]); + +// Load test specific plugin configuration +Configure::load('app', 'default', false); diff --git a/tests/config/app.php b/tests/config/app.php new file mode 100644 index 0000000..bdec49e --- /dev/null +++ b/tests/config/app.php @@ -0,0 +1,57 @@ + [ + 'namespace' => 'TestApp', + 'encoding' => 'UTF-8', + 'defaultLocale' => 'en_US', + 'defaultTimezone' => 'UTC', + 'base' => false, + 'dir' => 'src', + 'webroot' => 'webroot', + 'wwwRoot' => WWW_ROOT, + 'fullBaseUrl' => false, + 'imageBaseUrl' => 'img/', + 'cssBaseUrl' => 'css/', + 'jsBaseUrl' => 'js/', + 'paths' => [ + 'plugins' => [dirname(dirname(__DIR__))], + 'templates' => [APP . 'Template' . DS], + 'locales' => [APP . 'Locale' . DS], + ], + ], + 'Security' => [ + 'salt' => 'test-salt-for-testing', + ], + 'Datasources' => [ + 'default' => [ + 'className' => Connection::class, + 'driver' => Mysql::class, + 'persistent' => false, + 'host' => 'localhost', + 'username' => 'root', + 'password' => '', + 'database' => 'test_openapi_theme', + 'encoding' => 'utf8mb4', + 'timezone' => 'UTC', + 'cacheMetadata' => true, + ], + 'test' => [ + 'className' => Connection::class, + 'driver' => Mysql::class, + 'persistent' => false, + 'host' => 'localhost', + 'username' => 'root', + 'password' => '', + 'database' => 'test_openapi_theme_test', + 'encoding' => 'utf8mb4', + 'timezone' => 'UTC', + 'cacheMetadata' => true, + ], + ], + 'debug' => true, +]; \ No newline at end of file diff --git a/tests/config/bootstrap.php b/tests/config/bootstrap.php new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/tests/config/bootstrap.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/test_app/Controller/ArticlesController.php b/tests/test_app/Controller/ArticlesController.php new file mode 100644 index 0000000..5bfcd2f --- /dev/null +++ b/tests/test_app/Controller/ArticlesController.php @@ -0,0 +1,14 @@ + [ + 'namespace' => 'TestApp', + 'encoding' => 'UTF-8', + 'defaultLocale' => 'en_US', + 'defaultTimezone' => 'UTC', + 'base' => false, + 'dir' => 'src', + 'webroot' => 'webroot', + 'wwwRoot' => WWW_ROOT, + 'fullBaseUrl' => false, + 'imageBaseUrl' => 'img/', + 'cssBaseUrl' => 'css/', + 'jsBaseUrl' => 'js/', + 'paths' => [ + 'plugins' => [dirname(dirname(dirname(__DIR__)))], + 'templates' => [APP . 'Template' . DS], + 'locales' => [APP . 'Locale' . DS], + ], + ], + 'Security' => [ + 'salt' => 'test-salt-for-testing', + ], + 'debug' => true, +]; \ No newline at end of file diff --git a/tests/test_app/config/app_local.php b/tests/test_app/config/app_local.php new file mode 100644 index 0000000..29c708e --- /dev/null +++ b/tests/test_app/config/app_local.php @@ -0,0 +1,34 @@ + [ + 'default' => [ + 'className' => Connection::class, + 'driver' => Mysql::class, + 'persistent' => false, + 'host' => 'localhost', + 'username' => 'root', + 'password' => '', + 'database' => 'test_openapi_theme', + 'encoding' => 'utf8mb4', + 'timezone' => 'UTC', + 'cacheMetadata' => true, + ], + 'test' => [ + 'className' => Connection::class, + 'driver' => Mysql::class, + 'persistent' => false, + 'host' => 'localhost', + 'username' => 'root', + 'password' => '', + 'database' => 'test_openapi_theme_test', + 'encoding' => 'utf8mb4', + 'timezone' => 'UTC', + 'cacheMetadata' => true, + ], + ], +]; \ No newline at end of file diff --git a/tests/test_app/config/bootstrap.php b/tests/test_app/config/bootstrap.php new file mode 100644 index 0000000..15745c9 --- /dev/null +++ b/tests/test_app/config/bootstrap.php @@ -0,0 +1,6 @@ +addPlugin('Bake'); + $this->addPlugin('OpenApiTheme'); + } + + public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue + { + return $middlewareQueue; + } + + public function routes(RouteBuilder $routes): void + { + $routes->scope('/', function (RouteBuilder $builder): void { + $builder->fallbacks(); + }); + parent::routes($routes); + } +} \ No newline at end of file From 92ac21fbb797f47a111593eba5c330a9cf6260c8 Mon Sep 17 00:00:00 2001 From: Kaz WATANABE Date: Tue, 6 May 2025 14:04:09 +0900 Subject: [PATCH 2/7] fix --- tests/TestCase/Command/OpenApiControllerCommandTest.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/TestCase/Command/OpenApiControllerCommandTest.php b/tests/TestCase/Command/OpenApiControllerCommandTest.php index 4b9d8dd..86cb854 100644 --- a/tests/TestCase/Command/OpenApiControllerCommandTest.php +++ b/tests/TestCase/Command/OpenApiControllerCommandTest.php @@ -39,8 +39,8 @@ public function setUp(): void // テスト用のコントローラファイルを削除 $paths = [ - TEST_APP . 'Controller' . DS . 'ArticlesController.php', - TEST_APP . 'Controller' . DS . 'Admin' . DS . 'ArticlesController.php', + TEST_APP . 'Controller' . DS . 'ArticlesController.php', + TEST_APP . 'Controller' . DS . 'Admin' . DS . 'ArticlesController.php', ]; foreach ($paths as $path) { if (file_exists($path)) { @@ -72,6 +72,7 @@ public function testBasicBaking(): void $this->assertExitSuccess(); $this->assertOutputContains('Baking controller class for Articles'); $this->assertOutputContains('Wrote'); + $this->assertOutputContains(TEST_APP . 'Controller' . DS . 'ArticlesController.php'); } /** @@ -85,6 +86,7 @@ public function testBakeWithPrefix(): void $this->assertExitSuccess(); $this->assertOutputContains('Baking controller class for Articles'); $this->assertOutputContains('Wrote'); + $this->assertOutputContains(TEST_APP . 'Controller' . DS . 'Admin' . DS . 'ArticlesController.php'); } /** @@ -98,6 +100,7 @@ public function testBakeWithActions(): void $this->assertExitSuccess(); $this->assertOutputContains('Baking controller class for Articles'); $this->assertOutputContains('Wrote'); + $this->assertOutputContains(TEST_APP . 'Controller' . DS . 'ArticlesController.php'); } /** @@ -111,5 +114,6 @@ public function testBakeWithNoActions(): void $this->assertExitSuccess(); $this->assertOutputContains('Baking controller class for Articles'); $this->assertOutputContains('Wrote'); + $this->assertOutputContains(TEST_APP . 'Controller' . DS . 'ArticlesController.php'); } } From 43b710f68f2b3cd6c0e0adda5f7d9bb608fa626e Mon Sep 17 00:00:00 2001 From: Kaz WATANABE Date: Tue, 6 May 2025 14:16:25 +0900 Subject: [PATCH 3/7] add docker settings for local test --- Dockerfile.test | 18 ++++++++++++++++++ docker-compose.test.yml | 37 +++++++++++++++++++++++++++++++++++++ run-tests.sh | 17 +++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 Dockerfile.test create mode 100644 docker-compose.test.yml create mode 100755 run-tests.sh diff --git a/Dockerfile.test b/Dockerfile.test new file mode 100644 index 0000000..1576836 --- /dev/null +++ b/Dockerfile.test @@ -0,0 +1,18 @@ +ARG PHP_VERSION +FROM php:${PHP_VERSION}-cli + +RUN apt-get update && apt-get install -y \ + libicu-dev \ + libzip-dev \ + unzip \ + git \ + && docker-php-ext-install \ + intl \ + zip \ + pdo_mysql + +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +WORKDIR /app + +CMD ["vendor/bin/phpunit"] \ No newline at end of file diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 0000000..e558e32 --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,37 @@ +version: '3.8' + +services: + test-php84: + build: + context: . + dockerfile: Dockerfile.test + args: + PHP_VERSION: "8.4" + volumes: + - .:/app + depends_on: + - db-test + environment: + - DATABASE_URL=mysql://root:secret@db-test/test_openapi_theme?encoding=utf8mb4&timezone=UTC&cacheMetadata=true + + test-php83: + build: + context: . + dockerfile: Dockerfile.test + args: + PHP_VERSION: "8.3" + volumes: + - .:/app + depends_on: + - db-test + environment: + - DATABASE_URL=mysql://root:secret@db-test/test_openapi_theme?encoding=utf8mb4&timezone=UTC&cacheMetadata=true + + db-test: + image: mysql:8.0 + environment: + MYSQL_ROOT_PASSWORD: secret + MYSQL_DATABASE: test_openapi_theme + command: --default-authentication-plugin=mysql_native_password + ports: + - "3306" \ No newline at end of file diff --git a/run-tests.sh b/run-tests.sh new file mode 100755 index 0000000..7ed096d --- /dev/null +++ b/run-tests.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# コンテナをビルド +docker-compose -f docker-compose.test.yml build + +# PHP 8.4でテスト実行 +echo "Running tests with PHP 8.4..." +docker-compose -f docker-compose.test.yml run --rm test-php84 composer install +docker-compose -f docker-compose.test.yml run --rm test-php84 + +# PHP 8.3でテスト実行 +echo "Running tests with PHP 8.3..." +docker-compose -f docker-compose.test.yml run --rm test-php83 composer install +docker-compose -f docker-compose.test.yml run --rm test-php83 + +# コンテナを停止・削除 +docker-compose -f docker-compose.test.yml down \ No newline at end of file From 6006d4ce9a22f7f432b7c74705f8e2f7826bf4ac Mon Sep 17 00:00:00 2001 From: Kaz WATANABE Date: Tue, 6 May 2025 14:20:36 +0900 Subject: [PATCH 4/7] add ci workflow --- .github/workflows/test.yml | 64 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..b08a2af --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,64 @@ +name: Tests + +on: + pull_request: + branches: + - main + types: [opened, synchronize, reopened] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + php-version: ['8.3', '8.4'] + fail-fast: false + + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: secret + MYSQL_DATABASE: test_openapi_theme + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: intl, zip, pdo_mysql + coverage: none + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Wait for MySQL + run: | + while ! mysqladmin ping -h"127.0.0.1" -P"3306" --silent; do + sleep 1 + done + + - name: Run tests + run: vendor/bin/phpunit + env: + DATABASE_URL: mysql://root:secret@127.0.0.1/test_openapi_theme?encoding=utf8mb4&timezone=UTC&cacheMetadata=true \ No newline at end of file From 392248b5e0c229c710de5ad73963b649d3588a7a Mon Sep 17 00:00:00 2001 From: Kaz WATANABE Date: Tue, 6 May 2025 14:44:00 +0900 Subject: [PATCH 5/7] remove unused line --- .gitignore | 1 + .../Command/OpenApiControllerCommandTest.php | 2 -- tests/bootstrap.php | 5 ++--- tests/test_app/Controller/ArticlesController.php | 14 -------------- 4 files changed, 3 insertions(+), 19 deletions(-) delete mode 100644 tests/test_app/Controller/ArticlesController.php diff --git a/.gitignore b/.gitignore index 2b0d4df..aa76ed4 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ /vendor/ /.idea/ .DS_Store +/tests/test_app/src/Controller diff --git a/tests/TestCase/Command/OpenApiControllerCommandTest.php b/tests/TestCase/Command/OpenApiControllerCommandTest.php index 86cb854..878a145 100644 --- a/tests/TestCase/Command/OpenApiControllerCommandTest.php +++ b/tests/TestCase/Command/OpenApiControllerCommandTest.php @@ -5,10 +5,8 @@ use Cake\Console\TestSuite\ConsoleIntegrationTestTrait; use Cake\TestSuite\TestCase; -use OpenApiTheme\Command\OpenApiControllerCommand; use Cake\Core\Configure; use Cake\Core\Plugin; -use Cake\Filesystem\File; /** * OpenApiTheme\Command\OpenApiControllerCommand Test Case diff --git a/tests/bootstrap.php b/tests/bootstrap.php index d9db754..5cbafc2 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -29,14 +29,14 @@ } define('ROOT', $root); define('APP_DIR', 'TestApp'); -define('APP', ROOT . DS . 'tests' . DS . 'test_app' . DS); +define('APP', ROOT . DS . 'tests' . DS . 'test_app' . DS . 'src' . DS); define('TMP', sys_get_temp_dir() . DS); define('CONFIG', ROOT . DS . 'tests' . DS . 'config' . DS); define('CAKE_CORE_INCLUDE_PATH', ROOT . DS . 'vendor' . DS . 'cakephp' . DS . 'cakephp'); define('CORE_PATH', CAKE_CORE_INCLUDE_PATH . DS); define('CAKE', CORE_PATH . 'src' . DS); define('TESTS', ROOT . DS . 'tests' . DS); -define('TEST_APP', TESTS . 'test_app' . DS); +define('TEST_APP', TESTS . 'test_app' . DS . 'src' . DS); define('WWW_ROOT', TEST_APP . 'webroot' . DS); require_once $root . '/vendor/autoload.php'; @@ -44,7 +44,6 @@ use Cake\Cache\Cache; use Cake\Core\Configure; -use Cake\Core\Configure\Engine\PhpConfig; use Cake\Database\Connection; use Cake\Database\Driver\Mysql; use Cake\Datasource\ConnectionManager; diff --git a/tests/test_app/Controller/ArticlesController.php b/tests/test_app/Controller/ArticlesController.php deleted file mode 100644 index 5bfcd2f..0000000 --- a/tests/test_app/Controller/ArticlesController.php +++ /dev/null @@ -1,14 +0,0 @@ - Date: Tue, 6 May 2025 14:56:10 +0900 Subject: [PATCH 6/7] add test --- .../Command/OpenApiControllerCommandTest.php | 45 +++++++++++++++++++ .../test_app/src/Controller/AppController.php | 10 +++++ 2 files changed, 55 insertions(+) create mode 100644 tests/test_app/src/Controller/AppController.php diff --git a/tests/TestCase/Command/OpenApiControllerCommandTest.php b/tests/TestCase/Command/OpenApiControllerCommandTest.php index 878a145..0bf70e4 100644 --- a/tests/TestCase/Command/OpenApiControllerCommandTest.php +++ b/tests/TestCase/Command/OpenApiControllerCommandTest.php @@ -7,6 +7,14 @@ use Cake\TestSuite\TestCase; use Cake\Core\Configure; use Cake\Core\Plugin; +use OpenApi\Attributes\Get; +use OpenApi\Attributes\Post; +use OpenApi\Attributes\Put; +use OpenApi\Attributes\Delete; +use OpenApi\Attributes\Tag; +use OpenApi\Attributes\Response; +use ReflectionClass; +use ReflectionMethod; /** * OpenApiTheme\Command\OpenApiControllerCommand Test Case @@ -71,6 +79,43 @@ public function testBasicBaking(): void $this->assertOutputContains('Baking controller class for Articles'); $this->assertOutputContains('Wrote'); $this->assertOutputContains(TEST_APP . 'Controller' . DS . 'ArticlesController.php'); + + $controllerPath = TEST_APP . 'Controller' . DS . 'ArticlesController.php'; + $this->assertFileExists($controllerPath); + + require_once $controllerPath; + $className = 'TestApp\\Controller\\ArticlesController'; + + $reflClass = new ReflectionClass($className); + $indexMethod = $reflClass->getMethod('index'); + $this->assertNotEmpty( + $indexMethod->getAttributes(Get::class), + 'indexメソッドにGetアトリビュートが存在しません' + ); + + $viewMethod = $reflClass->getMethod('view'); + $this->assertNotEmpty( + $viewMethod->getAttributes(Get::class), + 'viewメソッドにGetアトリビュートが存在しません' + ); + + $addMethod = $reflClass->getMethod('add'); + $this->assertNotEmpty( + $addMethod->getAttributes(Post::class), + 'addメソッドにPostアトリビュートが存在しません' + ); + + $editMethod = $reflClass->getMethod('edit'); + $this->assertNotEmpty( + $editMethod->getAttributes(Put::class), + 'editメソッドにPutアトリビュートが存在しません' + ); + + $deleteMethod = $reflClass->getMethod('delete'); + $this->assertNotEmpty( + $deleteMethod->getAttributes(Delete::class), + 'deleteメソッドにDeleteアトリビュートが存在しません' + ); } /** diff --git a/tests/test_app/src/Controller/AppController.php b/tests/test_app/src/Controller/AppController.php new file mode 100644 index 0000000..bb8d7d7 --- /dev/null +++ b/tests/test_app/src/Controller/AppController.php @@ -0,0 +1,10 @@ + Date: Tue, 6 May 2025 15:07:04 +0900 Subject: [PATCH 7/7] =?UTF-8?q?=E3=82=A2=E3=83=88=E3=83=AA=E3=83=93?= =?UTF-8?q?=E3=83=A5=E3=83=BC=E3=83=88=E3=81=8C=E6=AD=A3=E5=B8=B8=E3=81=AB?= =?UTF-8?q?=E7=94=9F=E6=88=90=E3=81=95=E3=82=8C=E3=81=A6=E3=81=84=E3=82=8B?= =?UTF-8?q?=E3=81=8B=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Command/OpenApiControllerCommandTest.php | 209 +++++++++++++++--- 1 file changed, 178 insertions(+), 31 deletions(-) diff --git a/tests/TestCase/Command/OpenApiControllerCommandTest.php b/tests/TestCase/Command/OpenApiControllerCommandTest.php index 0bf70e4..517794d 100644 --- a/tests/TestCase/Command/OpenApiControllerCommandTest.php +++ b/tests/TestCase/Command/OpenApiControllerCommandTest.php @@ -25,6 +25,11 @@ class OpenApiControllerCommandTest extends TestCase { use ConsoleIntegrationTestTrait; + /** + * @var array + */ + private $loadedClasses = []; + /** * setUp method * @@ -43,21 +48,7 @@ public function setUp(): void define('Cake\TwigView\View\CACHE', TMP . 'twig' . DS); } - // テスト用のコントローラファイルを削除 - $paths = [ - TEST_APP . 'Controller' . DS . 'ArticlesController.php', - TEST_APP . 'Controller' . DS . 'Admin' . DS . 'ArticlesController.php', - ]; - foreach ($paths as $path) { - if (file_exists($path)) { - unlink($path); - } - // Adminディレクトリが存在する場合は、空のディレクトリも削除 - $adminDir = dirname($path); - if (basename($adminDir) === 'Admin' && is_dir($adminDir) && count(glob($adminDir . '/*')) === 0) { - rmdir($adminDir); - } - } + $this->cleanupTestFiles(); } public function tearDown(): void @@ -65,6 +56,71 @@ public function tearDown(): void parent::tearDown(); Configure::delete('App.namespace'); Plugin::getCollection()->remove('OpenApiTheme'); + $this->cleanupTestFiles(); + } + + /** + * テストファイルをクリーンアップ + */ + private function cleanupTestFiles(): void + { + $testFiles = glob(TEST_APP . 'Controller' . DS . '*Controller.php'); + $testAdminFiles = glob(TEST_APP . 'Controller' . DS . 'Admin' . DS . '*Controller.php'); + + foreach (array_merge($testFiles ?? [], $testAdminFiles ?? []) as $file) { + // AppController.phpは削除しない + if (basename($file) === 'AppController.php') { + continue; + } + + if (file_exists($file)) { + unlink($file); + } + } + + $adminDir = TEST_APP . 'Controller' . DS . 'Admin'; + if (is_dir($adminDir) && count(glob($adminDir . '/*')) === 0) { + rmdir($adminDir); + } + } + + /** + * クラスをアンロードする + * + * @param string $className + * @return void + */ + private function removeClass(string $className): void + { + $classExists = class_exists($className, false); + if ($classExists) { + $class = new ReflectionClass($className); + $fileName = $class->getFileName(); + + if ($fileName && is_file($fileName)) { + opcache_invalidate($fileName, true); + opcache_reset(); + } + } + } + + /** + * クラスをロードする + * + * @param string $controllerPath + * @param string $className + * @return ReflectionClass + */ + private function loadControllerClass(string $controllerPath, string $className): ReflectionClass + { + if (class_exists($className, false)) { + $this->removeClass($className); + } + + require $controllerPath; + $this->loadedClasses[] = $className; + + return new ReflectionClass($className); } /** @@ -74,19 +130,17 @@ public function tearDown(): void */ public function testBasicBaking(): void { - $this->exec('bake controller Articles --connection default --theme OpenApiTheme --no-test'); + $this->exec('bake controller BasicArticles --connection default --theme OpenApiTheme --no-test'); $this->assertExitSuccess(); - $this->assertOutputContains('Baking controller class for Articles'); + $this->assertOutputContains('Baking controller class for BasicArticles'); $this->assertOutputContains('Wrote'); - $this->assertOutputContains(TEST_APP . 'Controller' . DS . 'ArticlesController.php'); - $controllerPath = TEST_APP . 'Controller' . DS . 'ArticlesController.php'; + $controllerPath = TEST_APP . 'Controller' . DS . 'BasicArticlesController.php'; $this->assertFileExists($controllerPath); - require_once $controllerPath; - $className = 'TestApp\\Controller\\ArticlesController'; + $className = 'TestApp\\Controller\\BasicArticlesController'; + $reflClass = $this->loadControllerClass($controllerPath, $className); - $reflClass = new ReflectionClass($className); $indexMethod = $reflClass->getMethod('index'); $this->assertNotEmpty( $indexMethod->getAttributes(Get::class), @@ -125,11 +179,46 @@ public function testBasicBaking(): void */ public function testBakeWithPrefix(): void { - $this->exec('bake controller Articles --prefix Admin --connection default --theme OpenApiTheme --no-test'); + $this->exec('bake controller PrefixArticles --prefix Admin --connection default --theme OpenApiTheme --no-test'); $this->assertExitSuccess(); - $this->assertOutputContains('Baking controller class for Articles'); + $this->assertOutputContains('Baking controller class for PrefixArticles'); $this->assertOutputContains('Wrote'); - $this->assertOutputContains(TEST_APP . 'Controller' . DS . 'Admin' . DS . 'ArticlesController.php'); + + $controllerPath = TEST_APP . 'Controller' . DS . 'Admin' . DS . 'PrefixArticlesController.php'; + $this->assertFileExists($controllerPath); + + $className = 'TestApp\\Controller\\Admin\\PrefixArticlesController'; + $reflClass = $this->loadControllerClass($controllerPath, $className); + + $indexMethod = $reflClass->getMethod('index'); + $this->assertNotEmpty( + $indexMethod->getAttributes(Get::class), + 'Admin/indexメソッドにGetアトリビュートが存在しません' + ); + + $viewMethod = $reflClass->getMethod('view'); + $this->assertNotEmpty( + $viewMethod->getAttributes(Get::class), + 'Admin/viewメソッドにGetアトリビュートが存在しません' + ); + + $addMethod = $reflClass->getMethod('add'); + $this->assertNotEmpty( + $addMethod->getAttributes(Post::class), + 'Admin/addメソッドにPostアトリビュートが存在しません' + ); + + $editMethod = $reflClass->getMethod('edit'); + $this->assertNotEmpty( + $editMethod->getAttributes(Put::class), + 'Admin/editメソッドにPutアトリビュートが存在しません' + ); + + $deleteMethod = $reflClass->getMethod('delete'); + $this->assertNotEmpty( + $deleteMethod->getAttributes(Delete::class), + 'Admin/deleteメソッドにDeleteアトリビュートが存在しません' + ); } /** @@ -139,11 +228,43 @@ public function testBakeWithPrefix(): void */ public function testBakeWithActions(): void { - $this->exec('bake controller Articles --actions index,view,add --connection default --theme OpenApiTheme --no-test'); + $this->exec('bake controller ActionArticles --actions index,view,add --connection default --theme OpenApiTheme --no-test'); $this->assertExitSuccess(); - $this->assertOutputContains('Baking controller class for Articles'); + $this->assertOutputContains('Baking controller class for ActionArticles'); $this->assertOutputContains('Wrote'); - $this->assertOutputContains(TEST_APP . 'Controller' . DS . 'ArticlesController.php'); + + $controllerPath = TEST_APP . 'Controller' . DS . 'ActionArticlesController.php'; + $this->assertFileExists($controllerPath); + + $className = 'TestApp\\Controller\\ActionArticlesController'; + $reflClass = $this->loadControllerClass($controllerPath, $className); + + $indexMethod = $reflClass->getMethod('index'); + $this->assertNotEmpty( + $indexMethod->getAttributes(Get::class), + 'indexメソッドにGetアトリビュートが存在しません' + ); + + $viewMethod = $reflClass->getMethod('view'); + $this->assertNotEmpty( + $viewMethod->getAttributes(Get::class), + 'viewメソッドにGetアトリビュートが存在しません' + ); + + $addMethod = $reflClass->getMethod('add'); + $this->assertNotEmpty( + $addMethod->getAttributes(Post::class), + 'addメソッドにPostアトリビュートが存在しません' + ); + + $this->assertFalse( + method_exists($className, 'edit'), + 'editメソッドが存在してはいけません' + ); + $this->assertFalse( + method_exists($className, 'delete'), + 'deleteメソッドが存在してはいけません' + ); } /** @@ -153,10 +274,36 @@ public function testBakeWithActions(): void */ public function testBakeWithNoActions(): void { - $this->exec('bake controller Articles --no-actions --connection default --theme OpenApiTheme --no-test'); + $this->exec('bake controller NoActionArticles --no-actions --connection default --theme OpenApiTheme --no-test'); $this->assertExitSuccess(); - $this->assertOutputContains('Baking controller class for Articles'); + $this->assertOutputContains('Baking controller class for NoActionArticles'); $this->assertOutputContains('Wrote'); - $this->assertOutputContains(TEST_APP . 'Controller' . DS . 'ArticlesController.php'); + + $controllerPath = TEST_APP . 'Controller' . DS . 'NoActionArticlesController.php'; + $this->assertFileExists($controllerPath); + + $className = 'TestApp\\Controller\\NoActionArticlesController'; + $reflClass = $this->loadControllerClass($controllerPath, $className); + + $this->assertFalse( + method_exists($className, 'index'), + 'indexメソッドが存在してはいけません' + ); + $this->assertFalse( + method_exists($className, 'view'), + 'viewメソッドが存在してはいけません' + ); + $this->assertFalse( + method_exists($className, 'add'), + 'addメソッドが存在してはいけません' + ); + $this->assertFalse( + method_exists($className, 'edit'), + 'editメソッドが存在してはいけません' + ); + $this->assertFalse( + method_exists($className, 'delete'), + 'deleteメソッドが存在してはいけません' + ); } }