Como testar métodos privados em C#. (.net core)

Como testar métodos privados em C#. (.net core)

Overview

Neste post aventureiro, mergulharemos nas profundezas da programação C# para desvendar um dos mistérios mais intrigantes: é possível, e mais importante, é correto testar métodos privados? Armados com XUnit e .net Core 3.1, exploraremos técnicas, desafios e soluções que desafiam as convenções tradicionais de testes unitários. Prepare-se para uma viagem repleta de códigos, dilemas éticos e alternativas práticas que podem justamente mudar a forma como abordamos os testes em nossos projetos.

Neste post, mostro como testar métodos privados no C#. Falo um pouco também sobre o quão certo (ou errado) é fazer este tipo de teste.

A primeira coisa que a muita gente vai pensar, quando ver este post é: “Isso é errado! É anti-pattern! O test unitário é apenas para verificar a interface publica! Você não pode fazer isso!

Bom, é possível fazer e a linguagem de programação fornece meios para isso. Então eu posso fazer isso, mas… devo fazer isso?

Existe a escola de pensamento que diz que, se você precisa criar um método privado, então existe uma nova classe escondida na que você está trabalhando. Então o ideal seria extrair os métodos privados e criar outra classe com estes mesmos métodos (só que públicos). Este tipo de dica/diretiva/aviso/conselho é interessante, mas nem sempre este tipo de saída vai funcionar na prática. Seja por questões de tempo, de complexidade ou alguma outra característica do projeto.

Outro ponto a favor do teste de métodos privados é que os métodos privados podem ser considerados como uma forma intrínseca de evitar que acabe repetindo pedaços de código, o que iria ferir um dos princípios do SOLID. Sendo assim, criar testes para estes métodos vão fazer sua suite ser mais abrangente e granular. Isso faz com que você consiga garantir que as “peças” que compõe sua API pública funcionam individualmente. Depois você pode criar outros testes, mais voltados para o negócio, utilizando a API pública.

Se você está com a necessidade de testar métodos privados, uma abordagem que eu acho interessante:

  1. Crie os testes unitários para todas as “peças”, garantindo que tudo funcione individualmente.
  2. Crie testes usando alguma ferramenta tipo o SpecFlow (implementação do Cucumber para .net), pegando a interface pública e alinhando os testes com a área de negócio.

No fim das contas, você deve conversar com a sua equipe e/ou com a equipe de arquitetura para saber se existe algum impedimento. Abaixo alguns clichês muito utilizados neste tipo de discussão:

  1. Não existe bala de prata;
  2. A resposta inicial para as perguntas em ti é sempre: depende.

Chega de conversa conceitual. Vamos as implementações.

Notas sobre as implementações

  • O projeto foi criado utilizando XUnit e .net Core 3.1.
  • No .net core 3.1 não existe a classe PrivateObject.
  • Criei a seguinte interface:
namespace demo_test_private_method
{
    public interface IComplexOperations
    {
        bool ComplexOperations(int a, int b);
        bool ComplexOperationsFiveAndTen();
    }
}
  • E implementei ela na classe que será testada:
namespace demo_test_private_method
{
    public class ClassWithPrivateMethods: IComplexOperations
    {
        private static int _doSum(int a, int b)
        {
            return a + b;
        }

        private static int _doMultiply(int a, int b)
        {
            return a * b;
        }
        
        public bool ComplexOperations(int a, int b)
        {
            return _doMultiply(a, b) >= _doSum(a, b);
        }

        public bool ComplexOperationsFiveAndTen()
        {
            return ComplexOperations(5, 10);
        }
    }
}

O objetivo é testar os métodos _doSum e _doMultiply.

Implementação

Para fazer isso, precisamos alterar a classe que será testada.

Ela ficará assim:

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("demo_test_private_method.UnitTests")]
namespace demo_test_private_method
{
    public class ClassWithPrivateMethods: IComplexOperations
    {
        internal int _doSum(int a, int b)
        {
            return a + b;
        }

        internal int _doMultiply(int a, int b)
        {
            return a * b;
        }
        
        public bool ComplexOperations(int a, int b)
        {
            return _doMultiply(a, b) >= _doSum(a, b);
        }

        public bool ComplexOperationsFiveAndTen()
        {
            return ComplexOperations(5, 10);
        }
    }
}

Existem duas diferenças:

  1. Foi inserida uma linha, logo acima da declaração do namespace. Ela indica que os métodos marcados como internal podem ser vistos pelo namespace indicado. Sendo assim, você deve inserir esta linha na sua classe e passar o namespace do projeto que vai criar para testa-la.
  2. Os métodos que forem testados e estiverem como private devem ser alterados para internal.

Pronto. Não precisa fazer mais alterações. Os testes ficariam assim:

using Xunit;

namespace demo_test_private_method.UnitTests
{
    public class UnitTest1
    {
        [Fact]
        public void TestDoMultiply()
        {
            var testClass = new ClassWithPrivateMethods();
            Assert.Equal(50, testClass._doMultiply(5, 10));
        }
        
        [Fact]
        public void TestDoSum()
        {
            var testClass = new ClassWithPrivateMethods();
            Assert.Equal(15, testClass._doSum(5, 10));
        }
    }
}

Diferença entre private e internal

A grande diferença entre private e internal, para este caso, é que outras classes do mesmo assembly conseguirão ver os métodos que forem alterados para internal.

Vale lembrar que, se você estiver utilizando interfaces, isso não será um problema. Veja a classe de teste abaixo:

namespace demo_test_private_method
{
    public class AnotherClass
    {
        public static void TestClass()
        {
            //Internal Methods available here.
            var cwpm = new ClassWithPrivateMethods();
            cwpm._doMultiply(1, 2);
        }
        
        public static void TestInterface()
        {
            //Only interface methods available here.
            IComplexOperations cwpm = new ClassWithPrivateMethods();
            cwpm.ComplexOperationsFiveAndTen();
        }
    }
}

O método TestClass instancia a classe diretamente e por isso ela tem acesso aos métodos internos. Já o método TestInterface cria uma instancia da classe, mas utiliza a interface e, por consequencia, não tem acesso ao metodos internos.

Mais informações no site com documentação da Microsoft: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/access-modifiers

Sei que existe um certo inconveniente em ter que trocar isso, mas com o PrivateObject não foi incluido no dotnet core 3.1, a solução acaba ficando assim.

Criei um repositório no github com este exemplo.

Espero ter ajudado!