Arquivos únicos ou múltiplos para teste de unidade de uma única classe?


20

Ao pesquisar as práticas recomendadas de teste de unidade para ajudar a elaborar diretrizes para minha organização, eu me deparei com a questão de saber se é melhor ou útil separar equipamentos de teste (classes de teste) ou manter todos os testes para uma única classe em um arquivo.

Fwiw, estou me referindo a "testes de unidade" no sentido puro de que são testes de caixa branca direcionados a uma única classe, uma asserção por teste, todas as dependências zombadas etc.

Um cenário de exemplo é uma classe (chamada Documento) que possui dois métodos: CheckIn e CheckOut. Cada método implementa várias regras, etc., que controlam seu comportamento. Seguindo a regra de uma asserção por teste, terei vários testes para cada método. Posso colocar todos os testes em uma única DocumentTestsclasse com nomes como CheckInShouldThrowExceptionWhenUserIsUnauthorizede CheckOutShouldThrowExceptionWhenUserIsUnauthorized.

Ou, eu poderia ter duas classes de teste separadas: CheckInShoulde CheckOutShould. Nesse caso, meus nomes de teste seriam reduzidos, mas eles seriam organizados para que todos os testes de um comportamento específico (método) estejam juntos.

Tenho certeza de que existem prós e contras em qualquer uma das abordagens e estou imaginando se alguém já seguiu o caminho com vários arquivos e, se sim, por quê? Ou, se você optou pela abordagem de arquivo único, por que acha que é melhor?


2
Os nomes dos seus métodos de teste são muito longos. Simplifique-os, mesmo que isso signifique que um método de teste contenha várias asserções.
131311 Bernard

12
@ Bernard: é considerado uma prática ruim ter várias asserções por método; nomes longos de método de teste não são considerados uma prática ruim. Geralmente, documentamos o que o método está testando no próprio nome. Por exemplo, constructor_nullSomeList (), setUserName_UserNameIsNotValid () etc ...
c_maker

4
@ Bernard: eu uso nomes de teste longos (e só lá!) Também. Eu os amo, porque é tão claro o que não estava funcionando (se seus nomes são boas escolhas;)).
Sebastian Bauer

Múltiplas asserções não são uma prática ruim, se todas testarem o mesmo resultado. Por exemplo. você não teria testResponseContainsSuccessTrue(), testResponseContainsMyData()e testResponseStatusCodeIsOk(). Você poderia tê-los em um único testResponse()que tem três afirma: assertEquals(200, response.status), assertEquals({"data": "mydata"}, response.data)eassertEquals(true, response.success)
Juha Untinen

Respostas:


18

É raro, mas às vezes faz sentido ter várias classes de teste para uma determinada classe em teste. Normalmente, eu faria isso quando diferentes configurações são necessárias e compartilhadas em um subconjunto dos testes.


3
Bom exemplo por que essa abordagem me foi apresentada em primeiro lugar. Por exemplo, quando quero testar o método CheckIn, sempre desejo que o objeto em teste seja configurado de uma maneira, mas quando testo o método CheckOut, preciso de uma configuração diferente. Bom ponto.
SonOfPirate

14

Não vejo realmente nenhuma razão convincente para dividir um teste para uma única classe em várias classes de teste. Como a idéia principal deve ser manter a coesão no nível de classe, você deve se esforçar para isso também no nível de teste. Apenas algumas razões aleatórias:

  1. Não é necessário duplicar (e manter várias versões do) código de configuração
  2. Mais fácil de executar todos os testes para uma classe a partir de um IDE, se eles estiverem agrupados em uma classe de teste
  3. Solução de problemas mais fácil com mapeamento individual entre classes de teste e classes.

Bons pontos. Se a classe em teste for um péssimo argumento, eu não descartaria várias classes de teste, onde algumas delas contêm lógica auxiliar. Eu simplesmente não gosto de regras muito rígidas. Fora isso, ótimos pontos.
Job

1
Eu eliminaria o código de configuração duplicado por ter uma classe base comum para os equipamentos de teste trabalhando contra uma única classe. Não tenho certeza se concordo com os outros dois pontos.
18711 SonOfPirate

No JUnit, por exemplo, você pode testar coisas usando diferentes corredores, o que leva à necessidade de diferentes classes.
Joachim Nilsson

Enquanto escrevo uma classe com um grande número de métodos com matemática complexa em cada um, percebo que tenho 85 testes e só tenho testes escritos para 24% dos métodos até agora. Eu me encontro aqui porque estava me perguntando se é insano dividir esse grande número de testes em categorias separadas de alguma forma, para que eu possa executar subconjuntos.
precisa

7

Se você for obrigado a dividir testes de unidade para uma classe em vários arquivos, isso pode ser uma indicação de que a própria classe é mal projetada. Não consigo pensar em nenhum cenário em que seria mais benéfico dividir os testes de unidade para uma classe que cumpra razoavelmente o Princípio de Responsabilidade Única e outras práticas recomendadas de programação.

Além disso, ter nomes de métodos mais longos em testes de unidade é aceitável, mas se isso o incomoda, você sempre pode repensar sua convenção de nomenclatura para reduzir os nomes.


Infelizmente, no mundo real, nem todas as classes aderem ao SRP et al ... e isso se torna um problema quando você está testando o código legado.
Péter Török

3
Não tenho certeza de como o SRP se relaciona aqui. Eu tenho vários métodos na minha classe e preciso escrever testes de unidade para todos os diferentes requisitos que direcionam seu comportamento. É melhor ter uma classe de teste com 50 métodos de teste ou 5 classes de teste com 10 testes cada, relacionados a um método específico na classe sob teste?
SonOfPirate

2
@SonOfPirate: se você precisar de tantos testes, isso pode significar que seus métodos estão fazendo muito. Portanto, o SRP é muito relevante aqui. Se você aplicar SRP a classes e métodos, terá muitas classes e métodos pequenos que são fáceis de testar e não precisará de tantos métodos de teste. (Mas eu concordo com Péter Török que quando você código legado teste, boas práticas estão fora da porta e você só tem que fazer o melhor que pode com o que você é dado ...)
c_maker

O melhor exemplo que posso dar é o método CheckIn, no qual temos regras de negócios que determinam quem tem permissão para executar a ação (autorização), se o objeto estiver no estado adequado a ser verificado, como se tiver feito check-out e se houver alterações tem sido feito. Teremos testes de unidade que verificam se as regras de autorização estão sendo aplicadas, além de testes para verificar se o método se comporta corretamente se CheckIn for chamado quando o objeto não tiver saída, alteração, etc. É por isso que terminamos com muitos testes. Você está sugerindo que isso está errado?
SonOfPirate

Não acho que haja respostas duras e rápidas, certas ou erradas sobre esse assunto. Se o que você está fazendo está funcionando para você, isso é ótimo. Esta é apenas a minha perspectiva sobre uma orientação geral.
FishBasketGordo

2

Um dos meus argumentos contra a separação dos testes em várias classes é que fica mais difícil para outros desenvolvedores da equipe (especialmente aqueles que não são tão conhecedores de testes) localizar os testes existentes ( Puxa, gostaria de saber se já existe um teste para isso). Gostaria de saber onde seria? ) e também onde colocar novos testes ( vou escrever um teste para esse método nesta classe, mas não tenho muita certeza se devo colocá-los em um novo arquivo ou em um existente um? )

No caso em que testes diferentes exigem configurações drasticamente diferentes, vi alguns profissionais do TDD colocarem o "acessório" ou o "setup" em diferentes classes / arquivos, mas não os próprios testes.


1

Gostaria de poder lembrar / encontrar o link em que a técnica que escolhi adotar foi demonstrada pela primeira vez. Essencialmente, eu crio uma classe única e abstrata para cada classe em teste que contém equipamentos de teste aninhados (classes) para cada membro que está sendo testado. Isso fornece a separação desejada originalmente, mas mantém todos os testes no mesmo arquivo para cada local. Além disso, isso gera um nome de classe que permite fácil agrupamento e classificação no executor de teste.

Aqui está um exemplo de como essa abordagem é aplicada ao meu cenário original:

public abstract class DocumentTests : TestBase
{
    [TestClass()]
    public sealed class CheckInShould : DocumentTests
    {
        [TestMethod()]
        public void ThrowExceptionWhenUserIsNotAuthorized()
        {
        }
    }

    [TestClass()]
    public sealed class CheckOutShould : DocumentTests
    {
        [TestMethod()]
        public void ThrowExceptionWhenUserIsNotAuthorized()
        {
        }
    }
}

Isso resulta nos seguintes testes que aparecem na lista de testes:

DocumentTests+CheckInShould.ThrowExceptionWhenUserIsNotAuthorized
DocumentTests+CheckOutShould.ThrowExceptionWhenUserIsNotAuthorized

À medida que mais testes são adicionados, eles são facilmente agrupados e classificados por nome de classe, o que também mantém todos os testes da classe em teste listados juntos.

Outra vantagem dessa abordagem que aprendi a aproveitar é a natureza orientada a objetos dessa estrutura significa que posso definir o código dentro dos métodos de inicialização e limpeza da classe base DocumentTests que serão compartilhados por todas as classes aninhadas e dentro de cada uma. classe aninhada para os testes que ela contém.


1

Tente pensar o contrário por um minuto.

Por que você teria apenas uma classe de teste por classe? Você está fazendo um teste de classe ou de unidade? Você ainda depende de aulas ?

Um teste de unidade deve estar testando um comportamento específico, em um contexto específico. O fato de sua classe já ter um CheckInmeio deve ter um comportamento que precisa dele em primeiro lugar.

Como você pensaria sobre esse pseudocódigo:

// check_in_test.file

class CheckInTest extends TestCase {
        /** @test */
        public function unauthorized_users_cannot_check_in() {
                $this->expectException();

                $document = new Document($unauthorizedUser);

                $document->checkIn();
        }
}

Agora você não está testando diretamente o checkInmétodo. Em vez disso, você está testando um comportamento (que é fazer check-in felizmente;)) e, se precisar refatorá Document-lo e dividi-lo em classes diferentes, ou mesclar outra classe, seus arquivos de teste ainda são coerentes, ainda fazem sentido, pois você nunca muda a lógica ao refatorar, apenas a estrutura.

Um arquivo de teste para uma classe simplesmente torna mais difícil refatorar sempre que você precisar, e os testes também fazem menos sentido em termos de domínio / lógica do próprio código.


0

Em geral, eu recomendaria pensar em testes como testar um comportamento da classe em vez de um método. Ou seja, alguns de seus testes podem precisar chamar os dois métodos na classe para testar o comportamento esperado.

Geralmente começo com uma classe de teste de unidade por classe de produção, mas pode eventualmente dividir essa classe de teste em várias classes de teste com base no comportamento que eles testam. Em outras palavras, eu recomendaria não dividir a classe de teste em CheckInShould e CheckOutShould, mas sim dividir pelo comportamento da unidade em teste.


0

1. Considere o que provavelmente dará errado

Durante o desenvolvimento inicial, você sabe muito bem o que está fazendo e as duas soluções provavelmente funcionarão bem.

Fica mais interessante quando um teste falha após uma alteração muito mais tarde. Existem duas possibilidades:

  1. Você quebrou seriamente CheckIn(ou CheckOut). Novamente, tanto a solução de arquivo único quanto a solução de dois arquivos estão corretas nesse caso.
  2. Você modificou ambos CheckIne CheckOut(e talvez os testes) de uma maneira que faça sentido para cada um deles, mas não para os dois juntos. Você quebrou a coerência do par. Nesse caso, dividir os testes em dois arquivos dificultará a compreensão do problema.

2. Considere para que testes são usados

Os testes têm dois propósitos principais:

  1. Verificar automaticamente se o programa ainda funciona corretamente. Se os testes estão em um arquivo ou em vários, não importa para esse fim.
  2. Ajude um leitor humano a entender o programa. Isso será muito mais fácil se os testes de funcionalidade intimamente relacionados forem mantidos juntos, porque ver sua coerência é um caminho rápido para o entendimento.

Então?

Ambas as perspectivas sugerem que manter os testes juntos pode ajudar, mas não vai doer.

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.