Duplicar constantes entre testes e código de produção?

20

É bom ou ruim duplicar dados entre testes e código real? Por exemplo, suponha que eu tenho uma classe Python FooSaverque salva arquivos com nomes específicos em um determinado diretório:

class FooSaver(object):
  def __init__(self, out_dir):
    self.out_dir = out_dir

  def _save_foo_named(self, type_, name):
    to_save = None
    if type_ == FOOTYPE_A:
      to_save = make_footype_a()
    elif type == FOOTYPE_B:
      to_save = make_footype_b()
    # etc, repeated
    with open(self.out_dir + name, "w") as f:
      f.write(str(to_save))

  def save_type_a(self):
    self._save_foo_named(a, "a.foo_file")

  def save_type_b(self):
    self._save_foo_named(b, "b.foo_file")

Agora, no meu teste, gostaria de garantir que todos esses arquivos foram criados, então quero dizer algo como isto:

foo = FooSaver("/tmp/special_name")
foo.save_type_a()
foo.save_type_b()

self.assertTrue(os.path.isfile("/tmp/special_name/a.foo_file"))
self.assertTrue(os.path.isfile("/tmp/special_name/b.foo_file"))

Embora isso duplique os nomes de arquivos em dois lugares, acho que é bom: me obriga a escrever exatamente o que espero sair do outro lado, adiciona uma camada de proteção contra erros de digitação e geralmente me faz sentir confiante de que as coisas estão funcionando exatamente como eu esperava. Sei que, se mudar a.foo_filepara type_a.foo_fileo futuro, terei que pesquisar e substituir nos meus testes, mas não acho que isso seja muito importante. Prefiro ter alguns falsos positivos se esquecer de atualizar o teste em troca de garantir que meu entendimento do código e dos testes esteja sincronizado.

Um colega de trabalho acha que essa duplicação é ruim e recomendou que eu refatorasse os dois lados para algo assim:

class FooSaver(object):
  A_FILENAME = "a.foo_file"
  B_FILENAME = "b.foo_file"

  # as before...

  def save_type_a(self):
    self._save_foo_named(a, self.A_FILENAME)

  def save_type_b(self):
    self._save_foo_named(b, self.B_FILENAME)

e no teste:

self.assertTrue(os.path.isfile("/tmp/special_name/" + FooSaver.A_FILENAME))
self.assertTrue(os.path.isfile("/tmp/special_name/" + FooSaver.B_FILENAME))

Não gosto disso porque não me deixa confiante de que o código esteja fazendo o que eu esperava - acabei de duplicar a out_dir + nameetapa no lado da produção e no lado do teste. Ele não descobrirá um erro no meu entendimento de como +funciona as strings e não detectará erros de digitação.

Por outro lado, é claramente menos quebradiço do que escrever essas strings duas vezes, e me parece um pouco errado duplicar dados em dois arquivos como esse.

Existe um precedente claro aqui? É bom duplicar constantes nos testes e no código de produção ou é muito quebradiço?

Patrick Collins
fonte

Respostas:

16

Eu acho que depende do que você está tentando testar, que vai para o contrato da classe.

Se o contrato da classe é exatamente o que FooSavergera a.foo_filee b.foo_fileem um local específico, você deve testá-lo diretamente, ou seja, duplicar as constantes nos testes.

Se, no entanto, o contrato da classe é que ele gera dois arquivos em uma área temporária, os nomes de cada um deles são facilmente alterados, especialmente em tempo de execução, você deve testar de maneira mais genérica, provavelmente usando constantes fatoradas nos testes.

Portanto, você deve discutir com o seu colega de trabalho sobre a verdadeira natureza e contrato da classe de uma perspectiva de design de domínio de nível superior. Se você não concorda, eu diria que esse é um problema do nível de compreensão e abstração da própria classe, e não de testá-lo.

Também é razoável encontrar o contrato da classe mudando durante a refatoração, por exemplo, para que seu nível de abstração seja aumentado ao longo do tempo. No início, pode ser sobre os dois arquivos específicos em um local temporário específico, mas com o tempo, você pode achar que uma abstração adicional é necessária. Nesse momento, altere os testes para mantê-los sincronizados com o contrato da turma. Não há necessidade de criar o contrato da classe imediatamente, apenas porque você está testando (YAGNI).

Quando o contrato de uma classe não está bem definido, testá-lo pode nos fazer questionar a natureza da classe, mas o usaria. Eu diria que você não deve atualizar o contrato da classe apenas porque o está testando; você deve atualizar o contrato da classe por outros motivos, como, por exemplo, é uma abstração fraca para o domínio e, caso contrário, teste-o como está.

Erik Eidt
fonte
4

O que o @Erik sugeriu - em termos de garantir que você esteja claro sobre o que está testando - certamente deve ser seu primeiro ponto de consideração.

Mas sua decisão deve levá-lo à direção de fatorar as constantes, o que deixa a parte interessante de sua pergunta (parafraseando) "Por que eu devo trocar constantes duplicadas por códigos duplicados?". (Em referência a onde você fala sobre "duplicar [ing] a etapa out_dir + name".)

Eu acredito que (módulo comentários de Erik) a maioria das situações fazer beneficiar de remoção constantes duplicados. Mas você precisa fazer isso de uma maneira que não duplique o código. No seu exemplo específico, isso é fácil. Em vez de lidar com um caminho como seqüências de caracteres "brutas" em seu código de produção, trate um caminho como um caminho. Essa é uma maneira mais robusta de unir componentes de caminho do que a concatenação de cadeias:

os.path.join(self.out_dir, name)

No seu código de teste, por outro lado, eu recomendaria algo assim. Aqui, a ênfase está mostrando que você tem um caminho e está "conectando" um nome de arquivo folha:

self.assertTrue(os.path.isfile("/tmp/special_name/{0}".format(FooSaver.A_FILENAME)))

Ou seja, com uma seleção mais criteriosa dos elementos de idioma, você pode evitar automaticamente a duplicação de código. (Não o tempo todo, mas muitas vezes na minha experiência.)

Michael Sorens
fonte
1

Concordo com a resposta de Erik Eidt, mas há uma terceira opção: apagar a constante no teste, para que o teste seja aprovado mesmo que você altere o valor da constante no código de produção.

(veja stubbing uma constante em python unittest )

foo = FooSaver("/tmp/special_name")
foo.save_type_a()
foo.save_type_b()

with mock.patch.object(FooSaver, 'A_FILENAME', 'unique_to_your_test_a'):
  self.assertTrue(os.path.isfile("/tmp/special_name/unique_to_your_test_a"))
with mock.patch.object(FooSaver, 'B_FILENAME', 'unique_to_your_test_b'):
  self.assertTrue(os.path.isfile("/tmp/special_name/unique_to_your_test_b"))

E ao fazer coisas como essas, normalmente eu fazia um teste de sanidade em que eu withexecutava os testes sem a instrução e mostrava "'a.foo_file'! = 'Unique_to_your_test_a'" e depois colocava a withinstrução novamente no teste então passa de novo.

alexanderbird
fonte