Creazione di test di sistema SDV

Le verifiche del sistema si riferiscono a qualsiasi test SDV creato utilizzando il framework di test SDV.

Creazione di test

Località di test

  • <test_repository_root>/sample_tests
  • <test_repository_root>/e2e_tests
  • <test_repository_root>/long_running_tests
  • <test_repository_root>/performance_tests
  • <test_repository_root>/hardware

Test dei file

  • README.md: tutti i test devono includere una descrizione dello scopo del test e di come eseguirlo.

  • File di test: tutti i test seguono una struttura simile:

"""SDV Name Test"""

from sdv_test_fw.test_execution import sdv_base_test, sdv_test_runner

class SdvTypeNameTest(sdv_base_test.SdvBaseTestClass):

    def setup_class(self):
      # Setup code. Executed only once at the beginning of the test.
      super().setup_class()
      self.sdv_device1 = self.get_device('device1')
      self.sdv_device2 = self.get_device('device2')
      ...

    # Remove if not needed.
    def setup_test(self):
      super().setup_test()
      # Setup code. Executed before every test case.
      # Remove override if not needed.

    # Remove if not needed.
    def teardown_test(self):
      # Cleanup code. Executed after every test case.
      super().teardown_test()

    # Remove if not needed.
    def teardown_class(self):
      # Cleanup code. Executed once at the end of the test.
      super().teardown_class()

    def test_name_case1(self):
      # Test case step
      # Test case verification

    def test_name_case2(self):
      # Test case step
      # Test case verification

if __name__ == '__main__':
    # Start Test Execution Using SDV Test Framework
    sdv_test_runner.run()
  • File di build: Android.bp. La struttura del file è la seguente:
python_test_host {
    name: "SdvTypeNameTest", // Should match the name of the test class.
    main: "sdv_type_name_test.py",
    srcs: [
        "sdv_type_name_test.py",
    ],
    data: [
        ":sdv_test_fw_device_configs",
    ],
    test_options: {
        unit_test: false,
    },
    defaults: [
        "sdv_test_fw_defaults",
    ],
    test_config_template: ":<DEFAULT_TEMPLATE_NAME>",
}

Convenzione di denominazione

Per identificare e trovare i diversi tipi di test, questi devono essere creati seguendo una convenzione di denominazione specifica:

  • Test di esempio:
    • Nome file: sdv_sample_<NAME>_test.py
    • Nome del corso: SdvSampleNameTest
  • Test E2E:
    • Nome file: sdv_e2e_<NAME>_test.py
    • Nome del corso: SdvE2ENameTest
  • Test a lunga esecuzione:
    • Nome file: sdv_long_running_<NAME>_test.py
    • Nome del corso: SdvLongRunningNameTest
  • Test delle prestazioni:
    • Nome file: sdv_performance_<NAME>_test.py
    • Nome del corso: SdvPerformanceNameTest
  • Test dell'hardware:
    • Nome file: sdv_hw_<NAME>_test.py
    • Nome del corso: SdvHWNameTest

Linee guida per il codice

Questa sezione fornisce linee guida e best practice per la scrittura di test di sistema SDV.

Python e Mobly

Acquisisci familiarità con la guida di stile di Python e le best practice di Mobly e considera i seguenti consigli specifici per gli SDV:

  • Evita di utilizzare direttamente le importazioni Mobly, ad eccezione delle asserzioni. Il framework di test SDV si basa su questo, con un focus su SDV.

  • Asserzioni: utilizza direttamente le asserzioni Mobly.

Test SDV

Le sezioni seguenti descrivono linee guida e best practice specifiche per lo sviluppo di test all'interno del framework di test SDV.

Configurazione e pulizia

Il codice di configurazione e cleanup deve essere esterno agli scenari di test. I metodi di teardown vengono chiamati anche se il test non va a buon fine, per eseguire una corretta pulizia del dispositivo.

La posizione del codice di configurazione e di chiusura dipende dalle esigenze specifiche del test, anche se il test viene interrotto:

  1. Per l'esecuzione una sola volta all'inizio e alla fine dell'intero test, utilizza setup_class e teardown_class. Ad esempio, recuperare i dispositivi, impostare i valori delle variabili o lo stato che non cambia tra gli scenari di test, configurare le proprietà comuni dei dispositivi o impostare i flag.
def setup_class(self):
  super().setup_class()
  # setup code

def teardown_class(self):
  # teardown code
  super().teardown_class()
  1. Da eseguire tra gli scenari di test, prima e dopo ciascuno di essi. Ad esempio, una sessione interattiva o l'esecuzione di un servizio comune.
def setup_test(self):
  super().setup_test()
  # setup code

def teardown_test(self):
  # teardown code
  super().teardown_test()

Scenari di test parametrizzati

Utilizza scenari di test parametrizzati quando i passaggi sono comuni a diversi scenari di test per evitare la ripetizione del codice.

from absl.testing import parameterized

@parameterized.named_parameters(
  {
      'testcase_name': 'ab',
      'input1': 'a',
      'input2': 'b',
  },
  {
      'testcase_name': 'cd',
      'input1': 'c',
      'input2': 'd',
  },
)
def test_name(self, input1, input2):
  # test

L'esempio crea due scenari di test test_name_ab e test_name_cd.

Uno scenario di test per la verifica del comportamento

Gli scenari di test devono essere compatti e concentrarsi su un comportamento specifico. Se più comportamenti condividono precondizioni o passaggi comuni, valuta la possibilità di suddividerli. Puoi utilizzare setup_test o la parametrizzazione per ridurre al minimo la quantità di codice ripetitivo.

Questo approccio semplifica la lettura e il debug dei test perché indica chiaramente quali passaggi e condizioni non sono riusciti.

Esempio
def test_verify_process(self):
  self.device.start_process()

  # precondition 1
  self.device.send_signal1()
  # verification signal1 received
  ...

  # precondition 2
  self.device.send_signal2()
  # verification signal2 received
  ...

  # precondition 3
  self.device.start_agent()
  # verification behavior
  ...

  self.device.kill_process()
def setup_test(self):
  super().setup_test()
  self.device.start_process()

def test_signal1(self):
  # precondition
  self.device.send_signal1()
  # verification signal1 received
  ...

def test_signal2(self):
  # precondition
  self.device.send_signal2()
  # verification signal2 received
  ...

def test_agent(self):
  # precondition
  self.device.start_agent()
  # verification behavior
  ...

def teardown_test(self):
  self.device.kill_process()
  super().teardown_test()

Comportamento di test deterministico

Evita di aggiungere condizioni nel test che ne ramificano il comportamento. Se una verifica deve essere suddivisa, utilizza due scenari di test diversi.

Non utilizzare le eccezioni

I test e gli helper comuni devono utilizzare le asserzioni anziché le eccezioni. In questo modo, il debug è più semplice e vengono seguiti i pattern di test.

Esempio
result = self.some_calculations()
if result is None:
  raise Exception("No result")
result = self.some_calculations()
self.get_test_validator().assert_is_not_none(result)
Esempio
if not self.device.is_subprocess_running(
  self.EXPECTED_PROCESS
):
  raise Exception("Process is not running")
self.get_test_validator().assert_true(
  self.device.is_subprocess_running(self.EXPECTED_PROCESS),
  "Process is not running"
)

Non utilizzare la modalità di sospensione

Evita di utilizzare sleep perché aumenta il tempo di esecuzione del test e causa instabilità.

Quando il test richiede l'attesa di un evento o di una verifica per continuare, utilizza invece i metodi di attesa forniti nel framework.

Utilizza i metodi di attesa con attenzione, perché l'esecuzione del test rimane bloccata finché la condizione non corrisponde o non viene raggiunto il timeout.

Quando devi attendere il completamento di una condizione in un test, poniti le seguenti domande:

  1. Qual è un timeout ragionevole?

    Se un evento è previsto entro un periodo di tempo specifico, il timeout deve corrispondere a questa aspettativa per garantire che il test non vada a buon fine rapidamente. Se necessario, riduci il timeout (il valore predefinito è 30 s).

  2. Quanto è costosa l'operazione eseguita dal metodo in attesa?

    Evita di chiamare spesso operazioni costose. Se necessario, aumenta l'intervallo di polling (il valore predefinito è 0,5 s).

Scenari di test con requisiti

Se un test contiene scenari di test con requisiti espliciti per funzionare (ad esempio, la destinazione in cui deve essere eseguito), puoi ignorarli se non corrispondono ai requisiti:

def test_with_requirement(self):
  self.get_test_validator().skip_if(expr, reason)
  # Test case