json_encode(['ko' => '게시판 '.$i, 'en' => 'Board '.$i]), 'slug' => 'chunkskip-'.$i, 'created_at' => now(), 'updated_at' => now(), ]; } foreach (array_chunk($rows, 100) as $batch) { DB::table('boards')->insert($batch); } } /** * `\uXXXX` 이스케이프가 남아 있는 boards 건수를 반환합니다. * * @return int 잔여 건수 */ private function remainingEscapedCount(): int { return DB::table('boards')->where('name', 'LIKE', '%\\\\u%')->count(); } public function test_conversion_leaves_no_escaped_rows_when_filter_column_is_mutated_during_iteration(): void { $this->seedEscapedBoards(); $this->assertSame( self::SEED_COUNT, $this->remainingEscapedCount(), '시드 직후에는 \\uXXXX 이스케이프 행이 시드 건수만큼 존재해야 합니다.' ); $this->artisan('json:convert-unicode', ['--table' => 'boards', '--chunk' => 100]) ->assertExitCode(0); $this->assertSame( 0, $this->remainingEscapedCount(), 'OFFSET 순회로 인해 변환되지 않고 남은 행이 있습니다 (chunk → chunkById 필요).' ); } public function test_converted_values_preserve_original_payload(): void { $this->seedEscapedBoards(); $this->artisan('json:convert-unicode', ['--table' => 'boards', '--chunk' => 100]) ->assertExitCode(0); $rows = DB::table('boards')->orderBy('id')->get(['slug', 'name']); foreach ($rows as $row) { $index = (int) str_replace('chunkskip-', '', $row->slug); $this->assertSame( ['ko' => '게시판 '.$index, 'en' => 'Board '.$index], json_decode($row->name, true), '변환 후에도 원본 값이 그대로 보존되어야 합니다.' ); } } public function test_dry_run_does_not_modify_rows(): void { $this->seedEscapedBoards(); $this->artisan('json:convert-unicode', ['--table' => 'boards', '--chunk' => 100, '--dry-run' => true]) ->assertExitCode(0); $this->assertSame( self::SEED_COUNT, $this->remainingEscapedCount(), 'dry-run 은 실제 데이터를 변경하지 않아야 합니다.' ); } }