stubStepDir = base_path('upgrades'); } protected function tearDown(): void { foreach ($this->stubStepFiles as $file) { if (File::exists($file)) { File::delete($file); } } parent::tearDown(); } #[Test] public function runUpgradeSteps_는_다중_버전_step_을_버전_순서대로_실행한다(): void { // memory 가 target 이상이어야 stale 가드 우회 (테스트 환경) config(['app.version' => '0.0.5']); // beta.2 ~ beta.5 시뮬레이션을 0.0.2 ~ 0.0.5 로 대체 (실제 upgrades 디렉토리 오염 회피) foreach (['0_0_2', '0_0_3', '0_0_4', '0_0_5'] as $version) { $path = $this->stubStepDir.'/Upgrade_'.$version.'_test_multi_version.php'; $this->stubStepFiles[] = $path; $className = 'Upgrade_'.$version.'_test_multi_version'; File::put($path, <<runUpgradeSteps( '0.0.1', '0.0.5', function (string $version) use (&$executedOrder): void { $executedOrder[] = $version; }, ); // upgrade 파일명의 suffix (`_test_multi_version`) 는 prerelease 로 해석되어 버전에 추가됨 $this->assertSame( [ '0.0.2-test.multi.version', '0.0.3-test.multi.version', '0.0.4-test.multi.version', '0.0.5-test.multi.version', ], $executedOrder, 'step 은 버전 순서대로 실행되어야 한다', ); } #[Test] public function hasColumn_가드된_마이그레이션은_두_번_적용해도_SQL_error_없다(): void { // 테스트 전용 테이블 준비 Schema::create('partial_migrate_test', function (Blueprint $table): void { $table->id(); $table->string('name'); }); try { // 멱등 마이그레이션을 함수로 추출 — partial migrate 후 재실행 시뮬레이션 $idempotentMigration = function (): void { Schema::table('partial_migrate_test', function (Blueprint $table): void { if (! Schema::hasColumn('partial_migrate_test', 'extra_col')) { $table->string('extra_col')->nullable(); } }); }; // 1차 적용 $idempotentMigration(); $this->assertTrue(Schema::hasColumn('partial_migrate_test', 'extra_col'), '1차 적용 후 컬럼 존재'); // 2차 적용 — 가드 덕분에 silent skip, SQL error 없어야 함 $idempotentMigration(); $this->assertTrue(Schema::hasColumn('partial_migrate_test', 'extra_col'), '2차 적용 후에도 컬럼 존재 (silent skip)'); } finally { Schema::dropIfExists('partial_migrate_test'); } } #[Test] public function hasColumn_가드없는_마이그레이션은_두_번_적용시_SQL_error_발생한다(): void { // 회귀 가드의 역명제 — 가드 없을 때 SQL error 가 발생함을 명시 확인 Schema::create('partial_migrate_no_guard_test', function (Blueprint $table): void { $table->id(); $table->string('name'); }); try { $nonIdempotentMigration = function (): void { Schema::table('partial_migrate_no_guard_test', function (Blueprint $table): void { $table->string('extra_col')->nullable(); }); }; $nonIdempotentMigration(); $this->assertTrue(Schema::hasColumn('partial_migrate_no_guard_test', 'extra_col')); // 2차 적용 — SQL error 발생 기대 $exceptionCaught = false; try { $nonIdempotentMigration(); } catch (\Throwable $e) { $exceptionCaught = true; // SQLSTATE 또는 "Duplicate column"/"already exists" 메시지 확인 $this->assertMatchesRegularExpression( '/(already exists|duplicate column|SQLSTATE)/i', $e->getMessage(), ); } $this->assertTrue( $exceptionCaught, '가드 없는 마이그레이션을 2번 적용하면 SQL error 발생해야 한다 (회귀 가드 의의 확인)', ); } finally { Schema::dropIfExists('partial_migrate_no_guard_test'); } } }