Os resultados esperados do teste de unidade devem ser codificados?

29

Os resultados esperados de um teste de unidade devem ser codificados permanentemente ou podem depender de variáveis ​​inicializadas? Os resultados codificados ou codificados aumentam o risco de introdução de erros no teste de unidade? Existem outros fatores que não considerei?

Por exemplo, qual desses dois é um formato mais confiável?

[TestMethod]
public void GetPath_Hardcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

[TestMethod]
public void GetPath_Softcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

EDIT 1: Em resposta à resposta do DXM, a opção 3 é uma solução preferida?

[TestMethod]
public void GetPath_Option3()
{
    string field1 = "fields";
    string field2 = "that later";
    string field3 = "determine";
    string field4 = "a folder";
    MyClass target = new MyClass(field1, field2, field3, field4);
    string expected = "C:\\Output Folder\\" + string.Join("\\", field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}
Mão-E-Comida
fonte
2
Faz ambos. A sério. Os testes podem e devem se sobrepor. Também observe alguns tipos de testes orientados a dados, se você estiver lidando com valores codificados.
Job
Concordo que a terceira opção é o que eu gosto de usar. Eu não acho que a opção 1 machucaria uma vez que você elimina a manipulação na compilação.
Kwelch
Porém, ambas as opções usam codificação física e serão interrompidas se o teste não for executado em C: \\
Qwertie

Respostas:

27

Acho que calculamos os resultados do valor esperado em casos de teste mais robustos e flexíveis. Também usando bons nomes de variáveis ​​na expressão que calcula o resultado esperado, fica muito mais claro de onde o resultado esperado veio em primeiro lugar.

Dito isto, no seu exemplo específico, NÃO confiaria no método "Softcoded" porque ele usa o seu SUT (sistema em teste) como entrada para seus cálculos. Se houver um erro no MyClass em que os campos não sejam armazenados corretamente, seu teste será aprovado porque o cálculo do valor esperado usará a string errada, como target.GetPath ().

Minha sugestão seria calcular o valor esperado onde faz sentido, mas verifique se o cálculo não depende de nenhum código do próprio SUT.

Em resposta à atualização do OP à minha resposta:

Sim, com base no meu conhecimento, mas com uma experiência um pouco limitada no TDD, eu escolheria a opção 3.

DXM
fonte
1
Bom ponto! Não confie no objeto não verificado no teste.
Hand-E-Food
não é duplicação do código SUT?
Abyx
1
de certa forma, mas é assim que você verifica se o SUT está funcionando. Se usássemos o mesmo código e ele fosse preso, você nunca saberia. Obviamente, se para realizar o cálculo, você precisa duplicar muito SUT, talvez a opção 1 seja melhor, apenas codifique o valor.
DXM
16

E se o código fosse o seguinte:

MyTarget() // constructor
{
   Field1 = Field2 = Field3 = Field4 = "";
}

Seu segundo exemplo não pegaria o bug, mas o primeiro exemplo pegaria.

Em geral, eu recomendaria a codificação branda, pois ela pode ocultar bugs. Por exemplo:

string expected = "C:\\Output Folder" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);

Você consegue identificar o problema? Você não cometeria o mesmo erro em uma versão codificada. É mais difícil fazer os cálculos corretos do que os valores codificados. É por isso que prefiro trabalhar com valores codificados que com códigos codificados.

Mas há exceções. E se o seu código precisar ser executado no Windows e Linux? Não apenas o caminho precisa ser diferente, mas também deve ser usado separadores de caminho diferentes! Calcular o caminho usando funções que abstraem a diferença entre pode fazer sentido nesse contexto.

Winston Ewert
fonte
Eu ouço o que você está dizendo e isso me dá algo a considerar. A codificação eletrônica depende dos meus outros casos de teste (como ConstructorShouldCorrectlyInitialiseFields). A falha que você descreve seria referenciada por outros testes de unidade com falha.
Hand-E-Food
@ Hand-E-Food, parece que você está escrevendo testes em métodos individuais de seus objetos. Não. Você deve escrever testes que verifiquem a exatidão de todo o objeto e não métodos individuais. Caso contrário, seus testes serão frágeis em relação às alterações dentro do objeto.
Winston Ewert
Não tenho certeza se sigo. O exemplo que dei foi puramente hipotético, um cenário fácil de entender. Estou escrevendo testes de unidade para testar membros públicos de classes e objetos. Essa é a maneira correta de usá-los?
Hand-E-Food
@ Hand-E-Food, se eu entendi direito, seu teste ConstructShouldCorrectlyInitialiseFields chamaria o construtor e afirmaria que os campos estão definidos corretamente. Mas você não deveria fazer isso. Você não deve se importar com o que os campos internos estão fazendo. Você deve apenas afirmar que o comportamento externo do objeto está correto. Caso contrário, poderá chegar o dia em que você precisará substituir a implementação interna. Se você fez afirmações sobre o estado interno, todos os seus testes serão interrompidos. Mas se você fez apenas afirmações sobre comportamento externo, tudo continuará funcionando.
Winston Ewert
@ Winston - Na verdade, estou no processo de vasculhar o livro xUnit Test Patterns e antes disso terminar A Arte do Teste de Unidade. Não vou fingir que sei do que estou falando, mas gostaria de pensar que peguei algo nesses livros. Ambos os livros recomendam fortemente que cada método de teste teste o mínimo absoluto e você deve ter muitos casos de teste para testar todo o seu objeto. Dessa forma, quando as interfaces ou funcionalidades forem alteradas, você deve esperar apenas corrigir alguns métodos de teste, em vez da maioria deles. E, como são pequenas, as mudanças devem ser mais fáceis.
DXM
4

Na minha opinião, ambas as suas sugestões são inferiores ao ideal. A maneira ideal de fazer isso é esta:

[TestMethod]
public void GetPath_Hardcoded()
{
    const string f1 = "fields"; const string f2 = "that later"; 
    const string f3 = "determine"; const string f4 = "a folder";

    MyClass target = new MyClass( f1, f2, f3, f4 );
    string expected = "C:\\Output Folder\\" + string.Join("\\", f1, f2, f3, f4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

Em outras palavras, o teste deve funcionar exclusivamente com base na entrada e na saída do objeto, e não com base no estado interno do objeto. O objeto deve ser tratado como uma caixa preta. (Eu desconsidero outros problemas, como a inadequação de usar string.Join em vez de Path.Combine, porque este é apenas um exemplo.)

Mike Nakis
fonte
1
Nem todos os métodos são funcionais - muitos têm corretamente efeitos colaterais que alteram o estado de alguns objetos. Um teste de unidade para um método com efeitos colaterais provavelmente precisaria avaliar o estado do (s) objeto (s) afetado (s) pelo método.
Matthew Flynn
Então esse estado seria considerado como a saída do método. A intenção deste teste de amostra é verificar o método GetPath (), não o construtor de MyClass. Leia a resposta do @ DXM, ele fornece uma boa razão para adotar a abordagem da caixa preta.
Mike Nakis
@MatthewFlynn, você deve testar os métodos afetados por esse estado. O estado interno exato é um detalhe da implementação e não é da conta do teste.
Winston Ewert
@MatthewFlynn, só para esclarecer, isso está relacionado ao exemplo mostrado ou a algo a considerar para outros testes de unidade? Eu podia ver que importar para algo como target.Dispose(); Assert.IsTrue(target.IsDisposed);(um exemplo muito simples.)
Mão-E-Food
Mesmo nesse caso, a propriedade IsDisposed é (ou deveria ser) uma parte indispensável da interface pública da classe, e não um detalhe de implementação. (A interface IDispose não fornece essa propriedade, mas isso é lamentável.)
Mike Nakis
2

Existem dois aspectos na discussão:

1. Usando o próprio destino para o caso de teste
A primeira pergunta é / você pode usar a própria classe para confiar e fazer parte do trabalho realizado no esboço de teste? - A resposta é NÃO , pois, em geral, você nunca deve assumir o código que está testando. Se isso não for feito corretamente, com o tempo os bugs se tornam imunes a alguns testes de unidade.

2. Você
deve codificar corretamente ? Mais uma vez a resposta é não . porque como qualquer software - a codificação da informação torna-se difícil quando as coisas evoluem. Por exemplo, quando você deseja que o caminho acima seja modificado novamente, você precisa escrever uma unidade adicional ou continuar modificando. Um método melhor é manter a data de entrada e avaliação derivada da configuração separada que pode ser facilmente adaptada.

por exemplo, aqui está como eu corrigiria o esboço de teste.

[TestMethod]
public void GetPath_Tested(int CaseId)
{
    testParams = GetTestConfig(caseID,"testConfig.txt"); // some wrapper that does read line and chops the field. 
    MyClass target = new MyClass(testParams.field1, testParams.field2);
    string expected = testParams.field5;
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}
Dipan Mehta
fonte
0

Existem muitos conceitos possíveis, fizemos alguns exemplos para ver a diferença

[TestMethod]
public void GetPath_Softcoded()
{
    //Hardcoded since you want to see what you expect is most simple and clear
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";

    //If this test should also use a mocked filesystem it might be that you want to use
    //some base directory, which you could set in the setUp of your test class
    //that is usefull if you you need to run the same test on different environments
    string expected = this.outputPath + "fields\\that later\\determine\\a folder";


    //another readable way could be interesting if you have difficult variables needed to test
    string fields = "fields";
    string thatLater = "that later";
    string determine = "determine";
    string aFolder = "a folder";
    string expected = this.outputPath + fields + "\\" + thatLater + "\\" + determine + "\\" + aFolder;
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    //in general testing with real words is not needed, so code could be shorter on that
    //for testing difficult folder names you write a separate test anyway
    string f1 = "f1";
    string f2 = "f2";
    string f3 = "f3";
    string f4 = "f4";
    string expected = this.outputPath + f1 + "\\" + f2 + "\\" + f3 + "\\" + f4;
    MyClass target = new MyClass(f1, f2, f3, f4);

    //so here we start to see a structure, it looks more like an array of fields
    //so what would make testing more interesting with lots of variables is the use of a data provider
    //the data provider will re-use your test with many different kinds of inputs. That will reduce the amount of duplication of code for testing
    //http://msdn.microsoft.com/en-us/library/ms182527.aspx


    The part where you compare already seems correct
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

Para resumir: Em geral, seu primeiro teste apenas codificado faz mais sentido para mim porque é simples, direto ao ponto etc. Se você começar a codificar um caminho muitas vezes, basta colocá-lo no método de configuração.

Para mais testes estruturados futuros, eu verificaria as fontes de dados para que você possa adicionar mais linhas de dados se precisar de mais situações de teste.

Luc Franken
fonte
0

Estruturas de teste modernas permitem fornecer parâmetros ao seu método. Eu aproveitaria esses:

[TestCase("fields", "that later", "determine", "a folder", @"C:\Output Folder\fields\that later\determine\a folder")]
public void GetPathShouldReturnFullDirectoryPathBasedOnItsFields(
    string field1, string field2, string field3, string field,
    string expected)
{
    MyClass target = new MyClass(field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

Existem várias vantagens para isso, na minha opinião:

  1. Os desenvolvedores geralmente são tentados a copiar as partes aparentemente simples do código do SUT para os testes de unidade. Como Winston aponta , esses ainda podem ter erros complicados escondidos neles. "Codificar" o resultado esperado ajuda a evitar situações em que seu código de teste está incorreto pelo mesmo motivo que seu código original está incorreto. Mas se uma alteração nos requisitos forçar você a rastrear seqüências codificadas embutidas em dezenas de métodos de teste, isso pode ser irritante. Ter todos os valores codificados em um só lugar, fora da sua lógica de teste, oferece o melhor dos dois mundos.
  2. Você pode adicionar testes para diferentes entradas e saídas esperadas com uma única linha de código. Isso incentiva você a escrever mais testes, mantendo seu código de teste SECO e fácil de manter. Acho que, por ser tão barato adicionar testes, minha mente está aberta para novos casos de teste que eu não pensaria se tivesse que escrever um método totalmente novo para eles. Por exemplo, qual comportamento eu esperaria se uma das entradas tivesse ponto? Uma barra invertida? E se alguém estivesse vazio? Ou espaço em branco? Ou começou ou terminou com espaço em branco?
  3. A estrutura de teste tratará cada TestCase como seu próprio teste, inclusive colocando as entradas e saídas fornecidas no nome do teste. Se todos os TestCases passarem com apenas um, é super fácil ver qual deles quebrou e como foi diferente de todos os outros.
StriplingWarrior
fonte