PHPUnit e il testing di codice che usa file temporanei e filesystem

by theArchitect
SHARE
PHPUnit e il testing di codice che usa file temporanei e filesystem
© Guida-HTML5.it

Introduzione

Quando si lavora con PHP, molti componenti non si limitano a calcoli in memoria: leggono file di configurazione, scrivono report, generano cache, esportano dati o salvano upload. In questi casi, testare correttamente il comportamento del codice richiede attenzione, perché il filesystem introduce effetti collaterali e dipendenze dall’ambiente.

In questo tutorial vediamo un sotto-argomento molto pratico e spesso sottovalutato: come testare con PHPUnit classi che lavorano con file temporanei e con il filesystem. L’obiettivo è scrivere test affidabili, isolati e facili da mantenere, evitando di sporcare il progetto con file lasciati in giro o test fragili legati al sistema operativo.

Vedremo un esempio realistico: una classe che salva un report testuale in una directory scelta dall’applicazione. Il test verificherà che il file venga creato, che il contenuto sia corretto e che la logica si comporti bene anche quando la directory non esiste.

Codice completo

Di seguito trovi un esempio completo con una classe da testare e la relativa test suite PHPUnit.

<?php
declare(strict_types=1);

namespace AppService;

use RuntimeException;

class ReportWriter
{
    public function __construct(
        private string $outputDir
    ) {}

    public function writeReport(string $filename, array $rows): string
    {
        if (!is_dir($this->outputDir)) {
            if (!mkdir($this->outputDir, 0777, true) && !is_dir($this->outputDir)) {
                throw new RuntimeException(´Impossibile creare la directory di output.´);
            }
        }

        $path = rtrim($this->outputDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename;
        $content = $this->formatRows($rows);

        $bytes = file_put_contents($path, $content);

        if ($bytes === false) {
            throw new RuntimeException(´Impossibile scrivere il file di report.´);
        }

        return $path;
    }

    private function formatRows(array $rows): string
    {
        $lines = [];

        foreach ($rows as $row) {
            $lines[] = implode(´ | ´, $row);
        }

        return implode(PHP_EOL, $lines) . PHP_EOL;
    }
}
<?php
declare(strict_types=1);

namespace AppTestsService;

use AppServiceReportWriter;
use PHPUnitFrameworkTestCase;

class ReportWriterTest extends TestCase
{
    private string $tempDir;

    protected function setUp(): void
    {
        $this->tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . ´phpunit-report-writer-´ . uniqid(´´, true);
    }

    protected function tearDown(): void
    {
        if (is_dir($this->tempDir)) {
            $files = glob($this->tempDir . DIRECTORY_SEPARATOR . ´*´) ?: [];

            foreach ($files as $file) {
                if (is_file($file)) {
                    unlink($file);
                }
            }

            rmdir($this->tempDir);
        }
    }

    public function testWriteReportCreatesFileWithExpectedContent(): void
    {
        $writer = new ReportWriter($this->tempDir);

        $path = $writer->writeReport(´report.txt´, [
            [´Mario´, ´Admin´],
            [´Luisa´, ´Editor´],
        ]);

        $this->assertFileExists($path);
        $this->assertSame(
            "Mario | Admin" . PHP_EOL . "Luisa | Editor" . PHP_EOL,
            file_get_contents($path)
        );
    }

    public function testWriteReportCreatesDirectoryIfMissing(): void
    {
        $writer = new ReportWriter($this->tempDir);

        $path = $writer->writeReport(´log.txt´, [
            [´A1´, ´OK´],
        ]);

        $this->assertDirectoryExists($this->tempDir);
        $this->assertFileExists($path);
        $this->assertSame("A1 | OK" . PHP_EOL, file_get_contents($path));
    }

    public function testWriteReportReturnsFullPath(): void
    {
        $writer = new ReportWriter($this->tempDir);

        $path = $writer->writeReport(´export.txt´, [
            [´ID´, ´Nome´],
        ]);

        $expectedPath = $this->tempDir . DIRECTORY_SEPARATOR . ´export.txt´;

        $this->assertSame($expectedPath, $path);
    }
}

Spiegazione

La classe ReportWriter riceve in input una directory di output. Il metodo writeReport() fa tre cose importanti:

  • verifica che la directory esista;
  • se non esiste, la crea in modo ricorsivo;
  • scrive il contenuto del report su file e restituisce il percorso completo.

Il punto interessante, dal lato testing, è che non stiamo controllando solo un valore di ritorno, ma un effetto collaterale reale: la creazione di un file. Per questo usiamo il filesystem temporaneo del sistema tramite sys_get_temp_dir(), così i test non toccano cartelle del progetto e restano isolati.

Nel metodo setUp() costruiamo una directory univoca con uniqid(). Questo evita collisioni tra test eseguiti in parallelo o ripetuti più volte. Nel metodo tearDown() puliamo tutto manualmente, eliminando i file creati e poi la directory. È una buona pratica fondamentale: un test deve lasciare l’ambiente come l’ha trovato.

Vediamo le asserzioni usate:

  • assertFileExists(): verifica che il file sia stato creato davvero;
  • assertDirectoryExists(): controlla che la directory venga generata se assente;
  • assertSame(): confronta il contenuto del file o il path restituito in modo rigoroso.

Un aspetto importante è il formato del contenuto. Il metodo privato formatRows() unisce i valori di ogni riga con | e separa le righe con PHP_EOL. Usare PHP_EOL è una scelta corretta perché rende il codice più portabile tra Windows, Linux e macOS.

Il test testWriteReportCreatesDirectoryIfMissing() dimostra una situazione molto comune: la directory di destinazione non esiste ancora. Invece di considerare questo un errore, la classe la crea automaticamente. Questo tipo di comportamento è utile in contesti come export, log o cache, dove spesso la cartella viene preparata al volo.

Best practice

Quando testi codice che usa il filesystem, alcune regole aiutano a mantenere i test affidabili e veloci.

  • Usa sempre directory temporanee dedicate ai test. Evita cartelle condivise con l’applicazione o con altri test.
  • Pulisci sempre in tearDown(). Anche se il test fallisce, il cleanup deve essere prevedibile.
  • Testa sia il risultato logico sia l’effetto collaterale. Per esempio: contenuto del file, esistenza del file, path restituito.
  • Evita dipendenze da file reali del progetto. I test devono essere indipendenti dall’ambiente di sviluppo.
  • Usa PHP_EOL per i newline, così il test non diventa fragile su sistemi operativi diversi.
  • Preferisci nomi di file univoci quando il test può essere eseguito più volte o in parallelo.

Un’altra buona pratica è separare la logica di formattazione dalla logica di scrittura. In questo esempio la formattazione è in un metodo privato, ma in progetti più grandi può avere senso estrarla in un oggetto dedicato. In questo modo il test del writer si concentra sulla scrittura, mentre la formattazione può essere testata separatamente con maggiore semplicità.

Se il codice diventa più complesso, puoi anche valutare l’uso di librerie o componenti che facilitano il testing del filesystem virtuale. Tuttavia, per molti casi reali, un approccio diretto con directory temporanee è già sufficiente e molto chiaro.

Riepilogo

Testare con PHPUnit classi che lavorano con file e filesystem è una competenza molto utile in progetti reali. In questo tutorial abbiamo visto come:

  • creare una directory temporanea per isolare i test;
  • verificare la creazione di file reali;
  • controllare il contenuto scritto su disco;
  • pulire correttamente le risorse dopo ogni test;
  • gestire in modo robusto la creazione automatica delle directory mancanti.

Il messaggio chiave è semplice: un buon test sul filesystem deve essere isolato, ripetibile e pulito. Se rispetti queste regole, i tuoi test saranno più stabili e il codice più facile da mantenere.

Approfondisci con risorse ufficiali

  • Documentazione ufficiale PHPUnit: https://phpunit.de/documentation.html
  • PHP manual - filesystem functions: https://www.php.net/manual/en/ref.filesystem.php
  • PHP manual - sys_get_temp_dir(): https://www.php.net/manual/en/function.sys-get-temp-dir.php
  • PHP manual - file_put_contents(): https://www.php.net/manual/en/function.file-put-contents.php
  • PHP manual - mkdir(): https://www.php.net/manual/en/function.mkdir.php

SHARE