Tem tempo que procuro uma ferramenta útil que una testes unitários e escrita de especificação em uma coisa só. Ou seja, uma ferramenta boa de BDD. Recebi este link de um colega de trabalho e gostei. Esta pode ser uma boa ferramenta para se ter um port para php:
http://www.concordion.org/
quarta-feira, 24 de março de 2010
quinta-feira, 18 de março de 2010
Refactoring e Clareza de Código - Negação da Negação
Este faz parte de uma série de pequenos posts com os tags código limpo e refactoring que vou estar publicando na medida que vou limpando alguns códigos por aqui.
A negação da negação geralmente atrapalha a leitura:
Normalmente quando um if tem um else, é mais claro se não começamos negando. Porque então o else vai ser a negação da negação.
Ou então algo assim:
parece normal, mas da um trabalhinho pra ler. É bem mais fácil ler algo como:
ou no máximo:
A negação da negação geralmente atrapalha a leitura:
if(!$userAllowed) {ou seja, se o sistema não tiver acesso, faça x e se ele não não tiver (que é o mesmo que ele ter, mas da um trabalho para ler) faça y. Sei que isso parece simples, principalmente em um código pequeno, mas tudo que diminui a velocidade de leitura deve ser evitado, até mesmo porque em alguns métodos fica difícil não ter um código extenso.
//x
} // aqui estamos negando a negação do if acima...
else {
//y
}
Normalmente quando um if tem um else, é mais claro se não começamos negando. Porque então o else vai ser a negação da negação.
Ou então algo assim:
if(!$accesForbidden) {//a} else {//b}
parece normal, mas da um trabalhinho pra ler. É bem mais fácil ler algo como:
if($hasAccess) {//a} else {//b}
ou no máximo:
apesar de que este segundo caso só acho aceitável quando a variável accessForbiden já existe e tem esse nome justificado em outro contexto.
if($accesForbidden) {//b} else {//a}
quarta-feira, 17 de março de 2010
Desacoplando para Testar - Injeção de dependência, inversão de controle e outras palavras bonitas.
Objetivo: Ensinar uma maneira de se estruturar uma aplicação para ela ser mais fácil de testar. Reduzir o tempo dos testes. Simplificar os testes usando mocks de maneiras não intrusivas.
Uns dos pontos principais para o teste de software é:
Não vou entrar de jeito nenhum na discussão do que é mock e o que é um stub. Vou melhorar este termo e traduzir. Digamos que podemos usar um "orêia". Por exemplo, se temos alguma parte do software que acessa um serviço externo, provavelmente isso não vai acontecer em menos de 0,1 segundo e isso é MUITO demorado para um teste. Você não acha? Imagine que ao final de um projeto pequeno para médio você terá em torno de 1000 testes.
Bom, então como fazemos para o nosso teste não demorar tanto? Usamos um mock, quer dizer stub, ou melhor: um "orêia". O nosso orêia vai ser usado no lugar do serviço original e fingir que executa o serviço. Ele pode até retornar algo estático ou que esteja em memória para simular o componente que ele está substituindo.
Por exemplo, digamos que no método inserirTexto() usamos o serviço do yahoo de extrair palavras chaves. Para isso usamos uma classe nossa onde encapsulamos a chamada a esse serviço: a classe PalavraChaveExtractorTabajaras. então teremos algo assim:
Mas como vamos fazer no nosso método inserirTexto para poder dizer que queremos usar o ôreia (Mock)?
Temos que ter um jeito de fazer com que o sistema use o "PalavraChaveExtractorTabajaras" em um ambiente de produção e use o mock nos testes. Já sei, que tal...
Bom, uma outra alternativa é criar um builder para o extractor assim:
E então?
Então o melhor que já encontrei até agora para poder ter estes serviços variando de acordo com ambiente e não precisar saber de nada disso no código que usa o serviço é passar esta responsabilidade para uma outra classe. Esta classe vai gerenciar esse processo de instanciar e entregar os "serviços" de acordo com o ambiente que estamos. Essa classe geralmente é chamada de serviceContainer e já existem algumas implementações interessantes para ela em php, como por exemplo esta:
http://components.symfony-project.org/dependency-injection/trunk/book/03-Service-Container
Apesar de ser muito boa esta implementação, para um projeto que fizemos agora eu preferi escrever algo mais simples e que funcionou tão bem quanto, inclusive com algum ganho de performance, economizando em torno de 1m a cada requisição.
Porque o nome ServiceContainer ?
Porque a classe é um container que contém serviços. Por serviços podemos entender os objetos que implementam serviços como o de palavras chave do nosso exemplo, ou serviços de envio de email, ou até mesmo um repositório de objetos que se responsabiliza por cuidar da persistência de um determinado objeto.
Assim no nosso código podemos ter algo como:
O código fica um pouco mais extenso, mas continua claro. Estou pegando o serviço responsável por processar 'keywords'. Não sei qual serviço ele é, mas tudo bem, polimorfismo é isso aí.
Desta maneira apenas o container centraliza a responsabilidade de definir em qual ambiente estamos. Também ele vai instanciar o objeto de acordo com o ambiente. Um outro benefício que ganhamos disso é que se precisarmos trocar serviço que usamos poderemos intanciar ele e configurar também de outra maneira que não seja a que usavamos antes.
O SimpleServiceContainer
Com base na mesma interface do container do symfony, eu criei esse aí mais simplezinho:
Por padrão ele usa as definições do SimpleServiceContainer_all. Esse eu tenho que criar obrigatóriamente. Depois eu poso extender dele de acordo com o ambiente que eu quiser. Por exemplo, se eu chamo o ambiente de desenvolvimento de test eu posso criar as minhas classe da seguinte maneira:
Assim desta maneira, quando eu crio o meu serviço com:
Eu vou receber o mock para o ambiente de testes e o normal para quando eu não estiver rodando os meus testes. Pode parecer simples, mas faz toda a diferença.
Veja:
Uns dos pontos principais para o teste de software é:
- testar algo coeso, não atrapalhando um teste com funcionalidades de outras bibliotecas
- ser rápido
Não vou entrar de jeito nenhum na discussão do que é mock e o que é um stub. Vou melhorar este termo e traduzir. Digamos que podemos usar um "orêia". Por exemplo, se temos alguma parte do software que acessa um serviço externo, provavelmente isso não vai acontecer em menos de 0,1 segundo e isso é MUITO demorado para um teste. Você não acha? Imagine que ao final de um projeto pequeno para médio você terá em torno de 1000 testes.
Bom, então como fazemos para o nosso teste não demorar tanto? Usamos um mock, quer dizer stub, ou melhor: um "orêia". O nosso orêia vai ser usado no lugar do serviço original e fingir que executa o serviço. Ele pode até retornar algo estático ou que esteja em memória para simular o componente que ele está substituindo.
Por exemplo, digamos que no método inserirTexto() usamos o serviço do yahoo de extrair palavras chaves. Para isso usamos uma classe nossa onde encapsulamos a chamada a esse serviço: a classe PalavraChaveExtractorTabajaras. então teremos algo assim:
function inserirTexto($texto) {Beleza, então esse método extrai as palavras chaves e grava tudo no banco. Hum... e agora como vamos fazer para chamar esse método de um teste? Imaginem que temos o seguinte teste:
$this->palavrasChaves = PalavraChaveExtractorTabajaras::extrair($texto);
$this->gravarNoBanco();
}
function testDeveInserirTextoNoBanco() {
$texto = $this->getTextoFixture();
$conteudo = FabricaDoConteudo::criaUmNovoAiPorFavor();
$conteudo->inserirTexto($texto);
$conteudoNoBanco = RepositorioDoConteudo::lerPeloTexto($texto);
$this->assertTrue((boolean)$conteudoNoBanco,"O conteúdo deve ser gravado no banco.");
}
Mas como vamos fazer no nosso método inserirTexto para poder dizer que queremos usar o ôreia (Mock)?
Temos que ter um jeito de fazer com que o sistema use o "PalavraChaveExtractorTabajaras" em um ambiente de produção e use o mock nos testes. Já sei, que tal...
if($ambiente == 'test') {Funcionar funciona, mas ainda não é algo muito bonito. Se eu quiser colocar outro ambiente vou ter que adicionar mais um if. Além disso, neste meu método, o ideal é que eu escreva o código de maneira fluente sem ter que me preocupar com testes. Já pensou ter que colocar estes ifs em todo canto. Vai ficar muito sujo.
$this->palavrasChaves = PalavraChaveExtractorTabajarasMock::extrair($texto);
}
else {
$this->palavrasChaves = PalavraChaveExtractorTabajaras::extrair($texto);
}
Bom, uma outra alternativa é criar um builder para o extractor assim:
class PalavraChaveExtractorTabajaras {Eu teria que alterar a declaração do método "extrair", de estática para dinâmica, mas funciona melhorzinho, mas ainda assim eu acrescento código a minha classe que não tem a ver com ela. A minha classe não deve saber em que ambiente eu estou. Isso deve ficar para as classes das camadas mais altas, da aplicação ou do framework que eu estiver usando.
static function build() {
// retorna de acordo com o ambiente
}
E então?
Então o melhor que já encontrei até agora para poder ter estes serviços variando de acordo com ambiente e não precisar saber de nada disso no código que usa o serviço é passar esta responsabilidade para uma outra classe. Esta classe vai gerenciar esse processo de instanciar e entregar os "serviços" de acordo com o ambiente que estamos. Essa classe geralmente é chamada de serviceContainer e já existem algumas implementações interessantes para ela em php, como por exemplo esta:
http://components.symfony-project.org/dependency-injection/trunk/book/03-Service-Container
Apesar de ser muito boa esta implementação, para um projeto que fizemos agora eu preferi escrever algo mais simples e que funcionou tão bem quanto, inclusive com algum ganho de performance, economizando em torno de 1m a cada requisição.
Porque o nome ServiceContainer ?
Porque a classe é um container que contém serviços. Por serviços podemos entender os objetos que implementam serviços como o de palavras chave do nosso exemplo, ou serviços de envio de email, ou até mesmo um repositório de objetos que se responsabiliza por cuidar da persistência de um determinado objeto.
Assim no nosso código podemos ter algo como:
function inserirTexto($texto) {
$extrator = SimpleServiceContainer::getInstance()->getService('keywords');
$this->palavrasChaves = $extrator->extrair($texto);
$this->gravarNoBanco();
}
O código fica um pouco mais extenso, mas continua claro. Estou pegando o serviço responsável por processar 'keywords'. Não sei qual serviço ele é, mas tudo bem, polimorfismo é isso aí.
Desta maneira apenas o container centraliza a responsabilidade de definir em qual ambiente estamos. Também ele vai instanciar o objeto de acordo com o ambiente. Um outro benefício que ganhamos disso é que se precisarmos trocar serviço que usamos poderemos intanciar ele e configurar também de outra maneira que não seja a que usavamos antes.
O SimpleServiceContainer
Com base na mesma interface do container do symfony, eu criei esse aí mais simplezinho:
class SimpleServiceContainer {
public $parameters;
static $containers = array();
protected $services = array();
protected $env;
/**
* @return SimpleServiceContainer
*/
static function getContainer($env) {
if(!array_key_exists($env,self::$containers)) {
$class = 'SimpleServiceContainer_'.$env;
if(!class_exists($class)) {
$class = 'SimpleServiceContainer_all';
}
self::$containers[$env] = new $class($env);
}
return self::$containers[$env];
}
static function getInstance() {
return self::getContainer(ConfigSystem::get('environment'))
}
function init() {}
protected function __construct($env) {
$this->env = $env;
$this->init();
}
/**
* @return SimpleServiceContainer
*/
function setParameter($name,$value) {
$this->parameters[$name] = $value;
return $this;
}
function getParameter($name) {
return $this->parameters[$name];
}
function getService($serviceName) {
if(!isset($this->services[$serviceName])) {
$this->createService($serviceName);
}
return $this->services[$serviceName];
}
private function createService($serviceName) {
$methodName = 'createService_'.$serviceName;
if(!method_exists($this, $methodName)) {
throw new ServiceBuilderNotConfiguredException(
'this container does not have a builder for the service called "'
.$serviceName."', the method should be called ".$methodName
);
}
$this->services[$serviceName] = $this->$methodName();
}
public function getInstantiatedServiceNames() {
return array_keys($this->services);
}
}
Por padrão ele usa as definições do SimpleServiceContainer_all. Esse eu tenho que criar obrigatóriamente. Depois eu poso extender dele de acordo com o ambiente que eu quiser. Por exemplo, se eu chamo o ambiente de desenvolvimento de test eu posso criar as minhas classe da seguinte maneira:
class SimpleServiceContainer_all extends SimpleServiceContainer {
protected function createService_keywords() {
return PalavraChaveExtractorTabajaras::build();
}
}
class SimpleServiceContainer_test extends SimpleServiceContainer_all {
protected function createService_keywords() {
return PalavraChaveExtractorTabajarasMock::build();
}
}
Assim desta maneira, quando eu crio o meu serviço com:
$extrator = SimpleServiceContainer::getInstance()->getService('keywords');
Eu vou receber o mock para o ambiente de testes e o normal para quando eu não estiver rodando os meus testes. Pode parecer simples, mas faz toda a diferença.
Veja:
- No meu código eu não digo como instanciar o serviço, pois o container funciona como uma factory. Assim qualquer outro componente que tenha a mesma interface pode ser substituido por ambiente eu até em um refactoring futuro sem impacto nas camadas que o usam.
- Isso diminui o acoplamento das classes que usam o serviço com o serviço em sí.
- Quando eu rodo meus testes o sistema automáticamente instancia o serviço de acordo com o ambiente de maneira transparente e me manda um componente que eu posso usar só sabendo a sua interface.
- Isso permite eu usar mocks transparentemente nos testes e também em outros ambientes e até substituí-los futuramente por outras classes quando estas forem desenvolvidas.
- Eu gosto desta implementação pois também instancio os objetos em puro php, sem a necessidade de um xml ou algo assim.
- Os objetos são instanciados apenas quando necessito deles.
- Os objetos dos serviços são por padrão instanciados uma vez só e usados em diferentes partes do sistema. Claro que isso pode não ser o comportamento desejado, mas pela simplicidade da implementação é fácil trocar este comportamento.
Marcadores:
arquitetura para testes,
classe,
injeção de dependência,
php,
service container,
testes
Assinar:
Postagens (Atom)