A melhor maneira de testar métodos de unidade que chamam outros métodos dentro da mesma classe

35

Recentemente, discuti com alguns amigos qual dos 2 métodos a seguir é melhor para stub retornar resultados ou chamadas para métodos dentro da mesma classe e métodos dentro da mesma classe.

Este é um exemplo muito simplificado. Na realidade, as funções são muito mais complexas.

Exemplo:

public class MyClass
{
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionB()
     {
         return new Random().Next();
     }
}

Então, para testar isso, temos 2 métodos.

Método 1: Use Funções e Ações para substituir a funcionalidade dos métodos. Exemplo:

public class MyClass
{
     public Func<int> FunctionB { get; set; }

     public MyClass()
     {
         FunctionB = FunctionBImpl;
     }

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionBImpl()
     {
         return new Random().Next();
     }
}

[TestClass]
public class MyClassTests
{
    private MyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new MyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionB = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

Método 2: Tornar os membros virtuais, derivar classe e na classe derivada usar Funções e Ações para substituir a funcionalidade Exemplo:

public class MyClass
{     
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected virtual int FunctionB()
     {
         return new Random().Next();
     }
}

public class TestableMyClass
{
     public Func<int> FunctionBFunc { get; set; }

     public MyClass()
     {
         FunctionBFunc = base.FunctionB;
     }

     protected override int FunctionB()
     {
         return FunctionBFunc();
     }
}

[TestClass]
public class MyClassTests
{
    private TestableMyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new TestableMyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionBFunc = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

Quero saber o que é melhor e também POR QUE?

Atualização: NOTA: A FunctionB também pode ser pública

tranceru1
fonte
Seu exemplo é simples, mas não exatamente correto. FunctionAretorna um bool, mas apenas define uma variável local xe não retorna nada.
Eric P.
1
Neste exemplo em particular, a FunctionB pode estar public staticem uma classe diferente.
Para a Revisão de código, espera-se que você publique o código real, não uma versão simplificada dele. Veja o FAQ. Como está, você está fazendo uma pergunta específica sem procurar uma revisão de código.
Winston Ewert 27/02
1
FunctionBestá quebrado por design. new Random().Next()quase sempre está errado. Você deve injetar a instância de Random. ( RandomÉ também uma classe mal concebidos, o que pode causar alguns problemas adicionais)
CodesInChaos
em uma nota mais geral, o DI via delegados é absolutamente bom
jk.

Respostas:

32

Editado após a atualização original do pôster.

Isenção de responsabilidade: não é um programador de C # (principalmente Java ou Ruby). Minha resposta seria: eu não testaria nada e acho que não deveria.

A versão mais longa é: métodos privados / protegidos não fazem parte da API, são basicamente opções de implementação, que você pode decidir revisar, atualizar ou jogar completamente fora sem causar nenhum impacto externo.

Suponho que você tenha um teste em FunctionA (), que é a parte da classe que é visível do mundo externo. Deve ser o único que tem um contrato para implementar (e que pode ser testado). Seu método particular / protegido não tem contrato para cumprir e / ou testar.

Consulte uma discussão relacionada lá: https://stackoverflow.com/questions/105007/should-i-test-private-methods-or-only-public-ones

Após o comentário , se FunctionB for público, testarei ambos usando o teste de unidade. Você pode pensar que o teste da FunctionA não é totalmente "unidade" (como chama FunctionB), mas eu não ficaria muito preocupado com isso: se o teste da FunctionB funcionar, mas não o da FunctionA, significa claramente que o problema não está no subdomínio da FunctionB, que é bom o suficiente para mim como discriminador.

Se você realmente deseja separar totalmente os dois testes, eu usaria algum tipo de técnica de zombaria para zombar da FunctionB ao testar a FunctionA (normalmente, retorne um valor correto conhecido fixo). Não tenho o conhecimento do ecossistema C # para aconselhar uma biblioteca de zombaria específica, mas você pode olhar para esta pergunta .

Martin
fonte
2
Concordo totalmente com a resposta do @Martin. Ao escrever testes de unidade para a classe, você não deve testar métodos . O que você está testando é um comportamento de classe , que o contrato (a declaração que classe deve fazer) é satisfeito. Assim, os testes de unidade devem cobrir todos os requisitos expostos para esta classe (usando métodos / propriedades públicas), incluindo casos excepcionais
Olá, obrigado pela resposta, mas não respondeu à minha pergunta. Não importa se o FunctionB foi privado / protegido. Também pode ser público e ainda ser chamado a partir de FunctionA.
A maneira mais comum de lidar com esse problema, sem reprojetar a classe base, é subclassear MyClasse substituir o método pela funcionalidade que você deseja stub. Também pode ser uma boa ideia atualizar sua pergunta para incluir que FunctionBpode ser pública.
Eric P.
1
protectedOs métodos fazem parte da superfície pública de uma classe, a menos que você garanta que não haja implementações de sua classe em assemblies diferentes.
CodesInChaos
2
O fato de a FunctionA chamar FunctionB é um detalhe irrelevante de uma perspectiva de teste de unidade. Se os testes da FunctionA forem escritos corretamente, é um detalhe da implementação que poderá ser refatorado posteriormente sem interromper os testes (desde que o comportamento geral da FunctionA permaneça inalterado). O problema real é que a recuperação de um número aleatório pela FunctionB precisa ser feita com um objeto injetado, para que você possa usar uma simulação durante o teste para garantir que um número conhecido seja retornado. Isso permite testar entradas / saídas conhecidas.
Dan Lyons
11

Eu subscrevo a teoria de que, se uma função é importante para testar, ou é importante substituir, é importante o suficiente para não ser um detalhe de implementação privada da classe em teste, mas para ser um detalhe de implementação pública de uma classe diferente .

Então, se estou em um cenário em que tenho

class A 
{
     public B C()
     {
         D();
     }

     private E D();
     {
         // i actually want to control what this produces when I test C()
         // or this is important enough to test on its own
         // and, typically, both of the above
     }
}

Então eu vou refatorar.

class A 
{
     ICollaborator collaborator;

     public A(ICollaborator collaborator)
     {
         this.collaborator = collaborator;
     }

     public B C()
     {
         collaborator.D();
     }
}

Agora, tenho um cenário em que D () é testável de forma independente e totalmente substituível.

Como meio de organização, meu colaborador pode não viver no mesmo nível de namespace. Por exemplo, se Aestiver no FooCorp.BLL, meu colaborador poderá ter outra camada, como no FooCorp.BLL.Collaborators (ou qualquer outro nome que seja apropriado). Meu colaborador pode ainda estar visível apenas dentro da montagem por meio do internalmodificador de acesso, que eu também exporia aos meus projetos de teste de unidade por meio do InternalsVisibleToatributo assembly. O ponto principal é que você ainda pode manter sua API limpa, no que diz respeito aos chamadores, enquanto produz código verificável.

Anthony Pegram
fonte
Sim, se o ICollaborator precisar de vários métodos. Se você tem um objeto cujo único trabalho é quebrar um único método, prefiro vê-lo substituído por um delegado.
jk.
Você teria que decidir se um delegado nomeado faz sentido ou se uma interface faz sentido, e eu não decidirei por você. Pessoalmente, não sou avesso a classes de método únicas (públicas). Quanto menor, melhor, à medida que se tornam cada vez mais fáceis de entender.
Anthony Pegram
0

Adicionando o que Martin aponta,

Se o seu método for privado / protegido - não o teste. É interno à classe e não deve ser acessado fora da classe.

Nas duas abordagens mencionadas, tenho essas preocupações -

Método 1 - Isso realmente altera a classe sob o comportamento do teste.

Método 2 - Na verdade, isso não testa o código de produção, mas testa outra implementação.

No problema declarado, vejo que a única lógica de A é ver se a saída da FunçãoB é par. Embora ilustrativa, a FunctionB fornece valor aleatório, o que é difícil de testar.

Espero um cenário realista em que possamos configurar o MyClass para sabermos o que FunctionB retornaria. Então nosso resultado esperado é conhecido, podemos chamar FunctionA e afirmar o resultado real.

Srikanth Venugopalan
fonte
3
protectedé quase o mesmo que public. Somente privatee internalsão detalhes de implementação.
CodesInChaos
@codeinchaos - estou curioso aqui. Para um teste, os métodos protegidos são 'particulares', a menos que você modifique os atributos do assembly. Apenas tipos derivados têm acesso a membros protegidos. Com exceção do virtual, não vejo por que o protegido deve ser tratado de maneira semelhante ao público em um teste. você poderia elaborar por favor?
Srikanth Venugopalan
Como essas classes derivadas podem estar em assemblies diferentes, elas são expostas a códigos de terceiros e, portanto, fazem parte da superfície pública da sua classe. Para testá-los, você pode internal protectedcriá -los , usar um auxiliar de reflexão particular ou criar uma classe derivada em seu projeto de teste.
CodesInChaos 28/02
@CodesInChaos, concordou que a classe derivada pode estar em montagens diferentes, mas o escopo ainda está restrito aos tipos base e derivados. Modificar o modificador de acesso apenas para torná-lo testável é algo que me deixa um pouco nervoso. Eu fiz isso, mas parece ser um antipadrão para mim.
Srikanth Venugopalan
0

Eu pessoalmente uso o Method1, ou seja, transformando todos os métodos em Actions ou Funcs, pois isso melhorou bastante a testabilidade do código para mim. Como em qualquer solução, existem prós e contras com essa abordagem:

Prós

  1. Permite uma estrutura de código simples, onde o uso de Padrões de Código apenas para Teste de Unidade pode adicionar complexidade adicional.
  2. Permite que as classes sejam seladas e para a eliminação de métodos virtuais exigidos por estruturas de zombaria populares como o Moq. A selagem de classes e a eliminação de métodos virtuais os tornam candidatos a otimizações embutidas e outras otimizações de compilador. ( https://msdn.microsoft.com/en-us/library/ff647802.aspx )
  3. Simplifica a testabilidade, pois substituir a implementação do Func / Action em um teste de unidade é tão simples quanto atribuir um novo valor ao Func / Action
  4. Também permite testar se um Func estático foi chamado de outro método, pois os métodos estáticos não podem ser zombados.
  5. Fácil de refatorar métodos existentes para Funcs / Actions, pois a sintaxe para chamar um método no site de chamada permanece a mesma. (Para momentos em que você não pode refatorar um método em Func / Action, consulte contras)

Contras

  1. Os Func / Ações não podem ser usados ​​se sua classe puder ser derivada, pois os Func / Ações não possuem caminhos de herança como Métodos
  2. Não é possível usar os parâmetros padrão. Criar um Func com parâmetros padrão implica criar um novo delegado, o que pode tornar o código confuso, dependendo do caso de uso
  3. Não é possível usar a sintaxe do parâmetro nomeado para chamar métodos como algo (firstName: "S", lastName: "K")
  4. A maior desvantagem é que você não tem acesso à referência 'this' em Funcs and Actions e, portanto, quaisquer dependências na classe devem ser explicitamente passadas como parâmetros. É bom como você conhece todas as dependências, mas ruim se você tiver muitas propriedades das quais o seu Func dependerá. Sua milhagem variará com base no seu caso de uso.

Portanto, para resumir, usar o teste Func e Ações para unidade é ótimo se você souber que suas aulas nunca serão substituídas.

Além disso, geralmente não crio propriedades para Funcs, mas as inline diretamente como tal

public class MyClass
{
     public Func<int> FunctionB = () => new Random().Next();

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }
}

Espero que isto ajude!

Salman Hasrat Khan
fonte
-1

Usando Mock é possível. Nuget: https://www.nuget.org/packages/moq/

E acredite em mim, é bastante simples e faz sentido.

public class SomeClass
{
    public SomeClass(int a) { }

    public void A()
    {
        B();
    }

    public virtual void B()
    {

    }
}

[TestFixture]
public class Test
{
    [Test]
    public void Test_A_Calls_B()
    {
        var mockedObject = new Mock<SomeClass>(5); // You can also specify constructor arguments.
        //You can also setup what a function can return.
        var obj = mockedObject.Object;
        obj.A();

        Mock.Get(obj).Verify(x=>x.B(),Times.AtLeastOnce);//This test passes
    }
}

O mock precisa de um método virtual para substituir.

BrightFuture1029
fonte