Elemar DEV

Tecnologia e desenvolvimento

Sobre abstração e testes

Olá. Tudo certo?!

Retornando (finalmente) as atividades aqui no blog, resolvi trazer meus “dois cents” para uma discussão interessante que ocorreu outro dia no grupo .NET Brasil.

Estou optando por esse post para conseguir dar uma resposta abrangente (e concentrada) para toda a discussão. Também por ser uma forma mais fácil de eu organizar meus pensamentos e compartilhar minhas opções. Há bem mais na discussão do que eu trouxe para cá, então, recomendo que você gaste algum tempo lendo a thread.

Vejamos…

A questão original

Vamos a questão original (proposta por Francisco Hisashi Berrocal):

Muita gente cria interfaces para poder mockar os objetos em testes.

Na minha opinião, você deve somente criar interfaces quando você realmente precisa delas. Além disso, o números de interfaces em um projeto não reflete a abstração do código dele.
Se você nunca vai ter duas, é claro que a interface é desnecessária. Então como testar seu código sem a abstração por Interfaces?
Há a possibilidade de criar classes concretas com métodos virtuais para podermos mockar esses objetos em testes. Mas aí temos um cenário onde um outro desenvolvedor pode herdar sua classe e arruinar seu desgin.
Então, a pergunta é:
Como abstrair seu código e garantir a testabilidade dele, sem prejudicar o design do projeto?
Antes de qualquer coisa, qualquer alteração no design que vise apenas viabilizar uma estratégia de testes (ou seja, que não represente melhoria do design em si) nunca se justifica.
Arrisco dizer que essa “necessidade” é um claro feedback do código (para mim, os melhores) de que algo não foi bem projetado/implementado.

O (perigoso) vício por abstrações e o BDUF

Consideremos a opinião do Ricardo Noronha:

Eu pessoalmente acho que embora a classe nunca seja substituída, ter uma interface que a represente traz outros benefícios além de simplesmente mockar.

Segundo que o sistema pode não ter a perspectiva de mudar a médio prazo, mas dificilmente não mudará a longo prazo, algo como 5, 10 anos. Assim, com as interfaces você admite a mudança, mesmo sendo improvável e num momento distante, mas a flexibilidade para a mudança já parte da base do projeto.

Outro ponto, é que criar uma interface não exige tanto esforço assim, pode criar a classe normalmente, e utilizá-la no código diretamente, mas para criar uma interface hoje, tanto pode extraí-la com o refactor do vs, como de outra ferramente de sua preferência, é algo quase automático se já tiver a classe.

Há cenários onde a explicitação da interface se justifica inicialmente por sua “clara independência”. Por exemplo, quando considero a criação de um repositório, há uma clara distinção entre a “interface” que vou fornecer e a implementação que irei utilizar. Logo, é interessante, como desenvolvedor, perceber e evidenciar essa distinção.
Delinear interfaces colabora para a redução do acoplamento. Declarar “dependência” para uma interface é muito menos grave do que declarar dependência para uma implementação. Afinal, acaba garantindo, por exemplo, que eu não transfira para os clientes detalhes da implementação (o que seria desastroso). Voltando ao exemplo do repositório, criar uma interface  ajuda a garantir que o cliente (direto) NUNCA precise fornecer uma string de conexão.
Interfaces ajudam o programador a declarar melhor sua intenção! Elas devem ser criadas por serem necessárias naquela hora, seja para melhorar a legibilidade, seja para reduzir o acoplamento.
Por outro lado, criar uma interface porque “um dia, quem sabe” ela será usada é uma péssima prática. Significa adicionar complexidade acidental e isso nunca é bom. O VS ajuda a criar, mas não ajuda a manter – temos que lembrar disso.

Uma dúvida honesta: E o que fazer os testes?

Mais adiante, na mesma discussão, surge uma dúvida (ou provocação) interessante proposta pelo Bernardo Bosak de Resende:

Você pretende fazer testes? Pretende mockar ou criar fake objects?

Caso sim, como pretende fazê-los sem abstrações?
Para começar, vamos falar sobre testes de unidade. Melhor, falemos sobre o que é uma “unidade”.
Unidade é o menor bloco indivisível para um contexto. Se um método tem dependência forte para uma implementação concreta, então, método e classe formam uma unidade – e isso nem sempre é ruim.
Se um método tem dependência forte para a implementação de um repositório, então, (infelizmente), método e repositório formam uma unidade (estão acoplados). É nessas horas que temos a oportunidade de revisar o design e aliviar o acoplamento (afinal, a dependência real deveria ser apenas para a interface e não para a implementação).
Por outro lado, há diversos cenários onde não há essa divisão clara entre interface e implementação. O exemplo mais latente são as classes de modelo. Não vejo sentido em criar uma classe Supplier e uma interface ISupplier, por exemplo.

Não use atalhos! Não use “virtual” de forma irresponsável

O Francisco Berrocal foi preciso em sua contribuição. Veja:

É possível mockar objetos concretos com métodos marcados como virtual. Eliminaria as inúmeras Interfaces, porém um terceiro desenvolvedor poderia arruinar seu design, herdando a classe e mudando seu comportamento.
Para mim, toda classe que não tenha sido planejada para ser  “herdada” deveria ser marcada como “sealed”. O mesmo acontece com os métodos.
Nossa obrigação é escrever códigos que revelem a intenção do design – não código que facilite testes. Mais uma vez, usar virtual apenas para “contornar” a criação de uma interface é um TERRÍVEL bad smell.

Concluindo

Mais uma vez, vemos os testes sendo instrumentos de feedback sobre a qualidade do nosso design e de nossa capacidade de produzir código mais efetivo.

Gostaria, de compartilhar as “regras para um design simples” do Kent Beck. Segundo ele, um design é simples se:

  1. Roda todos os testes (é possível escrever e rodar testes para o código);
  2. Não tem duplicações;
  3. Expressa a intenção do desenvolvedor;
  4. Minimiza a quantidade de classes e métodos.

O uso adequado de interfaces alivia o monstro do acoplamento, melhorando a manutenabilidade, diminuindo unidades a tamanhos aceitáveis, e escondendo detalhes de implementação. Além disso, (para mim, o mais importante) colabora para revelar claramente a intenção do desenvolvedor.

O uso inadequado de interfaces aumenta a quantidade de artefatos desnecessários (classes e interfaces), gera duplicação de código (assinaturas dos métodos) e “nubla” a intenção do design.

A criação adequada de interfaces passa pela compreensão mais profunda dos limites entre “interface pública” e “implementação”.

Por favor, não use testes como justificativa para um design pobre.

8 comentários em “Sobre abstração e testes

  1. Quanto a Testes:
    A ideia de contexto pode ser relativa: Contextos podem ter subcontextos. Sempre foco meus primeiros testes no requisito, e somente depois este requisito é quebrado em varias funcionalidades que vão gritando por novos testes até chegar à indivisibilidade mencionada. Ao final do produto os testes que foram quebrados garantem o funcionamento da unidade e os primeiros testes escritos servem para garantir que a engrenagem do processo funciona. Quando alguma funcionalidade falha eu tenho pelo menos dois testes falhando: O da unidade e o da ‘engrenagem’ (que ainda sim não deixa de ser uma unidade).
    Quanto a Interfaces:
    Gosto de fazer só o básico para o atendimento do requisito e é seguindo esta linha que acredito que a criação de interfaces deveria acontecer principalmente nos contextos em que a utilização de IoC se faz necessária, ou seja, quando você chega na fronteira tecnologia x negócio.
    O que tenho percebido nos projetos que acompanho é que desenvolvedores tendem a fazer uma força enorme para reaproveitar coisas, mesmo que este reaproveitamento seja só o nome do método e nada diz para sua implementação ou nada fala para seu design. Assinar uma classe com um contrato de interface precisa ter um motivo, precisa de um grito: “Eu sou serializavel! Eu sei persistir!”. E este grito normalmente tem haver com a tecnologia imposta. Já vi coisas abomináveis como interfaces IContaContabil para assinar ContaDebito e ContaCredito, mesmo que as posteriores herdem de uma “ContaContabil” em comum. Sempre cabe um depende em nossa área mas o que tenho visto é que o depende esta virando regra…

    • elemarjr
      27/06/2013

      Olá Marcus,

      Concordo 100% com você.

      Os seus testes de “engrenagem” são os testes funcionais? Estou certo?

      • De certa forma… Sim, são funcionais porque validam o requisito. Mas não substituem os de integração :)

        • elemarjr
          27/06/2013

          Certo. Acho que entendi o seu ponto de vista. Mas, adoraria entender melhor a sua definição para testes de integração.

          Perceba, antes de voltarmos ao “101″, aceito testes de integração como um meio de testar o funcionamento, em conjunto, de dois ou mais componentes – sendo que esse teste pode, ou não, validar um requisito funcional.

          Também aceito que um teste funcional não é necessariamente, um teste de integração. Visto que é possível validar uma funcionalidade atendida por uma única unidade.

          Dito tudo isso, parece-me natural que o acoplamento entre componentes precise ser ainda mais fraco do que o acoplamento entre unidades …

          Enfim .. Pegou a linha?

          • Simplificando ao máximo o “meu” ponto de vista:

            Testes de Integração: Qualquer tipo de teste que seu IoC não aponte para Mocks e Stubs, ou seja, se você não envolve um terceiro componente tecnológico como banco de dados, arquivos ou hora do sistema não posso chamar de teste “integrado”. Eu identifico os testes de integração facilmente quando sei que um “corpo estranho” aos testes pode alterar seu resultado e mesmo assim seu código continuará correto.

            Testes Unitários: Você já explicou bem antes, o teste da menor parte divisível do código, basicamente que envolva apenas um método.

            Testes Funcionais: Teste de um conjunto de funções que juntas formam um requisito. ‘N’ testes unitários podem substituir um teste funcional no que diz respeito ao correto funcionamento do sistema, porém um teste funcional tem uma característica insubstituível: Ele documenta o Requisito.

            Um exemplo bobo:
            Requisito: Calcular os valores de ‘x’ de uma função de segundo grau:

            Testes Unitários:
            [TestMethod]
            void TestarCalcularValorDelta(); //testa: int ObterDelta(int a, int b, int c);
            [TestMethod]
            void TestarCalcularValorX1(); //testa: int ObterX1(int delta, int a, int b, int c);
            [TestMethod]
            void TestarCalcularValorX2(); //testa: int ObterX2(int delta, int a, int b, int c);

            Teste Funcional:
            void TestarCalcularFuncao2Grau(); //testa: int[] CalcularFuncaoSegundoGrau(int a, int b, int c);

            Neste exemplo estupido, eu ainda poderia fazer algum tipo de teste integrado se os valores de a, b, e c fossem obtidos a partir de um arquivo planilha.

            Note que, os três métodos (ObterDelta, ObterX1 e ObterX2) são muito fracos para merecer sua própria classe, pois não fazem sentido em nenhum outro contexto de calculo. A menos que você seja “Orientado a purismo” ou sofra de Paternite (lembrado o Quaiato) estes caras são métodos da classe CalculoFuncaoSegundoGrau.
            Se eu garantir que os 3 funcionam COM CERTEZA eu garanto que TestarCalcularFuncao2Grau também funciona.
            Mais estes 3 testes não documentam meu requisito, coisa que o TestarCalcularFuncao2Grau faz. Daí o meu entendimento que este é o teste funcional por estar intimamente ligado ao requisito. Além disto, se você programa por TDD c/ Baby Steps você não conhece os 3 testes unitários antes de conhecer o teste funcional, sendo ele sempre seu ponto de partida.

            Este é o motivo de, quando alguma coisa no sistema falha, falham pelo menos dois testes: O que identifica o requisito e pelo menos um teste unitário que rastreia o erro do requisito.

            Consegui ser claro? (Já falei várias vezes: meu português e didática são péssimos…)

  2. ceb10n
    27/06/2013

    Eu encaro a interface como um contrato que está diretamente ligado ao que se espera de ação de algo, conseguindo estabelecer uma comunicação entre partes distintas do sistema sem que haja um acoplamento desnecessário.

  3. Francisco Hisashi Berrocal
    28/06/2013

    Como sempre, suas contribuições são sempre valiosas.
    Fico feliz que tenha voltado as atividades do blog!

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

Informação

Publicado às 27/06/2013 por em Post e marcado .

Estatísticas

  • 625,853 hits
%d blogueiros gostam disto: