Test unitari

Godot Engine allows to write unit tests directly in C++. The engine integrates the doctest unit testing framework which gives ability to write test suites and test cases next to production code, but since the tests in Godot go through a different main entry point, the tests reside in a dedicated tests/ directory instead, which is located at the root of the engine source code.

Platform and target support

I test unitari in C++ si possono eseguire sui sistemi operativi Linux, macOS e Windows.

I test si possono eseguire solo con gli strumenti (tools) dell'editor abilitati, il che significa che al momento i modelli di esportazione non si possono testare.

Eseguire i test

Prima che i test possano essere effettivamente eseguiti, il motore deve essere compilato con l'opzione di compilazione tests abilitata (e qualsiasi altra opzione di compilazione utilizzata solitamente), poiché i test normalmente non vengono compilati come parte del motore:

scons tests=yes

Una volta completata la compilazione, eseguire i test con l'opzione della riga di comando --test:

./bin/<godot_binary> --test

L'esecuzione del test può essere configurata con le varie opzioni della riga di comando specifiche di doctest. Per ottenere l'elenco completo delle opzioni supportate, eseguire il comando --test con l'opzione --help:

./bin/<godot_binary> --test --help

Tutte le altre opzioni e argomenti dopo il comando --test sono trattati come argomenti per doctest.

Nota

Tests are compiled automatically if you use the dev_mode=yes SCons option. dev_mode=yes is recommended if you plan on contributing to the engine development as it will automatically treat compilation warnings as errors. The continuous integration system will fail if any compilation warnings are detected, so you should strive to fix all warnings before opening a pull request.

Filtrare i test

By default, all tests are run if you don't supply any extra arguments after the --test command. But if you're writing new tests or would like to see the successful assertions output coming from those tests for debugging purposes, you can run the tests of interest with the various filtering options provided by doctest.

La sintassi jolly * è supportata per corrispondere qualsiasi numero di caratteri nelle suite di test, nei casi di test e nei nomi dei file sorgente:

Opzioni di filtraggio

Abbreviazione

Esempi

--test-suite

-ts

-ts="*[GDScript]*"

--test-case

-tc

-tc="*[String]*"

--source-file

-sf

-sf="*test_color*"

Ad esempio, per eseguire solo i test unitari String, eseguire:

./bin/<godot_binary> --test --test-case="*[String]*"

Successful assertions output can be enabled with the --success (-s) option, and can be combined with any combination of filtering options above, for instance:

./bin/<godot_binary> --test --source-file="*test_color*" --success

Specific tests can be skipped with corresponding -exclude options. As of now, some tests include random stress tests which take a while to execute. In order to skip those kind of tests, run the following command:

./bin/<godot_binary> --test --test-case-exclude="*[Stress]*"

Scrivere i test

Le suite di test rappresentano file di intestazione C++ che si devono includere come parte del punto di ingresso principale dei test in tests/test_main.cpp. Gran parte delle suite di test si trova direttamente nella cartella tests/.

Tutti i file di intestazione hanno il prefisso test_, una convenzione di denominazione su cui si basa il sistema di compilazione Godot per rilevare i test in tutto il motore.

Ecco una suite di test minima e funzionante con un singolo caso di test scritto:

#ifndef TEST_STRING_H
#define TEST_STRING_H

#include "tests/test_macros.h"

namespace TestString {

TEST_CASE("[String] Hello World!") {
    String hello = "Hello World!";
    CHECK(hello == "Hello World!");
}

} // namespace TestString

#endif // TEST_STRING_H

Nota

È possibile generare rapidamente nuovi test utilizzando lo script create_test.py presente nella cartella tests/. Questo script crea automaticamente un nuovo file di test con il codice boilerplate richiesto nel posto appropriato. È anche in grado di includere automaticamente la nuova intestazione in tests/test_main.cpp utilizzando la modalità invasiva (flag -i). Per visualizzare le istruzioni d'uso, eseguire lo script con il flag -h.

L'intestazione tests/test_macros.h incapsula tutto il necessario per scrivere test unitari in C++ in Godot. Include macro di asserzione e log dei doctest come CHECK come visto in precedenza, e ovviamente le definizioni per scrivere i casi di test stessi.

Vedi anche

tests/test_macros.h codice sorgente riguardo ogni macro e alias attualmente implementato.

I casi di test si creano utilizzando la macro TEST_CASE, simile a una funzione. Ogni caso di test deve avere una breve descrizione tra parentesi, includendo facoltativamente tag personalizzati che consentono di filtrare i test durante l'esecuzione, come [String], [Stress] ecc.

I casi di test vengono scritti in uno spazio dei nomi dedicato. Questo non è obbligatorio, ma consente di evitare conflitti di nomi quando altre funzioni ausiliari statiche vengono scritte per gestire le procedure di test ripetute, come popolare i dati di test comuni per ogni test o scrivere test parametrici.

Godot supporta la scrittura di test per modulo C++. Per istruzioni su come scrivere test per modulo, fare riferimento a Scrivere test unitari personalizzati.

Sotto-casi

In situazioni in cui c'è una configurazione comune per diversi casi di test con solo lievi variazioni, i sotto-casi possono essere molto utili. Ecco un esempio:

TEST_CASE("[SceneTree][Node] Testing node operations with a very simple scene tree") {
    // ... common setup (e.g. creating a scene tree with a few nodes)
    SUBCASE("Move node to specific index") {
        // ... setup and checks for moving a node
    }
    SUBCASE("Remove node at specific index") {
        // ... setup and checks for removing a node
    }
}

Ogni SUBCASE assicura che TEST_CASE venga eseguito dall'inizio. I sotto-casi si possono annidare a una profondità arbitraria, ma si consiglia di limitare l'annidamento a non più di un livello di profondità.

Asserzioni

Elenco di tutte le asserzioni comunemente utilizzate nei test di Godot, ordinate in base alla severità.

Asserzione

Descrizione

REQUIRE

Verifica se la condizione è vera. Fallisce immediatamente l'intero test se la condizione non è vera.

REQUIRE_FALSE

Verifica se la condizione non è vera. Fallisce immediatamente l'intero test se la condizione è vera.

CHECK

Verifica se la condizione è vera. Contrassegna il giro di test come fallito, ma permette di eseguire altre asserzioni.

CHECK_FALSE

Verifica se la condizione non è vera. Contrassegna il giro di test come fallito, ma permette di eseguire altre asserzioni.

WARN

Verifica se la condizione è vera. Non fallisce il test in nessuna circostanza, ma registra un avviso se qualcosa non è vero.

WARN_FALSE

Verifica se la condizione non è vera. Non fallisce il test in nessuna circostanza, ma registra un avviso se qualcosa è vero.

Tutte le suddette asserzioni hanno macro *_MESSAGE corrispondenti, le quali consentono di stampare un messaggio facoltativo con una spiegazione di cosa dovrebbe accadere.

È preferibile utilizzare CHECK per le asserzioni auto-esplicative e CHECK_MESSAGE per quelle più complesse, se si ritiene che meritino una spiegazione migliore.

Logging

Il risultato del test è gestito da doctest stesso e non dipende in alcun modo dalla funzionalità di stampa o di registrazione di Godot, quindi si consiglia di utilizzare le macro dedicate, che consentono di registrare il risultato del test in un formato scritto da doctest.

Macro

Descrizione

MESSAGE

Stampa un messaggio.

FAIL_CHECK

Contrassegna il test come fallito, ma continua l'esecuzione. Può essere racchiuso in istruzioni condizionali per verifiche complesse.

FAIL

Fallisce immediatamente il test. Può essere racchiuso in istruzioni condizionali per verifiche complesse.

È possibile scegliere diversi segnalatori in fase di esecuzione. Ad esempio, ecco come l'output può essere reindirizzato a un file XML:

./bin/<godot_binary> --test --source-file="*test_validate*" --success --reporters=xml --out=doctest.txt

Testare percorsi di errore

Sometimes, it's not always feasible to test for an expected result. With the Godot development philosophy of that the engine should not crash and should gracefully recover whenever a non-fatal error occurs, it's important to check that those failure paths are indeed safe to execute without crashing the engine.

Unexpected behavior can be tested in the same way as anything else. The only problem this creates is that the error printing shall unnecessarily pollute the test output with errors coming from the engine itself (even if the end result is successful).

Per alleviare questo problema, utilizzare le macro ERR_PRINT_OFF e ERR_PRINT_ON direttamente dentro i casi di test per disattivare temporaneamente l'output di errore proveniente dal motore, ad esempio:

TEST_CASE("[Color] Constructor methods") {
    ERR_PRINT_OFF;
    Color html_invalid = Color::html("invalid");
    ERR_PRINT_ON; // Don't forget to re-enable!

    CHECK_MESSAGE(html_invalid.is_equal_approx(Color()),
        "Invalid HTML notation should result in a Color with the default values.");
}

Tag speciali nei nomi dei casi di test

Questi tag si possono aggiungere al nome di un caso di test per modificare o estendere l'ambiente di test:

Tag

Descrizione

[SceneTree]

Necessario per i casi di test che dipendono da un albero di scene che richiedono un MessageQueue disponibile. Abilita anche un server fittizio di rendering e ThemeDB.

[Editor]

Simile a [SceneTree], ma con un'infrastruttura aggiuntiva correlata all'editor disponibile, come EditorSettings.

[Audio]

Inizializza l'AudioServer utilizzando un driver audio fittizio.

[Navigation]

Crea i server di navigazione 2D/3D predefiniti e li rende disponibili per i test.

È possibile utilizzarli insieme per combinare più estensioni nell'ambiente di test.

Testare i segnali

Per testare i segnali è possibile utilizzare le seguenti macro:

Macro

Descrizione

SIGNAL_WATCH(oggetto, "nome_del_segnale")

Comincia a osservare il segnale specificato sull'oggetto specificato.

SIGNAL_UNWATCH(oggetto, "nome_del_segnale")

Finisce a osservare il segnale specificato sull'oggetto specificato.

SIGNAL_CHECK("nome_del_segnale", Vector<Vector<Variant>>)

Verifica gli argomenti di tutti i segnali attivati. Il vettore esterno contiene ogni segnale attivato, mentre il vettore interno contiene l'elenco degli argomenti per quel segnale. L'ordine dei segnali è importante.

SIGNAL_CHECK_FALSE("nome_del_segnale")

Controlla se il segnale specificato non è stato attivato.

SIGNAL_DISCARD("nome_del_segnale")

Discards all records of the specified signal.

Di seguito è riportato un esempio che dimostra l'uso di queste macro:

//...
SUBCASE("[Timer] Timer process timeout signal must be emitted") {
    SIGNAL_WATCH(test_timer, SNAME("timeout"));
    test_timer->start(0.1);

    SceneTree::get_singleton()->process(0.2);

    Array signal_args;
    signal_args.push_back(Array());

    SIGNAL_CHECK(SNAME("timeout"), signal_args);

    SIGNAL_UNWATCH(test_timer, SNAME("timeout"));
}
//...

Strumenti di test

Gli strumenti di test sono metodi avanzati che consentono di eseguire procedure arbitrarie per facilitare il processo di test manuale e il debugging dei componenti interni del motore.

Questi strumenti si possono eseguire specificandone il nome dopo l'opzione --test della riga di comando. Ad esempio, il modulo GDScript implementa e registra diversi strumenti per facilitare il debugging del tokenizzatore, del parser e del compilatore:

./bin/<godot_binary> --test gdscript-tokenizer test.gd
./bin/<godot_binary> --test gdscript-parser test.gd
./bin/<godot_binary> --test gdscript-compiler test.gd

Se uno di questi strumenti viene rilevato, i test unitari rimanenti vengono saltati.

Gli strumenti di test si possono registrare ovunque nel motore, poiché il meccanismo di registrazione è molto simile a quello fornito da doctest durante la registrazione dei casi di test tramite la tecnica di inizializzazione dinamica, ma solitamente si possono registrare nelle corrispondenti sorgenti register_types.cpp (per modulo o core).

Ecco un esempio di come GDScript registra gli strumenti di test in modules/gdscript/register_types.cpp:

#ifdef TESTS_ENABLED
void test_tokenizer() {
    TestGDScript::test(TestGDScript::TestType::TEST_TOKENIZER);
}

void test_parser() {
    TestGDScript::test(TestGDScript::TestType::TEST_PARSER);
}

void test_compiler() {
    TestGDScript::test(TestGDScript::TestType::TEST_COMPILER);
}

REGISTER_TEST_COMMAND("gdscript-tokenizer", &test_tokenizer);
REGISTER_TEST_COMMAND("gdscript-parser", &test_parser);
REGISTER_TEST_COMMAND("gdscript-compiler", &test_compiler);
#endif

L'analisi personalizzata della riga di comando può essere eseguita da uno strumento di test stesso con l'ausilio del metodo get_cmdline_args di OS.

Test di integrazione per GDScript

Godot utilizza doctest per prevenire regressioni in GDScript durante lo sviluppo. Esistono diversi tipi di script di test che si possono scrivere:

  • test per gli errori previsti;

  • test per gli avvertimenti;

  • test per le funzionalità.

Pertanto, il processo di scrittura dei test di integrazione per GDScript è il seguente:

  1. Scegliere il tipo di script di test che si desidera scrivere e creare un nuovo file GDScript nella cartella modules/gdscript/tests/scripts all'interno della sottocartella corrispondente.

  2. Scrivere il codice GDScript. Lo script di test deve avere una funzione chiamata test() che non accetta argomenti. Tale funzione verrà chiamata dal test runner. Il test non deve avere alcuna dipendenza, a meno che ciò non faccia parte del test stesso. Le classi globali (quelle che usano class_name) vengono registrate prima dell'avvio del test runner, quindi queste dovrebbero funzionare se necessario.

    Ecco uno script di test di esempio:

    func test():
        if true # Missing colon here.
            print("true")
    
  3. Change directory to the Godot source repository root.

    cd godot
    
  4. Generare i file *.out per aggiornare i risultati previsti dall'output:

    bin/<godot_binary> --gdscript-generate-tests modules/gdscript/tests/scripts
    

You may add the --print-filenames option to see filenames as their test outputs are generated. If you are working on a new feature that is causing hard crashes, you can use this option to quickly find which test file causes the crash and debug from there.

  1. Esegui i test di GDScript con:

    ./bin/<godot_binary> --test --test-suite="*GDScript*"
    

Accetta anche l'opzione --print-filenames (vedere in precedenza).

Se non vengono stampati errori e tutto va bene, ecco fatto!

Avvertimento

Assicurarsi che l'output contenga effettivamente i valori previsti prima di inviare una richiesta di pull. Se --gdscript-generate-tests produce file *.out che non riguardano i nuovi test aggiunti, si dovrebbero ripristinare tali file e inviare i file *.out solo per i nuovi test.

Nota

Il test runner di GDScript è pensato per testare l'implementazione di GDScript, non per testare gli script utente né per testare il motore tramite script. Consigliamo di scrivere nuovi test per i problemi relativi a GDScript già risolti su GitHub, oppure di scrivere test per le funzionalità attualmente funzionanti.

Nota

Se il caso di test richiede che nessuna funzione test() sia presente dentro il file di script, è possibile disabilitare la sezione runtime del test rinominando il file di script in modo che corrisponda al pattern *.notest.gd. Ad esempio, "test_empty_file.notest.gd".