Como testar a unidade uma função que é refatorada para o padrão de estratégia?

10

Se eu tiver uma função no meu código que seja como:

class Employee{

    public string calculateTax(string name, int salary)
    {
        switch (name)
        {
            case "Chris":
                doSomething($salary);
            case "David":
                doSomethingDifferent($salary);
            case "Scott":
               doOtherThing($salary);               
       }
}

Normalmente eu refatoraria isso para usar o Ploymorphism usando uma classe de fábrica e um padrão de estratégia:

public string calculateTax(string name)
{
    InameHandler nameHandler = NameHandlerFactory::getHandler(name);
    nameHandler->calculateTax($salary);
}

Agora, se eu estivesse usando TDD, teria alguns testes que funcionam no original calculateTax()antes de refatorar.

ex:

calculateTax_givenChrisSalaryBelowThreshold_Expect111(){}    
calculateTax_givenChrisSalaryAboveThreshold_Expect111(){}

calculateTax_givenDavidSalaryBelowThreshold_Expect222(){}   
calculateTax_givenDavidSalaryAboveThreshold_Expect222(){} 

calculateTax_givenScottSalaryBelowThreshold_Expect333(){}
calculateTax_givenScottSalaryAboveThreshold_Expect333(){}

Após a refatoração, terei uma classe Factory NameHandlerFactorye pelo menos três implementações InameHandler.

Como devo proceder para refatorar meus testes? Devo excluir o teste de unidade claculateTax()de EmployeeTestse criar uma classe de teste para cada implementação de InameHandler?

Devo testar a classe Factory também?

Songo
fonte

Respostas:

6

Os testes antigos são ótimos para verificar se calculateTaxainda funcionam como deveriam. No entanto, você não precisa de muitos casos de teste para isso, apenas três (ou talvez mais, se quiser testar o tratamento de erros também, usando valores inesperados de name).

Cada um dos casos individuais (no momento implementado em doSomethinget al.) Também deve ter seu próprio conjunto de testes, que testam os detalhes internos e casos especiais relacionados a cada implementação. Na nova configuração, esses testes podem / devem ser convertidos em testes diretos na respectiva classe de estratégia.

Prefiro remover testes de unidade antigos apenas se o código que eles exercitarem e a funcionalidade implementada deixarem de existir completamente. Caso contrário, o conhecimento codificado nesses testes ainda é relevante, apenas os testes precisam ser refatorados.

Atualizar

Pode haver alguma duplicação entre os testes de calculateTax(vamos chamá-los de testes de alto nível ) e os testes para as estratégias de cálculo individuais ( testes de baixo nível ) - isso depende da sua implementação.

Acho que a implementação original de seus testes afirma o resultado do cálculo de imposto específico, verificando implicitamente que a estratégia de cálculo específica foi usada para produzi-lo. Se você mantiver esse esquema, haverá duplicação de fato. No entanto, como o @Kristof sugeriu, você também pode implementar os testes de alto nível usando zombarias, para verificar apenas se o tipo certo de estratégia (falsa) foi selecionado e invocado por calculateTax. Nesse caso, não haverá duplicação entre os testes de nível alto e baixo.

Portanto, se a refatoração dos testes afetados não for muito cara, eu prefiro a última abordagem. No entanto, na vida real, ao fazer uma refatoração maciça, eu tolero uma pequena quantidade de duplicação de código de teste se isso me poupar tempo suficiente :-)

Devo testar a classe Factory também?

Mais uma vez, depende. Observe que os testes calculateTaxefetivamente testam a fábrica. Portanto, se o código de fábrica é um switchbloco trivial como o código acima, esses testes podem ser tudo o que você precisa. Mas se a fábrica fizer algumas coisas mais complicadas, convém dedicar alguns testes especificamente para isso. Tudo se resume a quantos testes você precisa ter certeza de que o código em questão realmente funciona. Se, ao ler o código - ou analisar os dados de cobertura de código - você vir caminhos de execução não testados, dedique mais alguns testes para exercê-los. Repita isso até estar totalmente confiante no seu código.

Péter Török
fonte
Modifiquei um pouco o código para torná-lo mais próximo do meu código prático real. Agora, uma segunda entrada salarypara a função calculateTax()foi adicionada. Dessa forma, acho que duplicarei o código de teste para a função original e as 3 implementações da classe de estratégia.
Songo
@Ongo, veja minha atualização.
Péter Török
5

Começarei dizendo que não sou especialista em TDD ou teste de unidade, mas aqui está como eu testaria isso (usarei código pseudo-semelhante):

CalculateTaxDelegatesToNameHandler()
{
    INameHandlerFactory fakeNameHandlerFactory = Fake(INameHandlerFactory);
    INameHandler fakeNameHandler = Fake(INameHandler);

    A.Call.To(fakeNameHandlerFactory.getHandler("John")).Returns(fakeNameHandler);

    Employee employee = new Employee(fakeNameHandlerFactory);
    employee.CalculateTax("John");

    Assert.That.WasCalled(fakeNameHandler.calculateTax());
}

Então, eu testaria se o calculateTax()método da classe empregado pede corretamente NameHandlerFactorya NameHandlere chama o calculateTax()método retornado NameHandler.

Kristof Claes
fonte
hmmmm, você quer dizer que eu deveria fazer o teste um teste comportamental (testar que certas funções foram chamadas) e fazer as asserções de valor nas classes delegadas?
Songo
Sim, é o que eu faria. Na verdade, eu escreveria testes separados para o NameHandlerFactory e o NameHandler. Quando você os possui, não há motivo para testar sua funcionalidade novamente no Employee.calculateTax()método. Dessa forma, você não precisa adicionar testes de funcionários extras ao introduzir um novo NameHandler.
27512 Kristof Claes
3

Você está fazendo uma aula (funcionário que faz tudo) e fazendo 3 grupos de aulas: a fábrica, o funcionário (que contém apenas uma estratégia) e as estratégias.

Então faça 3 grupos de testes:

  1. Teste a fábrica isoladamente. Ele lida com as entradas corretamente. O que acontece quando você passa em um desconhecido?
  2. Teste o funcionário isoladamente. Você pode definir uma estratégia arbitrária e ela funciona conforme o esperado? O que acontece se não houver estratégia ou conjunto de fábrica? (se isso for possível no código)
  3. Teste as estratégias isoladamente. Cada um deles executa a estratégia que você espera? Eles lidam com entradas de fronteira ímpares de maneira consistente?

É claro que você pode fazer testes automatizados para toda a shebang, mas agora são mais como testes de integração e devem ser tratados como tal.

Telastyn
fonte
2

Antes de escrever qualquer código, eu começaria com um teste para uma fábrica. Zombando das coisas de que preciso, eu me forçaria a pensar nas implementações e nos casos de uso.

Então, eu implementaria uma fábrica e continuaria com um teste para cada implementação e, finalmente, as próprias implementações para esses testes.

Finalmente, eu removia os testes antigos.

Patkos Csaba
fonte
2

Minha opinião é que você não deve fazer nada, o que significa que não deve adicionar novos testes.

Enfatizo que essa é uma opinião e, na verdade, depende da maneira como você percebe as expectativas do objeto. Você acha que o usuário da classe gostaria de fornecer uma estratégia para o cálculo de impostos? Se ele não se importa, os testes devem refletir isso, e o comportamento refletido nos testes de unidade deve ser que eles não se importem se a classe começou a usar um objeto de estratégia para calcular impostos.

Na verdade, eu tive esse problema várias vezes ao usar o TDD. Penso que o principal motivo é que um objeto de estratégia não é uma dependência natural, em vez de dizer uma dependência de limite arquitetural como um recurso externo (um arquivo, um banco de dados, um serviço remoto, etc.). Como não é uma dependência natural, geralmente não baseio o comportamento da minha classe nessa estratégia. Meu instinto é que só devo mudar meus testes se as expectativas da minha classe tiverem mudado.

Há um ótimo post do tio Bob, que fala exatamente sobre esse problema ao usar o TDD.

Eu acho que a tendência de testar cada classe separada é o que está matando o TDD. Toda a beleza do TDD é que você usa testes para estimular esquemas de design e não vice-versa.

Rafi Goldfarb
fonte