Como escrever códigos testáveis e de fácil manutenção em PHP
Author: Sara Silva - Postado em: 05/09/2013
Relacionado as categorias: Guias e Tutoriais, Planeta PHP, Tecnologia, Tutoriais do Conhecimento | Leave a Comment
Os frameworks geralmente provêm um ferramental para desenvolvimento rápido de aplicações, mas na maioria dos casos, quanto mais rápido você desenvolve a aplicação, maior são os débitos técnicos. Débitos técnicos são criados quando a manutenção do código não é o foco principal do desenvolvedor. Modificações futuras e debbuging começam a ficar custosos devido a falta de testes unitários e estrutura.
Aqui vamos ensinar como estruturar seu código para alcançar uma boa testabilidade e manutenibilidade – e também te salvar um tempo.
Vamos ver (por alto)
1 – DRY
2 – Dependency Injection
3 – Interfaces
4 – Containers
5 – Testes unitários com PHPUnit
Vamos começar com um código artificial, mas típico. Poderia ser qualquer classe de qualquer framework.
Esse código vai funcionar, mas precisa de algumas melhorias:
1 – Não é testável.
– Estamos dependendo da variável global $_SESSION. Frameworks de teste unitário, como o PHPUnit, se baseiam em linha de comando, onde $_SESSION e outras variáveis globais não funcionam.
– Nos baseamos em conexão com banco de dados. Por conceito, conexões com banco de dados devem ser evitadas em testes unitários. O teste é sobre o código, não sobre dados.
2 – Esse código não é de tão fácil manutenção como poderia ser. Por exemplo, se mudarmos a fonte dos dados, teremos que mudar o código em cada caso de App::db usado em nossa aplicação. Além disso, o que dizer de casos em que não queremos apenas as informações do usuário atual?
Uma tentativa de teste unitário
Aqui vai uma tentativa de teste unitário para a funcionalidade:
Vamos examinar. Primeiro, o teste vai falhar. A variável $_SESSION usada no objeto User não existe em testes unitários, visto que eles rodam via linha de comando.
Segundo, não tem conexão com a base de dados. Significa, que para fazermos esse teste funcionar, precisaremos fazer um bootstrap da nossa aplicação para conseguirmos o objeto App e seu objeto db. Além disso, precisaremos de uma base de dados que funcione para testar.
Para que esse teste unitário funcione, o que precisamos fazer:
1 – Setar uma configuração no CLI (PHPUnit) para rodar nossa aplicação;
2 – Depender de uma conexão com banco de dados. Fazendo isso teremos que criar uma base de dados específica para os testes. E se não houverem os dados necessários? E se a conexão com a base estiver lenta?
3 – Depender de um bootstrap para que nossa aplicação funcione, aumenta a carga dos testes, diminuindo significantemente a velocidade dos testes. Idealmente falando, os testes devem funcionar independente do framework que estamos usando para a aplicação.
Então, continuemos abaixo para melhorar um pouco essa situação.
Mantenha seu código DRY (Don’t Repeat Yourself)
Nesse nosso exemplo, não precisamos necessariamente pegar o usuário atual. Vou exemplificar melhor como é o conceito de manter o código DRY, fazendo com que o método seja mais generalizado:
Veja que escrevemos um código que pode ser acessado e reutilizado por todo a aplicação. Nós podemos passar o usuário atual no momento em que chamamos o método, ao invés de chamarmos dentro do método. O código é mais modular e manutível quando não depende de outras funcionalidades (como uma variável global de sessão).
Entretanto, este código ainda não é tão testável e de fácil manutenção como poderia ser, visto que ainda depende de uma conexão com o banco de dados.
Dependency Injection
Vamos melhorar um pouco mais o código adicionando um pouco de Dependency Injection. Abaixo como o código ficará ao passar a conexão com a base de dados para a classe:
Agora a dependência do model User estão injetadas. Nossa classe não assume mais um determinado tipo de conexão com banco, ou depende de variáveis globais.
A essa altura, a nossa classe está basicamente testável. Nós podemos passar a fonte dos nossos dados e o id do usuário e testar o resultado. Também podemos utilizar dados de bases separadas (assumindo que elas possuam os mesmos métodos para obtenção de dados).
Vamos dar uma olhada como o teste unitário ficará:
Note que adicionamos algo novo nesse teste: Mockery. O Mockery permite que você crie objetos PHP falsos, dando a impressão que existem dados nos mesmos. No nosso caso estamos criando dados na nossa base, para não precisar testar se existe ou não base de dados.
Resolvemos mais alguns probleminhas na nossa classe:
1 – Ainda estamos testando nosso model e não a conexão com a base de dados.
2 – Somos capazes de controlar as entradas e saídas da conexão do banco de dados simulados, e, portanto, pode-se testar de forma confiável contra o resultado da chamada de dados. Eu sei que vou ter um ID de usuário de “1” como resultado da chamada de dados simulado.
3 – Não precisamos fazer um bootstrap da aplicação e nem ter dados reais para executar o teste.
Interfaces
Para melhorar ainda mais, vamos implementar uma interface. Considere o código abaixo:
Temos algumas coisas acontecendo aqui.
1 – Primeiro, vamos definir uma interface para a nossa fonte de dados do usuário. Isso define o método getUser ().
2 – Em seguida, vamos implementar essa interface. Neste caso, criamos uma implementação MySQL. Aceitamos um objeto de conexão de banco de dados, e usamos para pegar um usuário de banco de dados.
3 – Por último, forçamos o uso da classe, implementando a interface UserInterface no nosso model User. Isso garante que a fonte de dados sempre terá o método getUser() disponível, não importando qual fonte de dados é usada para implementar a interface.
Qual o resultado?
– Nosso código é totalmente testável agora. Para a classe User, podemos facilmente simular a fonte dos dados.
– O código é muito mais manutível. Podemos trocar entre diferentes fontes de dados sem mudar o código em toda a aplicação.
– Podemos criar qualquer fonte de dados. ArrayUser, MongoDbUser, CouchDbUser, MemoryUser, etc.
– Podemos passar facilmente qualquer fonte de dados para nosso objeto User se preciso. Se você decidir simplesmente enterrar o SQL, basta apenas criar uma implementação diferente (por exemplo, MongoDbUser) e passá-la para o model User.
O código do teste unitário também fica mais simples:
Nós retiramos do teste todo o trabalho de simular a conexão com a base de dados. Ao inés disso, simplesmente simulamos a fonte de dados, e dizemos o que fazer quando o método getUser é chamado.
Mas ainda podemos melhorar mais!
Containers
Considere o uso do código abaixo (em algum controller):
Nosso passo final, é introduzir os containers. No código acima, precisamos criar e usar um monte de objetos para conseguir o nosso usuário atual. Esse código pode estar espalhado por toda a aplicação. Se você precisar mudar de MySQL para MongoDB, por exemplo, você ainda vai ter que editar em cada lugar que esse código acima aparecer. Isso, dificilmente é DRY. Os containers podem consertar isso.
Um container simplesmente “contém” um objeto ou funcionalidade. É similar ao conceito de registrar em sua aplicação. Podemos usar um container para instanciar automaticamente um novo objeto User com todas as dependências necessárias. Abaixo, usei o Pimple, uma classe de container bem popular.
Movi a criação do model User para apenas um local, na configuração da aplicação. Como resultado temos:
1 – Mantemos nosso código DRY. O objeto User e qual será a escolha do armazenamento de dados é escolhido em apenas um local na nossa aplicação.
2 – Podemos mudar nosso model User de usar MySQL ou qualquer fonte de dados em apenas UM lugar. É, de longe, um código muito mais fácil de dar manutenção.
Para encerrar
Durante esse tutorial, conseguimos entender um pouco mais sobre:
1 – Mantivemos nosso código DRY e reutilisável;
2 – Criamos um código manutível – Podemos mudar nossa fonte de dados para nossos objetos em um lugar apenas;
3 – Fizemos nosso código ser testável – Podemos simular objetos sem depender de bootstrap ou de criarmos um banco de dados de teste;
4 – Aprendemos a usar Dependency Injection e Interfaces para permitir a criação de códigos testáveis e manutíveis;
5 – Vimos como os containers podem ajudar a fazer nosso código mais simples de se dar manutenção;
Tenho certeza que você notou que adicionamos muito mais código em nome da testabilidade e manutenabilidade. Um forte argumento pode ser usado contra esse tipo de implementação: estamos acrescentando complexidade. De fato, esse tipo de implementação necessita de um conhecimento mais profundo de codificação, tanto para o autor do código quando para os colaboradores do projeto.
Entretanto, o custo de explanação e entendimento é de longe bem menor que o débito técnico que o projeto sem essa implementação cria.
Atenção nos prós:
– O código ficar muito mais simples de se dar manutenção, fazer modificações em apenas um local, já um ganho considerável de tempo;
– Ser capaz de executar testes unitários (rapidamente) vai reduzir a quantidade de bugs no seu código em larga escala – especialmente em projetos longos ou open-source;
– Ter mais trabalho agora vai te salvar tempo e dor de cabeça mais pra frente;
Para PHP, recomendo fortemente que você procure saber mais sobre o Laravel 4, que usa brihantemente containers e alguns conceitos aplicados aqui.
Comments
Leave a Reply