Código de teste de unidade com uma dependência do sistema de arquivos


138

Estou escrevendo um componente que, dado um arquivo ZIP, precisa:

  1. Descompacte o arquivo.
  2. Encontre uma dll específica entre os arquivos descompactados.
  3. Carregue essa dll através da reflexão e invoque um método nela.

Eu gostaria de testar este componente.

Estou tentado a escrever código que lida diretamente com o sistema de arquivos:

void DoIt()
{
   Zip.Unzip(theZipFile, "C:\\foo\\Unzipped");
   System.IO.File myDll = File.Open("C:\\foo\\Unzipped\\SuperSecret.bar");
   myDll.InvokeSomeSpecialMethod();
}

Mas as pessoas costumam dizer: "Não escreva testes de unidade que dependem do sistema de arquivos, banco de dados, rede etc."

Se eu escrevesse isso de uma maneira amigável para teste de unidade, suponho que ficaria assim:

void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
   string path = zipper.Unzip(theZipFile);
   IFakeFile file = fileSystem.Open(path);
   runner.Run(file);
}

Yay! Agora é testável; Eu posso alimentar testes duplos (zombes) para o método DoIt. Mas a que custo? Agora eu tive que definir três novas interfaces apenas para tornar isso testável. E o que exatamente estou testando? Estou testando se minha função DoIt interage corretamente com suas dependências. Não testa se o arquivo zip foi descompactado corretamente etc.

Parece que não estou mais testando a funcionalidade. Parece que estou apenas testando interações de classe.

Minha pergunta é a seguinte : qual é a maneira correta de testar algo que depende do sistema de arquivos?

edit Estou usando o .NET, mas o conceito também pode aplicar código nativo ou Java.


8
As pessoas dizem que não escrevem no sistema de arquivos em um teste de unidade, porque se você é tentado a escrever no sistema de arquivos, não está entendendo o que constitui um teste de unidade. Um teste de unidade geralmente interage com um único objeto real (a unidade em teste) e todas as outras dependências são zombadas e passadas. A classe de teste consiste em métodos de teste que validam os caminhos lógicos através dos métodos do objeto e SOMENTE os caminhos lógicos em a unidade em teste.
Christopher Perry

1
Na sua situação, a única parte que precisa de teste de unidade seria myDll.InvokeSomeSpecialMethod();onde você verificaria se ele funciona corretamente em situações de sucesso e falha, para que eu não fizesse teste de unidade, DoItmas DllRunner.Runque disse usar mal um teste UNIT para verificar se todo o processo funciona. um uso indevido aceitável e, como seria um teste de integração que oculta um teste de unidade, as regras normais de teste de unidade não precisam ser aplicadas estritamente
miket

Respostas:


47

Não há realmente nada de errado com isso, é apenas uma questão de chamá-lo de teste de unidade ou de integração. Você só precisa garantir que, se você interagir com o sistema de arquivos, não houver efeitos colaterais indesejados. Especificamente, certifique-se de limpar depois de você mesmo - excluir todos os arquivos temporários criados - e não substituir acidentalmente um arquivo existente que possuísse o mesmo nome de arquivo temporário que você estava usando. Sempre use caminhos relativos e não caminhos absolutos.

Também seria uma boa ideia chdir() entrar em um diretório temporário antes de executar o teste e chdir()voltar depois.


27
+1, no entanto, observe que chdir()é um processo completo, portanto você pode interromper a capacidade de executar seus testes em paralelo, se sua estrutura de teste ou uma versão futura dela suportar isso.

69

Yay! Agora é testável; Eu posso alimentar testes duplos (zombes) para o método DoIt. Mas a que custo? Agora eu tive que definir três novas interfaces apenas para tornar isso testável. E o que exatamente estou testando? Estou testando se minha função DoIt interage corretamente com suas dependências. Não testa se o arquivo zip foi descompactado corretamente etc.

Você acertou a unha na cabeça. O que você deseja testar é a lógica do seu método, não necessariamente se um arquivo verdadeiro pode ser endereçado. Você não precisa testar (neste teste de unidade) se um arquivo está descompactado corretamente, seu método toma isso como garantido. As interfaces são valiosas por si só porque fornecem abstrações nas quais você pode programar, em vez de depender implícita ou explicitamente de uma implementação concreta.


12
A DoItfunção testável conforme declarada nem precisa de teste. Como o questionador apontou corretamente, não resta nada de importante a ser testado. Agora é a implementação de IZipper, IFileSysteme IDllRunnerisso precisa ser testado, mas são exatamente as coisas que foram ridicularizadas para o teste!
Ian Goldby

56

Sua pergunta expõe uma das partes mais difíceis dos testes para desenvolvedores que acabaram de entrar:

"O que diabos eu teste?"

Seu exemplo não é muito interessante porque apenas cola algumas chamadas de API. Se você escrevesse um teste de unidade, acabaria afirmando que os métodos foram chamados. Testes como esse acoplam fortemente seus detalhes de implementação ao teste. Isso é ruim porque agora você precisa alterar o teste toda vez que alterar os detalhes de implementação do seu método, pois alterar os detalhes da implementação interrompe o (s) seu (s) teste (s)!

Ter testes ruins é realmente pior do que não ter testes.

No seu exemplo:

void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
   string path = zipper.Unzip(theZipFile);
   IFakeFile file = fileSystem.Open(path);
   runner.Run(file);
}

Enquanto você pode passar em zombarias, não há lógica no método para testar. Se você tentar um teste de unidade para isso, pode ser algo como isto:

// Assuming that zipper, fileSystem, and runner are mocks
void testDoIt()
{
  // mock behavior of the mock objects
  when(zipper.Unzip(any(File.class)).thenReturn("some path");
  when(fileSystem.Open("some path")).thenReturn(mock(IFakeFile.class));

  // run the test
  someObject.DoIt(zipper, fileSystem, runner);

  // verify things were called
  verify(zipper).Unzip(any(File.class));
  verify(fileSystem).Open("some path"));
  verify(runner).Run(file);
}

Parabéns, você basicamente copiou os detalhes de implementação do seu DoIt()método em um teste. Manutenção feliz.

Quando você escreve testes, deseja testar o QUE e não o COMO . Consulte Teste da caixa preta para obter mais informações.

O QUE é o nome do seu método (ou pelo menos deveria ser). O HOW são todos os pequenos detalhes de implementação que vivem dentro do seu método. Bons testes permitem que você troque o COMO sem quebrar o QUE .

Pense desta maneira, pergunte-se:

"Se eu alterar os detalhes de implementação deste método (sem alterar o contrato público), ele quebrará meus testes?"

Se a resposta for sim, você está testando o COMO e não o QUE .

Para responder sua pergunta específica sobre o teste de código com dependências do sistema de arquivos, digamos que você tenha algo um pouco mais interessante em um arquivo e deseje salvar o conteúdo codificado em Base64 de a byte[]em um arquivo. Você pode usar fluxos para isso para testar se o seu código faz a coisa certa sem precisar verificar como ele o faz. Um exemplo pode ser algo assim (em Java):

interface StreamFactory {
    OutputStream outStream();
    InputStream inStream();
}

class Base64FileWriter {
    public void write(byte[] contents, StreamFactory streamFactory) {
        OutputStream outputStream = streamFactory.outStream();
        outputStream.write(Base64.encodeBase64(contents));
    }
}

@Test
public void save_shouldBase64EncodeContents() {
    OutputStream outputStream = new ByteArrayOutputStream();
    StreamFactory streamFactory = mock(StreamFactory.class);
    when(streamFactory.outStream()).thenReturn(outputStream);

    // Run the method under test
    Base64FileWriter fileWriter = new Base64FileWriter();
    fileWriter.write("Man".getBytes(), streamFactory);

    // Assert we saved the base64 encoded contents
    assertThat(outputStream.toString()).isEqualTo("TWFu");
}

O teste usa um ByteArrayOutputStream, mas na aplicação (usando a injeção de dependência) a StreamFactory real (talvez chamado FileStreamFactory) deve retornar FileOutputStreama partir outputStream()e iria escrever para um File.

O que foi interessante sobre o writemétodo aqui é que ele estava escrevendo o conteúdo codificado em Base64, e foi para isso que testamos. Para o seu DoIt()método, isso seria testado de maneira mais apropriada com um teste de integração .


1
Não sei se concordo com sua mensagem aqui. Você está dizendo que não há necessidade de testar a unidade esse tipo de método? Então você está basicamente dizendo que TDD é ruim? Como se você fizesse TDD, não poderá escrever esse método sem antes escrever um teste. Ou você deve confiar que seu método não exigirá um teste? A razão pela qual TODAS as estruturas de teste de unidade incluem um recurso "verificar" é que não há problema em usá-lo. "Isso é ruim porque agora você precisa alterar o teste toda vez que altera os detalhes de implementação do seu método" ... bem-vindo ao mundo dos testes de unidade.
11139 Ronnie

2
Você deveria testar o CONTRATO de um método, não a implementação. Se você precisar alterar seu teste sempre que a implementação desse contrato for alterada, será um momento horrível manter a base de código do aplicativo e a base de código do teste.
Christopher Perry

@ Ronnie aplicar cegamente o teste de unidade não é útil. Existem projetos de natureza bastante variada e o teste de unidade não é eficaz em todos eles. Como exemplo, estou trabalhando em um projeto no qual 95% do código trata dos efeitos colaterais (observe que essa natureza pesada de efeito colateral é por exigência , é uma complexidade essencial, não incidental , uma vez que reúne dados de uma grande variedade de fontes com estado e a apresenta com muito pouca manipulação; portanto, dificilmente existe uma lógica pura). O teste de unidade não é eficaz aqui, o teste de integração é.
Vicky Chijwani

Os efeitos colaterais devem ser empurrados para as bordas do seu sistema, não devem ser entrelaçados nas camadas. Nas bordas, você testa os efeitos colaterais, que são comportamentos. Em qualquer outro lugar, você deve tentar ter funções puras sem efeitos colaterais, que são facilmente testados e fáceis de raciocinar, reutilizar e compor.
Christopher Perry

24

Sou reticente em poluir meu código com tipos e conceitos que existem apenas para facilitar o teste de unidade. Claro, se ele torna o design mais limpo e melhor do que ótimo, mas acho que esse não é o caso.

Minha opinião é que seus testes de unidade fariam o máximo possível, o que pode não ser 100% de cobertura. De fato, pode ser apenas 10%. O ponto é que seus testes de unidade devem ser rápidos e sem dependências externas. Eles podem testar casos como "este método lança uma ArgumentNullException quando você passa nulo para esse parâmetro".

Eu adicionaria testes de integração (também automatizados e provavelmente usando a mesma estrutura de teste de unidade) que podem ter dependências externas e testar cenários de ponta a ponta como esses.

Ao medir a cobertura do código, eu medo os testes de unidade e de integração.


5
Sim, eu ouvi você. Existe esse mundo bizarro que você alcança onde se separou tanto, que tudo o que resta são invocações de métodos em objetos abstratos. Fluff arejado. Quando você chega a esse ponto, não parece que você está realmente testando algo real. Você está apenas testando interações entre classes.
Judah Gabriel Himango 24/09/08

6
Esta resposta está equivocada. O teste de unidade não é como glacê, é mais como açúcar. Está assado no bolo. Faz parte da escrita do seu código ... uma atividade de design. Portanto, você nunca "polui" seu código com algo que "facilite o teste", porque é o teste que facilita a gravação do código. 99% do tempo de um teste é difícil de escrever porque o desenvolvedor escreveu o código antes do teste, e acabou escrevendo mal código testável
Christopher Perry

1
@ Christopher: para estender sua analogia, não quero que meu bolo acabe parecendo uma fatia de baunilha só para que eu possa usar açúcar. Tudo o que estou defendendo é pragmatismo.
Kent Boogaart

1
@ Christopher: sua biografia diz tudo: "Eu sou um fanático por TDD". Eu, por outro lado, sou pragmático. Eu faço TDD onde ele se encaixa e não onde não - nada na minha resposta sugere que eu não faça TDD, embora você pareça pensar que sim. E seja TDD ou não, não apresentarei grandes quantidades de complexidade para facilitar o teste.
Kent Boogaart

3
@ChristopherPerry Você pode explicar como resolver a questão original do OP de uma maneira TDD? Eu topo com isso o tempo todo; Eu preciso escrever uma função cujo único objetivo é executar uma ação com uma dependência externa, como nesta pergunta. Portanto, mesmo no cenário de gravação do teste, qual seria esse teste?
Dax Fohl

8

Não há nada de errado em acessar o sistema de arquivos, considere apenas um teste de integração e não um teste de unidade. Eu trocaria o caminho codificado por um caminho relativo e criaria uma subpasta TestData para conter os zips dos testes de unidade.

Se seus testes de integração demorarem muito para serem executados, separe-os para que não funcionem com a mesma frequência que os testes rápidos de unidade.

Eu concordo, às vezes acho que o teste baseado em interação pode causar muito acoplamento e muitas vezes acaba não fornecendo valor suficiente. Você realmente deseja testar o descompactação do arquivo aqui e não apenas verificar se está chamando os métodos certos.


Quantas vezes eles correm é de pouca preocupação; usamos um servidor de integração contínua que os executa automaticamente para nós. Nós realmente não nos importamos quanto tempo eles levam. Se "por quanto tempo executar" não é uma preocupação, existe algum motivo para distinguir entre testes de unidade e integração?
Judah Gabriel Himango 24/09/08

4
Na verdade não. Mas se os desenvolvedores desejam executar rapidamente todos os testes de unidade localmente, é bom ter uma maneira fácil de fazer isso.
JC.

6

Uma maneira seria escrever o método descompactar para usar InputStreams. Em seguida, o teste de unidade poderia construir um InputStream a partir de uma matriz de bytes usando ByteArrayInputStream. O conteúdo dessa matriz de bytes pode ser uma constante no código de teste da unidade.


Ok, então isso permite a injeção do fluxo. Injeção de dependência / COI. Que tal descompactar o fluxo em arquivos, carregar uma dll entre esses arquivos e chamar um método nessa dll?
Judah Gabriel Himango 24/09/08

3

Parece ser mais um teste de integração, pois você depende de um detalhe específico (o sistema de arquivos) que pode mudar, em teoria.

Eu abstraia o código que lida com o sistema operacional em seu próprio módulo (classe, montagem, jar, o que for). No seu caso, você deseja carregar uma DLL específica, se encontrada, portanto, faça uma interface IDllLoader e uma classe DllLoader. Seu aplicativo adquiriu a DLL do DllLoader usando a interface e testou que ... você não é responsável pelo código de descompactação afinal, certo?


2

Supondo que "interações do sistema de arquivos" sejam bem testadas na própria estrutura, crie seu método para trabalhar com fluxos e teste isso. Abrir um FileStream e passá-lo para o método pode ser deixado de fora de seus testes, pois o FileStream.Open é bem testado pelos criadores da estrutura.


Você e o nsayer têm essencialmente a mesma sugestão: faça meu código funcionar com fluxos. Que tal a parte de descompactar o conteúdo do fluxo em arquivos DLL, abrindo essa DLL e chamando uma função? O que você faria lá?
Judah Gabriel Himango 24/09/08

3
@JudahHimango. Essas partes podem não ser necessariamente testáveis. Você não pode testar tudo. Abstraia os componentes não testáveis ​​em seus próprios blocos funcionais e assuma que eles funcionarão. Quando você se deparar com um bug com a maneira como esse bloco funciona, crie um teste para ele e pronto. Teste de unidade NÃO significa que você precisa testar tudo. A cobertura de código 100% não é realista em alguns cenários.
Zoran Pavlovic

1

Você não deve testar a interação de classe e a chamada de função. em vez disso, você deve considerar o teste de integração. Teste o resultado necessário e não a operação de carregamento de arquivo.


1

Para teste de unidade, sugiro que você inclua o arquivo de teste em seu projeto (arquivo EAR ou equivalente) e use um caminho relativo nos testes de unidade, como "../testdata/testfile".

Desde que seu projeto seja exportado / importado corretamente, o teste de unidade deve funcionar.


0

Como outros já disseram, o primeiro é bom como teste de integração. O segundo testa apenas o que a função deve realmente fazer, que é tudo o que um teste de unidade deve fazer.

Como mostrado, o segundo exemplo parece um pouco inútil, mas oferece a oportunidade de testar como a função responde a erros em qualquer uma das etapas. Você não tem nenhuma verificação de erro no exemplo, mas no sistema real, a injeção de dependência permite testar todas as respostas a quaisquer erros. Então o custo terá valido a pena.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.