Dificuldades com TDD e refatoração (ou - por que isso é mais doloroso do que deveria ser?)

20

Eu queria me ensinar a usar a abordagem TDD e tinha um projeto em que vinha trabalhando há um tempo. Não era um projeto grande, então pensei que seria um bom candidato para TDD. No entanto, sinto que algo deu errado. Deixe-me dar um exemplo:

Em um nível alto, meu projeto é um suplemento para o Microsoft OneNote que me permitirá rastrear e gerenciar projetos com mais facilidade. Agora, eu também queria manter a lógica comercial para isso o mais dissociada possível do OneNote, caso decidisse criar meu próprio armazenamento personalizado e back-end algum dia.

Primeiro, comecei com um teste básico de aceitação de palavras simples para descrever o que queria que meu primeiro recurso fizesse. Parece algo assim (emburrecendo-o por questões de concisão):

  1. Cliques do usuário criar projeto
  2. Tipos de usuário no título do projeto
  3. Verifique se o projeto foi criado corretamente

Ignorando o material da interface do usuário e algum planejamento intermediário, chego ao meu primeiro teste de unidade:

[TestMethod]
public void CreateProject_BasicParameters_ProjectIsValid()
{
    var testController = new Controller();
    Project newProject = testController(A.Dummy<String>());
    Assert.IsNotNull(newProject);
}

Por enquanto, tudo bem. Vermelho, verde, refatorador, etc. Tudo bem, agora ele precisa realmente salvar coisas. Cortando alguns passos aqui, acabo com isso.

[TestMethod]
public void CreateProject_BasicParameters_ProjectMatchesExpected()
{
    var fakeDataStore = A.Fake<IDataStore>();
    var testController = new Controller(fakeDataStore);
    String expectedTitle = fixture.Create<String>("Title");
    Project newProject = testController(expectedTitle);

    Assert.AreEqual(expectedTitle, newProject.Title);
}

Eu ainda estou me sentindo bem neste momento. Ainda não tenho um armazenamento de dados concreto, mas criei a interface como previa que fosse.

Vou pular algumas etapas aqui porque esta postagem está ficando longa o suficiente, mas segui processos semelhantes e, eventualmente, chego a esse teste para meu armazenamento de dados:

[TestMethod]
public void SaveNewProject_BasicParameters_RequestsNewPage()
{
    /* snip init code */
    testDataStore.SaveNewProject(A.Dummy<IProject>());
    A.CallTo(() => oneNoteInterop.SavePage()).MustHaveHappened();
}

Isso foi bom até que eu tentei implementá-lo:

public String SaveNewProject(IProject project)
{
    Page projectPage = oneNoteInterop.CreatePage(...);
}

E há o problema exatamente onde está o "...". Percebo agora, neste ponto, que o CreatePage requer um ID de seção. Eu não percebi isso de volta quando estava pensando no nível do controlador, porque estava preocupado apenas em testar os bits relevantes para o controlador. No entanto, todo o caminho até aqui agora percebo que tenho que pedir ao usuário um local para armazenar o projeto. Agora eu tenho que adicionar um ID de local ao armazenamento de dados, depois adicionar um ao projeto, adicionar um ao controlador e adicioná-lo a TODOS os testes que já foram escritos para todas essas coisas. Tornou-se entediante muito rapidamente e não posso deixar de sentir que seria mais rápido se eu desenhasse o design com antecedência, em vez de deixá-lo ser projetado durante o processo TDD.

Alguém pode me explicar se fiz algo errado nesse processo? Existe alguma maneira de evitar esse tipo de refatoração? Ou isso é comum? Se é comum, existem maneiras de torná-lo mais indolor?

Obrigado a todos!

Landon
fonte
Você receberá alguns comentários muito interessantes se publicar este tópico neste fórum de discussão: groups.google.com/forum/#!forum/…, especificamente para tópicos do TDD.
Chuck Krutsinger
1
Se você precisar adicionar algo a todos os seus testes, parece que seus testes foram mal escritos. Você deve refatorar seus testes e considerar o uso de um acessório sensato.
Dave Hillier

Respostas:

19

Embora o TDD seja (justamente) apresentado como uma maneira de projetar e expandir seu software, ainda é uma boa idéia pensar sobre o design e a arquitetura com antecedência. IMO, "esboçar o design antes do tempo" é um jogo justo. Muitas vezes, isso estará em um nível mais alto do que as decisões de design às quais você será conduzido através do TDD.

Também é verdade que quando as coisas mudam, você geralmente precisará atualizar os testes. Não há como eliminar isso completamente, mas há algumas coisas que você pode fazer para tornar seus testes menos frágeis e minimizar a dor.

  1. Tanto quanto possível, mantenha os detalhes da implementação fora de seus testes. Isso significa apenas testar através de métodos públicos e, sempre que possível, favorecer a verificação baseada no estado em vez da interação . Em outras palavras, se você testar o resultado de algo e não as etapas para chegar lá, seus testes deverão ser menos frágeis.

  2. Minimize a duplicação no seu código de teste, como faria no código de produção. Este post é uma boa referência. No seu exemplo, parece doloroso adicionar a IDpropriedade ao seu construtor porque você o invocou diretamente em vários testes diferentes. Em vez disso, tente extrair a criação do objeto para um método ou inicializá-lo uma vez para cada teste em um método de inicialização de teste.

jhewlett
fonte
Eu li os méritos do estado versus da interação e o entendi na maioria das vezes. No entanto, não vejo como isso é possível em todos os casos sem expor as propriedades EXPLICITAMENTE para o teste. Veja o meu exemplo acima. Não sei como verificar se o armazenamento de dados foi realmente chamado sem usar uma asserção para "MustHaveBeenCalled". Quanto ao ponto 2, você está absolutamente correto. Acabei fazendo isso depois de todas as edições, mas queria garantir que minha abordagem fosse geralmente consistente com as práticas aceitas de TDD. Obrigado!
Landon
@Landon Há casos em que o teste de interação é mais apropriado. Por exemplo, verificar se uma chamada foi feita para um banco de dados ou serviço da web. Basicamente, sempre que você precisar isolar seu teste, especialmente de um serviço externo.
jhewlett
@Landon Sou um "classicista convencido", por isso não tenho muita experiência com testes baseados em interações ... Mas você não precisa fazer uma afirmação para "MustHaveBeenCalled". Se você estiver testando uma inserção, poderá usar uma consulta para ver se ela foi inserida. PS: Eu uso stubs devido a considerações de desempenho ao testar tudo, menos a camada do banco de dados.
Hbas
@ jhewlett Essa é a conclusão a que cheguei também. Obrigado!
Landon
@ Hbas Não há banco de dados para consulta. Concordo que seria o caminho mais direto a seguir se eu tivesse um, mas estou adicionando isso a um bloco de anotações do OneNote. O melhor que posso fazer é adicionar um método Get à minha classe auxiliar de interoperabilidade para tentar puxar a página. Eu poderia escrever o teste para fazer isso, mas senti como se estivesse testando duas coisas ao mesmo tempo: salvei isso? e Minha classe auxiliar recupera páginas corretamente? Embora, acho que em algum momento seus testes possam ter que confiar em outro código testado em outro lugar. Obrigado!
Landon
10

... Não posso deixar de sentir que eu teria percebido isso mais rapidamente se eu esboçasse o design com antecedência, em vez de deixá-lo ser projetado durante os processos TDD ...

Talvez talvez não

Por um lado, o TDD funcionou bem, oferecendo testes automatizados à medida que você constrói a funcionalidade e quebrando imediatamente quando você precisava alterar a interface.

Por outro lado, talvez se você tivesse iniciado com o recurso de alto nível (SaveProject) em vez de um recurso de nível inferior (CreateProject), teria notado a falta de parâmetros mais cedo.

Então, novamente, talvez você não teria. É um experimento irrepetível.

Mas se você estiver procurando uma lição para a próxima vez: comece do topo. E pense no design o quanto quiser primeiro.

Steven A. Lowe
fonte
0

https://frontendmasters.com/courses/angularjs-and-code-testability/ De cerca de 2:22:00 até o fim (cerca de 1 hora). Lamentamos que o vídeo não seja gratuito, mas não encontrei um que o explique tão bem.

Uma das melhores apresentações para escrever código testável está nesta lição. É uma classe AngularJS, mas a parte de teste é baseada no código java, principalmente porque o que ele está falando não tem nada a ver com a linguagem e tudo a ver com a escrita de um bom código testável em primeiro lugar.

A mágica é escrever código testável, em vez de escrever testes de código. Não se trata de escrever código que finge ser um usuário.

Ele também passa algum tempo escrevendo as especificações na forma de asserções de teste.

boatcoder
fonte