Criação de teste do sistema SDV

Os testes do sistema se referem a qualquer teste de SDV criado usando o framework de teste de SDV.

Criação de testes

Local do teste

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

Testar arquivos

  • README.md: todos os testes precisam incluir uma descrição da finalidade do teste e como executá-lo.

  • Arquivo de teste: todos os testes seguem uma estrutura semelhante:

"""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()
  • Arquivo de build: Android.bp. A estrutura do arquivo é a seguinte:
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>",
}

Convenção de nomenclatura

Para identificar e encontrar os diferentes tipos de testes, eles precisam ser criados seguindo uma convenção de nomenclatura específica:

  • Exemplo de teste:
    • Nome do arquivo: sdv_sample_<NAME>_test.py
    • Nome da turma: SdvSampleNameTest
  • Teste E2E:
    • Nome do arquivo: sdv_e2e_<NAME>_test.py
    • Nome da turma: SdvE2ENameTest
  • Teste de longa duração:
    • Nome do arquivo: sdv_long_running_<NAME>_test.py
    • Nome da turma: SdvLongRunningNameTest
  • Teste de desempenho:
    • Nome do arquivo: sdv_performance_<NAME>_test.py
    • Nome da turma: SdvPerformanceNameTest
  • Teste de hardware:
    • Nome do arquivo: sdv_hw_<NAME>_test.py
    • Nome da turma: SdvHWNameTest

Diretrizes de código

Nesta seção, apresentamos diretrizes e práticas recomendadas para escrever testes de sistema de SDV.

Python e Mobly

Conheça o guia de estilo do Python e as práticas recomendadas do Mobly e considere as seguintes recomendações específicas para SDVs:

  • Evite usar importações do Mobly diretamente, exceto para declarações. O framework de teste de SDV se baseia nele com foco em SDV.

  • Declarações: use as declarações do Mobly diretamente.

Testes de SDV

As seções a seguir descrevem diretrizes e práticas recomendadas específicas para desenvolver testes no framework de teste de SDV.

Configuração e limpeza

O código de configuração e limpeza precisa estar fora dos casos de teste. Os métodos de desmontagem são chamados mesmo que o teste falhe, para fazer a limpeza adequada do dispositivo.

O local do código de configuração e de encerramento depende das necessidades específicas do teste, mesmo que ele seja interrompido:

  1. Para executar apenas uma vez no início e no fim de todo o teste, use setup_class e teardown_class. Por exemplo, receber dispositivos, definir valores de variáveis ou estados que não mudam entre os casos de teste, configurar propriedades comuns de dispositivos ou definir flags.
def setup_class(self):
  super().setup_class()
  # setup code

def teardown_class(self):
  # teardown code
  super().teardown_class()
  1. Para executar entre os casos de teste, antes e depois de cada um deles. Por exemplo, uma sessão interativa ou uma execução de serviço comum.
def setup_test(self):
  super().setup_test()
  # setup code

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

Casos de teste parametrizados

Use casos de teste parametrizados quando as etapas forem comuns em diferentes casos de teste para evitar a repetição 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

O exemplo cria dois casos de teste: test_name_ab e test_name_cd.

Um caso de teste para verificação de comportamento

Os casos de teste precisam ser compactos e se concentrar em um comportamento específico. Se vários comportamentos compartilharem condições prévias ou etapas comuns, considere dividi-los. É possível usar setup_test ou parametrização para minimizar a quantidade de código repetitivo.

Seguir essa abordagem facilita a leitura e a depuração dos testes, porque ela indica claramente quais etapas e condições falharam.

Exemplo
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 de teste determinista

Evite adicionar condicionais no teste que ramifiquem o comportamento dele. Se uma verificação precisar ser dividida, use dois casos de teste diferentes.

Não usar exceções

Os testes e os auxiliares comuns precisam usar declarações em vez de exceções. Isso facilita a depuração e segue padrões de teste.

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

Não usar o modo de suspensão

Evite usar sleep porque isso aumenta o tempo de execução do teste e causa instabilidade.

Quando o teste exigir que você aguarde um evento ou uma verificação para continuar, use os métodos de espera fornecidos no framework.

Use os métodos de espera com cuidado, porque a execução do teste permanece bloqueada até que a condição seja atendida ou o tempo limite seja atingido.

Quando você precisar esperar que uma condição seja concluída em um teste, faça as seguintes perguntas:

  1. Qual é um tempo limite razoável?

    Se um evento for esperado em um período específico, o tempo limite precisará corresponder a essa expectativa para garantir que o teste falhe rapidamente. Reduza o tempo limite, se necessário (o padrão é 30s).

  2. Qual é o custo da operação realizada pelo método de espera?

    Evite chamar operações caras com frequência. Aumente o intervalo de sondagem, se necessário (o padrão é 0,5s).

Casos de teste com requisitos

Se um teste tiver casos de teste com requisitos explícitos para funcionar (por exemplo, o destino em que ele precisa ser executado), será possível ignorá-los se eles não corresponderem aos requisitos:

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