Creación de pruebas del sistema de SDV

Las pruebas del sistema se refieren a cualquier prueba de SDV creada con el framework de pruebas de SDV.

Creación de pruebas

Ubicación de la prueba

  • <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

Probar archivos

  • README.md: Todas las pruebas deben incluir una descripción de su propósito y cómo ejecutarlas.

  • Archivo de prueba: Todas las pruebas siguen una estructura similar:

"""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()
  • Archivo de compilación: Android.bp. La estructura del archivo es la siguiente:
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>",
}

Convención de nombres

Para identificar y encontrar los diferentes tipos de pruebas, estas se deben crear siguiendo una convención de nomenclatura específica:

  • Prueba de ejemplo:
    • Nombre del archivo: sdv_sample_<NAME>_test.py
    • Nombre de la clase: SdvSampleNameTest
  • Prueba de E2E:
    • Nombre del archivo: sdv_e2e_<NAME>_test.py
    • Nombre de la clase: SdvE2ENameTest
  • Prueba de larga duración:
    • Nombre del archivo: sdv_long_running_<NAME>_test.py
    • Nombre de la clase: SdvLongRunningNameTest
  • Prueba de rendimiento:
    • Nombre del archivo: sdv_performance_<NAME>_test.py
    • Nombre de la clase: SdvPerformanceNameTest
  • Prueba de hardware:
    • Nombre del archivo: sdv_hw_<NAME>_test.py
    • Nombre de la clase: SdvHWNameTest

Lineamientos de código

En esta sección, se proporcionan lineamientos y prácticas recomendadas para escribir pruebas del sistema de SDV.

Python y Mobly

Familiarízate con la guía de estilo de Python y las prácticas recomendadas de Mobly, y ten en cuenta las siguientes recomendaciones específicas de SDV:

  • Evita usar las importaciones de Mobly directamente, excepto para las aserciones. El framework de pruebas de SDV se basa en él y se enfoca en SDV.

  • Aserciones: Usa aserciones de Mobly directamente.

Pruebas de SDV

En las siguientes secciones, se describen los lineamientos específicos y las prácticas recomendadas para desarrollar pruebas dentro del framework de pruebas de SDV.

Configuración y limpieza

El código de configuración y limpieza debe estar fuera de los casos de prueba. Los métodos de desmontaje se llaman incluso si la prueba falla, para realizar una limpieza adecuada del dispositivo.

La ubicación del código de configuración y de eliminación depende de las necesidades específicas de la prueba, incluso si esta se interrumpe:

  1. Para ejecutar solo una vez al principio y al final de toda la prueba, usa setup_class y teardown_class. Por ejemplo, obtener dispositivos, establecer valores de variables o estados que no cambian entre los casos de prueba, configurar propiedades comunes de los dispositivos o establecer marcas.
def setup_class(self):
  super().setup_class()
  # setup code

def teardown_class(self):
  # teardown code
  super().teardown_class()
  1. Para ejecutarse entre casos de prueba, antes y después de cada uno de ellos Por ejemplo, una sesión interactiva o la ejecución de un servicio común.
def setup_test(self):
  super().setup_test()
  # setup code

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

Casos de prueba con parámetros

Usa casos de prueba parametrizados cuando los pasos sean comunes en diferentes casos de prueba para evitar la repetición de código.

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

En el ejemplo, se crean dos casos de prueba: test_name_ab y test_name_cd.

Un caso de prueba para la verificación del comportamiento

Los casos de prueba deben ser compactos y enfocarse en un comportamiento específico. Si varios comportamientos comparten condiciones previas o pasos comunes, considera dividirlos. Puedes usar setup_test o la parametrización para minimizar la cantidad de código repetitivo.

Con este enfoque, las pruebas son más fáciles de leer y depurar, ya que se indica claramente qué pasos y condiciones fallaron.

Ejemplo
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()

Comportamiento de prueba determinista

Evita agregar condicionales en la prueba que ramifiquen su comportamiento. Si es necesario dividir una verificación, usa dos casos de prueba diferentes.

No uses excepciones

Las pruebas y los ayudantes comunes deben usar aserciones en lugar de excepciones. Esto facilita la depuración y sigue los patrones de prueba.

Ejemplo
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)
Ejemplo
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"
)

No usar la función de suspensión

Evita usar sleep, ya que aumenta el tiempo de ejecución de la prueba y provoca inestabilidad.

Cuando la prueba requiera esperar a que continúe un evento o una verificación, usa los métodos de espera que se proporcionan en el framework.

Usa los métodos de espera con cuidado, ya que la ejecución de la prueba permanece bloqueada hasta que se cumple la condición o se alcanza el tiempo de espera.

Cuando necesites esperar a que se complete una condición en una prueba, hazte las siguientes preguntas:

  1. ¿Cuál es un tiempo de espera razonable?

    Si se espera un evento dentro de un período específico, el tiempo de espera debe coincidir con esa expectativa para garantizar que la prueba falle con rapidez. Reduce el tiempo de espera si es necesario (el valor predeterminado es 30 s).

  2. ¿Qué tan costosa es la operación que realiza el método de espera?

    Evita llamar a operaciones costosas con frecuencia. Aumenta el intervalo de sondeo si es necesario (el valor predeterminado es 0.5 s).

Casos de prueba con requisitos

Si una prueba tiene casos de prueba con requisitos explícitos para funcionar (por ejemplo, el destino en el que se debe ejecutar), puedes omitirlos si no coinciden con los requisitos:

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