Está tudo bem em fingir parte da classe em teste?

22

Suponha que eu tenha uma classe (perdoe o exemplo artificial e o mau design dele):

class MyProfit
{
  public decimal GetNewYorkRevenue();
  public decimal GetNewYorkExpenses();
  public decimal GetNewYorkProfit();

  public decimal GetMiamiRevenue();
  public decimal GetMiamiExpenses();
  public decimal GetMiamiProfit();

  public bool BothCitiesProfitable();

}

(Observe que os métodos GetxxxRevenue () e GetxxxExpenses () têm dependências stubbed)

Agora estou testando unit BothCitiesProfitable (), que depende de GetNewYorkProfit () e GetMiamiProfit (). É bom stub GetNewYorkProfit () e GetMiamiProfit ()?

Parece que se não o fizer, estou testando simultaneamente GetNewYorkProfit () e GetMiamiProfit () junto com BothCitiesProfitable (). Eu precisaria certificar-me de configurar o stubbing para GetxxxRevenue () e GetxxxExpenses () para que os métodos GetxxxProfit () retornem os valores corretos.

Até agora, só vi exemplos de stubbing de dependências em classes externas e não em métodos internos.

E se estiver tudo bem, existe um padrão específico que devo usar para fazer isso?

ATUALIZAR

Preocupa-me que possamos estar perdendo o problema principal e que isso provavelmente é culpa do meu pobre exemplo. A questão fundamental é: se um método em uma classe depende de outro método exposto publicamente nessa mesma classe, está tudo bem (ou até recomendado) remover esse outro método?

Talvez esteja faltando alguma coisa, mas não tenho certeza de que dividir a turma sempre faz sentido. Talvez outro exemplo minimamente melhor seja:

class Person
{
 public string FirstName()
 public string LastName()
 public string FullName()
}

onde o nome completo é definido como:

public string FullName()
{
  return FirstName() + " " + LastName();
}

É bom stub FirstName () e LastName () ao testar FullName ()?

Do utilizador
fonte
Se você testar algum código simultaneamente, como isso pode ser ruim? Qual é o problema? Normalmente, deve ser melhor testar o código com mais frequência e intensidade.
usuário desconhecido
Só descobri sobre sua atualização, eu atualizei a minha resposta
Winston Ewert

Respostas:

27

Você deve terminar a aula em questão.

Cada classe deve realizar alguma tarefa simples. Se sua tarefa é muito complicada para testar, a tarefa que a classe realiza é muito grande.

Ignorando a idiotice deste design:

class NewYork
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}


class Miami
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}

class MyProfit
{
     MyProfit(NewYork new_york, Miami miami);
     boolean bothProfitable();
}

ATUALIZAR

O problema com métodos de stubbing em uma classe é que você está violando o encapsulamento. Seu teste deve verificar se o comportamento externo do objeto corresponde ou não às especificações. O que quer que aconteça dentro do objeto não é da sua conta.

O fato de FullName usar FirstName e LastName é um detalhe da implementação. Nada fora da classe deve se importar com o que é verdade. Ao ridicularizar os métodos públicos para testar o objeto, você está assumindo que esse objeto está implementado.

Em algum momento no futuro, essa suposição pode deixar de estar correta. Talvez toda a lógica do nome seja realocada para um objeto Name que Person simplesmente chama. Talvez o FullName acesse diretamente as variáveis-membro first_name e last_name, em vez de chamar FirstName e LastName.

A segunda pergunta é por que você sente a necessidade de fazê-lo. Afinal, sua classe pessoal pode ser testada como:

Person person = new Person("John", "Doe");
Test.AssertEquals(person.FullName(), "John Doe");

Você não deve sentir a necessidade de remover algo deste exemplo. Se você o faz, fica satisfeito e bem ... pare com isso! Não há nenhum benefício em zombar dos métodos lá, porque você tem controle sobre o que está neles de qualquer maneira.

O único caso em que parece fazer sentido que os métodos que FullName usa para ser ridicularizado é se, de alguma forma, FirstName () e LastName () eram operações não triviais. Talvez você esteja escrevendo um desses geradores de nome aleatório, ou o Nome e o Sobrenome consultem o banco de dados em busca de respostas ou algo assim. Mas se é isso que está acontecendo, sugere que o objeto está fazendo algo que não pertence à classe Person.

Dito de outra maneira, zombar dos métodos é pegar o objeto e dividir em dois pedaços. Uma peça está sendo zombada enquanto a outra está sendo testada. O que você está fazendo é essencialmente uma quebra ad-hoc do objeto. Se for esse o caso, basta quebrar o objeto já.

Se a sua turma for simples, você não sentirá a necessidade de zombar de partes dela durante um teste. Se a sua turma é complexa o suficiente para que você sinta necessidade de zombar, divida a turma em partes mais simples.

ATUALIZAR NOVAMENTE

Na minha opinião, um objeto tem comportamento externo e interno. O comportamento externo inclui retornos de chamadas de valores para outros objetos etc. Obviamente, qualquer coisa nessa categoria deve ser testada. (caso contrário, o que você testaria?) Mas o comportamento interno não deveria ser realmente testado.

Agora o comportamento interno é testado, porque é o que resulta no comportamento externo. Mas não escrevo testes diretamente no comportamento interno, apenas indiretamente através do comportamento externo.

Se eu quiser testar algo, acho que ele deve ser movido para que se torne um comportamento externo. É por isso que acho que, se você quiser zombar de algo, deve dividir o objeto para que o que deseja zombar esteja agora no comportamento externo dos objetos em questão.

Mas que diferença isso faz? Se FirstName () e LastName () são membros de outro objeto, isso realmente altera o problema de FullName ()? Se decidirmos que é necessário zombar de Nome e Sobrenome, é realmente útil que eles estejam em outro objeto?

Acho que se você usar sua abordagem de zombaria, criará uma costura no objeto. Você tem funções como Nome () e Sobrenome () que se comunicam diretamente com uma fonte de dados externa. Você também tem FullName () que não possui. Mas uma vez que todos estão na mesma classe que não é aparente. Algumas partes não devem acessar diretamente a fonte de dados e outras são. Seu código será mais claro se você apenas dividir esses dois grupos.

EDITAR

Vamos dar um passo atrás e perguntar: por que zombamos de objetos quando testamos?

  1. Faça os testes executados de forma consistente (evite acessar itens que mudam de uma execução para outra)
  2. Evite acessar recursos caros (não atinja serviços de terceiros, etc.)
  3. Simplifique o sistema em teste
  4. Facilite o teste de todos os cenários possíveis (como simulações de falhas, etc.)
  5. Evite depender dos detalhes de outros trechos de código para que as alterações nesses outros trechos não quebrem esse teste.

Agora, acho que os motivos 1 a 4 não se aplicam a esse cenário. A zombaria da fonte externa ao testar o nome completo cuida de todos esses motivos de zombaria. A única peça não manuseada é a simplicidade, mas parece que o objeto é simples o suficiente, o que não é uma preocupação.

Penso que a sua preocupação é a razão número 5. A preocupação é que, em algum momento no futuro, alterar a implementação do Nome e Sobrenome interrompa o teste. No futuro, Nome e Sobrenome podem obter os nomes de um local ou fonte diferente. Mas FullName provavelmente sempre será FirstName() + " " + LastName(). É por isso que você deseja testar o FullName zombando de FirstName e LastName.

O que você tem, então, é um subconjunto do objeto pessoa que tem maior probabilidade de mudar que os outros. O restante do objeto usa esse subconjunto. Atualmente, esse subconjunto busca seus dados usando uma fonte, mas pode buscá-los de uma maneira completamente diferente em uma data posterior. Mas para mim parece que esse subconjunto é um objeto distinto que tenta sair.

Parece-me que se você zomba do método do objeto, está dividindo o objeto. Mas você está fazendo isso de uma maneira ad-hoc. Seu código não deixa claro que existem duas partes distintas dentro do seu objeto Pessoa. Portanto, basta dividir esse objeto no seu código real, para que fique claro ao ler o seu código o que está acontecendo. Escolha a divisão real do objeto que faz sentido e não tente dividir o objeto de maneira diferente para cada teste.

Eu suspeito que você pode se opor a dividir seu objeto, mas por quê?

EDITAR

Eu estava errado.

Você deve dividir objetos em vez de introduzir divisões ad-hoc zombando de métodos individuais. No entanto, eu estava muito focado no único método de dividir objetos. No entanto, o OO fornece vários métodos para dividir um objeto.

O que eu proporia:

class PersonBase
{
      abstract sring FirstName();
      abstract string LastName();

      string FullName()
      {
            return FirstName() + " " + LastName();
      }
 }

 class Person extends PersonBase
 {
      string FirstName(); 
      string LastName();
 }

 class FakePerson extends PersonBase
 {
      void setFirstName(string);
      void setLastName(string);
      string getFirstName();
      string getLastName();
 }

Talvez seja isso que você estava fazendo o tempo todo. Mas não acho que esse método tenha os problemas que vi com os métodos de zombaria, porque delineamos claramente de que lado cada método está. E, usando a herança, evitamos o constrangimento que surgiria se usássemos um objeto wrapper adicional.

Isso introduz alguma complexidade e, por apenas algumas funções utilitárias, eu provavelmente as testaria zombando da fonte subjacente de terceiros. Claro, eles correm um risco maior de quebrar, mas não vale a pena reorganizar. Se você tem um objeto suficientemente complexo para dividir, acho que algo assim é uma boa ideia.

Winston Ewert
fonte
8
Bem dito. Se você está tentando encontrar soluções alternativas nos testes, provavelmente precisará reconsiderar seu design (como observação lateral, agora tenho a voz de Frank Sinatra presa na minha cabeça).
Problemático
2
"Ao ridicularizar os métodos públicos para testar o objeto, você está assumindo que [como] esse objeto é implementado". Mas não é esse o caso sempre que você apaga um objeto? Por exemplo, suponha que eu saiba que meu método xyz () chama outro objeto. Para testar xyz () isoladamente, devo stub o outro objeto. Portanto, meu teste deve conhecer os detalhes de implementação do meu método xyz ().
Utilizador
No meu caso, os métodos "FirstName ()" e "LastName ()" são simples, mas consultam uma API de terceiros para obter o resultado.
Utilizador
@ Usuário, atualizado, provavelmente você está zombando da API de terceiros para testar o Nome e o Sobrenome. O que há de errado em fazer a mesma zombaria ao testar o FullName?
Winston Ewert
@Winston, esse é o meu argumento, atualmente eu zombei da API de terceiros usada no nome e sobrenome para testar o nome completo (), mas prefiro não me importar com a maneira como o nome e o sobrenome são implementados ao testar o nome completo (é claro tudo bem quando estou testando o nome e o sobrenome). Daí a minha pergunta sobre zombar de nome e sobrenome.
Utilizador
10

Embora eu concorde com a resposta de Winston Ewert, às vezes simplesmente não é possível interromper uma classe, seja devido a restrições de tempo, a API já está sendo usada em outro lugar ou o que você tem.

Em um caso como esse, em vez de zombar de métodos, eu escreveria meus testes para cobrir a classe de forma incremental. Teste os métodos getExpenses()e getRevenue(), em seguida, teste os getProfit()métodos e os métodos compartilhados. É verdade que você terá mais de um teste cobrindo um método específico, mas desde que você escreveu testes aprovados para cobrir os métodos individualmente, pode ter certeza de que suas saídas são confiáveis, com base em uma entrada testada.

Problemático
fonte
1
Acordado. Mas apenas para enfatizar o ponto para quem lê isso, esta é a solução que você usa se não puder fazer a melhor solução.
Winston Ewert
5
@Winston, e esta é a solução que você usa antes de chegar a uma solução melhor. Ou seja, tendo uma grande bola de código legado, você deve cobri-lo com testes de unidade antes de poder refatorá-lo.
Péter Török
@ Péter Török, bom ponto. Embora eu ache que isso seja coberto "não pode fazer a melhor solução". :)
Winston Ewert
@ all Testar o método getProfit () será muito difícil, pois os diferentes cenários para getExpenses () e getRevenues () realmente multiplicarão os cenários necessários para getProfit (). se estivermos testando as getExpenses () e as receitas () de forma independente Não é uma boa ideia zombar desses métodos para testar getProfit ()?
Mohd Farid
7

Para simplificar ainda mais seus exemplos, diga que você está testando C(), o que depende A()e B()cada um deles tem suas próprias dependências. Na IMO, tudo se resume ao que seu teste está tentando alcançar.

Se você está testando o comportamento dos C()dados comportamentos conhecidos das A()e B(), em seguida, é provavelmente mais fácil e melhor para stub A()e B(). Provavelmente isso seria chamado de teste de unidade por puristas.

Se você estiver testando o comportamento de todo o sistema (da C()perspectiva), eu deixaria A()e, B()como implementado, ou apagaria suas dependências (se isso possibilitar o teste) ou configuraria um ambiente de sandbox, como um teste base de dados. Eu chamaria isso de teste de integração.

Ambas as abordagens podem ser válidas; portanto, se ele foi projetado para que você possa testá-lo de qualquer maneira, provavelmente será melhor a longo prazo.

Rebecca Scott
fonte