Utilizando dados JSON em formato TDataSet com TRESTResponseDataSetAdapter

A cada dia que passa estamos tendo mais desenvolvimento de webservices RESTFul, utilizando-se JSON como conteúdo de integração. Para os desenvolvedores Delphi, o que isso significa? Com certeza não significa problema algum.

Acostumados como estamos a trabalhar com datasets, é possível facilmente conectar sua aplicação a um servidor REST e obter as informações em formato tabular, como estivesse abrindo uma query em uma consulta de banco de dados, e simplesmente fazer o uso das informações a partir daí. Parece simples, não? E realmente é.

Vamos utilizar para nossos exemplos, um fake REST server, com informações fictícias de usuários: https://jsonplaceholder.typicode.com/users

Como uma das facilidades de uma ferramenta RAD, o RAD Studio já prepara todo o ambiente com as configurações necessárias para que isso aconteça. No  menu Tools->REST Debugger, coloque o endereço acima em URL e depois click em Send Request. Com isso, todas as informações necessárias foram obtidas do servidor. Click na aba Tabular Data e veja as informações já em formato tabular.

Veja agora que existe um botão com o nome de Copy Components, feito justamente para gerar em memória todos os componentes necessários para a sua aplicação realizar a mesma consulta. Ao clicar, você receberá a mensagem:

The following components have been copied to the clipboard: TRESTClient, TRESTRequest, TRESTResponse, TRESTResponseDataSetAdapter, TFDMemTable

Isso apenas está indicando que foi gerado na memória 5 componentes:

  • TRESTClient
  • TRESTRequest
  • TRESTResponse
  • TRESTResponseDataSetAdapter
  • TFDMemTable

Cole o conteúdo da memória (CTRL + V) em um formulário ou data módulo e veja que todos os componentes vão ser colados.

O três primeiros são os componentes de comunicação com o servidor. O importante quando utilizamos o REST Debugger é que todos esses componentes já vem configurados para o acesso ao webservice, inclusive suas propriedades ContentType, Acept, AceptCharset, URL, etc. Assim, você não precisa se preocupar mais com isso.

O grande responsável pela conversão do JSON para um TDataSet é o TRESTResponseDataSetAdapter, que também já vem todo configurado.

Adicione um TDataSource e ligue-o com a tabela temporária (TFDMemTable). Adicione também um TDBGrid e ligue-o com seu novo datasource. Adicione ainda um botão para o disparo do evento de leitura das informações.

A dica aqui fica na execução da requisição para o webservice, e a abertura do adaptador para obtenção das informações. No evento OnClick do botão, faça:

Execute sua aplicação e veja o resultado ao clicar no botão.

Feito isso, você pode facilmente utilizar as informações do webservice lendo-as através da tabela temporária (TFDMemTable).

Muito legal, não é?

A tabela temporária poderia ser outro dataset, como um TClientDataSet, por exemplo, e não necessariamente o TFDMemTable.

Consegue visualizar o poder que isso traz aos desenvolvedores? O Delphi traz um ótimo recurso de produtividade e adaptabilidade. Sirva-se dele.

Dica para migração de VCL para Firemonkey

Toda migração é problemática. Se migrar de uma versão para outra de compilador já pode ser um problema, em virtude das alterações de funcionalidades, imagine migrar de um framework para outro.

O Firemonkey é o novo (nem tanto assim) framework da Embarcadero, no qual a empresa está investindo todas as suas fichas. Lembro-me que no começo a comunidade o recebeu preocupada, pois já havia sofrido com o fiasco do Kylix, mas parece que agora as coisas mudaram. Vejo muita propagando em cima dele não apenas do marketing da própria Embarcadero, mas também de membros da comunidade que pularam de cabeça nessa tecnologia. Claro que tudo isso deve-se à mudança de estratégia da Embarcadero para o fomento de uma comunidade muito maior, que veio ocorrendo nos últimos anos, através dos eventos oficiais, webinars, programas MVPs, etc.

A questão é que o Firemonkey se consolidou no mercado, e existe uma forte tendência de migração para essa nova tecnologia. Mas como fazer isso de forma menos traumática? Nativamente, a VCL e o Firemonkey não trabalham juntos, porque o compilador divide muito bem isso. Criar um novo projeto seria a solução? Claro que não, principalmente se o projeto for de grande escala. Trabalhar com dois projetos paralelos? Poderia resolver, mas imaginou as dificuldades de gerenciamento disso tudo?

Basicamente, se você adicionar um form firemonkey em um projeto VCL ele funciona, e o contrário também. O problema reside na dificuldade de trabalhar com isso no mesmo projeto, pois o compilador apenas permite criar novos formularios VCLs em projetos VCLs, e novos formulários FMX (FireMonkey) em projetos FMX.

A dica aqui é mudar abrir o arquivo DPROJ do projeto e alterar a tag FrameworkType de:

para

Assim, quando estiver em FMX, será possível adicionar novos formulários FMXs, e quando estiver em VCL, será possível adicionar novos formulários VCLs.

Muito simples, hein?

Mas e para os casos onde seja necessário a alteração contínua dessa tag? Ainda bem que Simon J. Stuart criou  um plugin chamado MonkeyMixer que faz isso facilmente, bastando escolher no popup menu do projeto qual o tipo de framework você quer trabalhar. Assim não precisamos adentrar nas entranhas do DPROJ para realizar nenhuma alteração, e podemos fazer as alterações em tempo de design, deixando tudo mais produtivo.

Video aula demonstrativa do MonkeyMixer: 
https://www.youtube.com/watch?v=TbCLU6vWjeQ

Código fonte do pugin MonkeyMixer:  https://github.com/LaKraven/MonkeyMixer

Pronto para iniciar sua migração?

Multithreading e Processamento Paralelo no Delphi (PPL)

Processamento paralelo é um assunto que mexe com os profissionais. Muitos a amam, outros nem tanto, mas todos respeitam seu potencial. A capacidade de dividir a carga de processamento em pequenas unidades que executam ao mesmo tempo, é uma vantagem que não pode ser desprezada.

A maioria dos desenvolvedores não sabe explorar os recursos de multithreading e da computação paralela em seus projetos. O grande problema não é a tecnologia. São as pessoas. Programar paralelamente não é apenas criar uma thread, mas sim pensar paralelamente. Eis a grande dificuldade. Não é possível utilizar os recursos de mutithreading e da computação paralela enquanto o desenvolvedor pensa apenas serialmente (linearmente). Isso é uma barreira, porque uma vez que você ultrapassa esse limite, o resto é apenas tecnologia. E a tecnologia está aí para nos ajudar.

Mas o que é uma thread? É simplesmente um processo. Um programa é composto, inicialmente, por um processo único, conhecido como main thread (thread principal). É o seu processo inicial. Entretanto, o programa não precisa ter simplesmente um único processo, podendo criar outros processos à vontade. Ele começa com um, porque é o mínimo possível para ele existir (caso contrário, ele não iria executar).

A dica aqui é pensarmos em threads como se fossem outros programas, desvinculados do programa principal. Ela não é realmente isso, porque existem áreas do sistema que precisam ser compartilhadas (de outra forma, você não usaria threads, e sim, faria outro programa) mas essa forma de pensar ajuda a estruturar o pensamento e evitar falhas. Vamos ver mais sobre isso depois.

A grande parte dos exemplos que encontramos quando vamos trabalhar com TThread é sobre como fazer percorrer dois TGauges. Esse exemplo é bom para mostrar como o processamento paralelo funciona, porque é possível visualmente perceber que um dos gauges é mais rápido que o outro, o que dá a noção de que as threads estão sendo gerenciadas por algo que está além dos nossos programas, no caso, o sistema operacional. Além disso, dá a percepção importante que não existe uma sequencialidade na execução das threads, uma vez que provavelmente uma delas irá terminar bem antes da outra. Todos esses conceitos são importantes, e necessários, mas eles não dão a capacidade de pensar dieferente. Eles só dizem respeito à como a tecnologia funciona. E lembra o que é o mais importante? Saber como pensar!

Outros exemplos mostram como tornar uma tela responsível mesmo quando está executando um processamento. No Delphi, se você utiliza VCL, verá que se você não utilizar um recurso de multithreading, a tela irá parar de responder porque a thread principal, que é a responsável pelo processamento gráfico da VCL, estará ocupada executando outra tarefa. Mesmo nesse caso, estamos falando apenas de tecnologia novamente.

É importante saber que não sou contra o conhecimento da tecnologia. Ele é essencial porque sem ela você não consegue desenvolver seus sistemas. Mas dou mais importância em “como as coisas são feitas” ao invés de “do que elas são feitas”.

Multithreading versus Processamento Paralelo

Precisamos antes de tudo definir as diferenças, para evitar qualquer mal entendido. Multithreading não é o mesmo que processamento paralelo. Quando falamos de processamento paralelo, falamos de utilização de recursos de hardware com múltiplos processadores e execução de código ao mesmo tempo, ou seja, de forma paralela. Multithreading diz respeito à concorrência na execução dos processos. Um computador com um única core de processamento não pode executar códigos de forma paralela, embora consiga executar códigos de forma concorrente. A concorrência é sobre a disputa da utilização do CPU por diferentes processos, enquanto paralelismo é sobre a utilização de diferentes “cores” de processamento. Contudo, como o sistema operacional manipula as threads em execução, ele pode fazer com que um sistema multithread também seja paralelo, caso haja o recurso de hardware para isso.

Thread

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Classes.TThread

TThread é um wrapper de uma thread do Windows (ou outros sistemas operacionais, caso esteja utilizando o Firemonkey. No caso do Windows, você pode saber mais em: https://msdn.microsoft.com/en-us/library/windows/desktop/ms684841(v=vs.85).aspx) utilizando a própria API do sistema . Um wrapper é uma abstração, uma transposição, uma ponte de algo que existe na API do Windows e que podemos utilizar no Delphi.

Utilizamos a TThread para conseguirmos executar processos concorrentes à main thread, ou seja, à nossa thread principal. Assim, faremos com que o sistema “faça” duas coisas concorrentemente, de forma que o sistema operacional divida o tempo de processamento do processador entre todas as threads.

Cabe ao sistema operacional determinar o paralelismo da execução da thread, conforme a carga do CPU. Dessa forma, na utilização das threads, não necessariamente o código será executado de forma paralela, porque isso dependente de outros fatores, que não a própria existência da thread.

Minha experiência demonstrou que o uso de threads concorrentes, mesmo que não utilizando os recursos da computação paralela, embora não em todos os casos, ainda melhora a performance do sistema em geral.

Create

Esse é um exemplo básico de uma implementação de uma  thread no Delphi.

No construtor (Create) temos algumas coisas interessantes a saber:

  • Parâmetro CreateSuspended da TThread – O padrão para a criação de uma thread é que tenha o parâmetro CreateSuspended como true. Isso fará com que a thread seja criada de forma suspensa, ou seja, ela não inicia seu funcionamento imediatamente depois de sua criação. Caso seja definido para false, a thread já inicia a sua execução logo depois de ser criada, mas nem sempre isso é interessante. Quando criamos uma thread suspensa, precisamos fazer o uso do método Start para iniciarmos a thread (conforme exemplo acima). Caso ela não tenha sido criada suspensa, então o uso do método Start não é necessário.
  • FreeOnTerminate – Isso é uma facilidade quando trabalhamos com thread. Por se tratar de processamento concorrente, nós não temos o controle de quando o processo irá terminar. Por isso, é uma boa prática (a não ser que você tenha alguma boa razão para não fazê-lo) sempre optar por FreeOnTerminate := True, pois isso fará com que a instância da thread seja destruída, e a memória liberada, depois que terminar sua execução.

Execute

O método mais importante é o método Execute. Ele é o método que será chamado quando o processo concorrente entrar em execução. A thread encerra sua execução (e seu ciclo de vida, caso tenha FreeOnTerminate = True) assim que chegar na última linha do método Execute. Por isso, caso a thread precise permanecer viva por longo tempo, você deve criar um laço de repetição (provavelmente o While) para que a thread não seja destruída.

Existe uma “flag” que determina quando a thread precisa ser destruída, chamada Terminated. Quando utilizamos TThread.Terminated := True, estamos apenas sinalizando que a thread foi marcada para finalização. Ela não termina a execução da thread abruptamente, apenas sinaliza que ela pode ser finalizada. Isso, com o uso do laço de repetição, teríamos:

Ou seja, estaríamos constantemente verificando se a thread está marcada para terminar, antes de realizarmos nossas operações.

OnTerminate

Outro método muito importante de uma thread é o OnTerminated. Ele é executado no momento que a thread finaliza sua execução e começa a se preparar para sua liberação.

Assim, caso queiramos que algo seja executado assim que a thread finalizar suas operações, OnTerminated deve ser utilizado:

Antes do início da thread, foi definido:

FMeuProcesso.OnTerminate := ExecutarDepoisThread;

A procedure ExecutarDepoisThread simplesmente chamou o método Synchronize passando um método anônimo para exibir as informações no memo. Synch…oque?

Synchronize

O método syncronize foi utilizado nos dois exemplos àcima quando quisemos alimentar as informações no memo. Lembra que as thread são procedimentos concorrentes e funcionam como se fossem programas diferentes? Synchronize é uma forma de sincronizar as informações entre as threads, no caso, com a thread principal.

Toda alteração visual da VCL deve ser feita através da thread principal, a responsável por gerenciar a VCL. Assim, quando uma thread pretende alterar qualquer aspecto visual deve fazê-lo através de algum método de sincronização. No caso acima, utilizei o shyncronize.

Queue

Queue, assim como Synchronize, possui a capacidade de trabalhar com diferentes thread, todavia, enquanto Synchronize é executada de forma serializada pela main thread quando existem diferentes threads chamando Syncronize ao mesmo tempo, Queue simplesmente gera uma fila que é executada no tempo livre da main thread. E porque isso importa? Porque a chamada dos métodos Syncronize penetra na main thread e, se não forem performáticos, travarão a main thread, enquanto Queue só será executada no tempo livre da thread principal. Legal né? Assim, use Queue sempre que possível.

Tratamento de exceções nas threads

Qualquer exceção não tratada gera uma parada abrupta da execução do programa. Na thread, isso ocorre da mesma forma, mas com o fato de que se a thread principal não ficar sabendo da exceção, a interrupção da thread que gerou a exceção será tratada de forma transparente, sem nenhum aviso. Ela simplesmente irá parar de funcionar. Como exemplo, altere o código conforme abaixo e faça um teste:

Mas então, o que fazer?

Vamos utilizar o método OnTerminate para nos ajudar com o tratamento:

Foi criado a procedure TratamentoDaExcecao para tratarmos a exceção. Ela é utilizada em:

FMeuProcesso.OnTerminate := TratamentoDaExcecao;

Assim, quando terminar a execução da thread, verificamos se houve alguma exceção simplesmente vendo se TThread(Sender).FatalException é diferente de nil:

if Assigned(TThread(Sender).FatalException) then

Se for, é porque ocorreu alguma exceção que parou a execução da thread. Simples, não é?

Só com esse conhecimento já é possível realizar grandes coisas com as threads, mas vamos explorar ainda mais esse recurso.

Threads sem complexidade

Todas as formas vista aqui anteriormente foram feitas com a criação de classes que herdam de TThread. Muitas vezes, você gostaria que seu código fosse executado sem a complexidade de criação e gerenciamento de uma classe. Vamos imaginar que você deseje que o sistema crie um log, e resolve jogar isso para um processamento concorrente, para já liberar a thread principal para seguir seu caminho. Seu desejo é simplesmente que o log seja processado longe da main thread.

A TThread possui o método TThread.CreateAnonymousThread. Com ele, você pode simplesmente passar o seu bloco de código e dizer: “Executa isso para mim de forma concorrente!”. O CreateAnonymoThread internamente irá criar uma thread, executar o bloco de código e finalizar a thread.

Veja que aqui fizemos uso de um método anônimo.

O problema dos recursos compartilhados

Vamos imaginar uma casa com 4 irmãos. Cada irmão tem suas próprias necessidades, como estudar, trabalhar, viajar, etc, todavia, existe um único carro de utilização compartilhada.

Há que se prever que em algum momento, um dos irmãos queira o carro para ir estudar e outro para trabalhar. Pode ainda um terceiro irmão solicitar o carro para ir na farmácia, comprar medicamentos. O que fazer? Absolutamente nada. Apenas um dos irmãos terá a condição de pegar o carro enquanto os outros terão que esperar, ou dar outra solução. Parece lógico, certo?

Quando falamos de recursos computacionais, estamos falando da mesma coisa. Imagine que você possui um objeto instanciado para a conexão com o banco de dados. Quando uma das thread está utilizando o objeto para trocar dados com o banco, as outras threads precisam necessariamente esperar ele terminar. Imagine todos os irmãos dentro do mesmo carro dirigindo-o de forma compartilhada. Quando um vira para a esquerda, o outro logo em seguida vira para a direita. Eles só podem encontrar um poste em seu caminho. Na computação, os erros podem ser imprevisíveis.

Por isso, no logo no início, ressaltei a importância de “como” fazer as coisas, ao invés de qual tecnologia utilizar. A tecnologia é apenas um meio para chegarmos aos resultados, mas precisamos antes saber para onde vamos e como vamos.

O grande problema do desenvolvimento concorrente e paralelo é justamente preparar o sistema para trabalhar adequadamente. O desesenvolvedor, analista, arquiteto, precisa imaginar o funcionamento do sistema como processos independentes que podem estar continuamente em atividade e que utilizam recursos compartilhados.

E como trabalhamos com isso? Antes de tudo, devemos saber o que é um lock.

Lock

Literalmente traduzido como trava ou bloqueio. Um mecanismo de lock é um mecanismo de bloqueio, que gerencia quando um recurso está disponível ou não. Existem diferentes formas de bloqueio, cada qual com suas próprias características, mas todas são utilizadas com recursos compartilhados para evitar problemas.

TCriticalSection

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.SyncObjs.TCriticalSection

Também conhecido como seção crítica. Uma sessão crítica é o mecanismo de lock que determina quem vai utilizar o carro. Imagine que na casa dos 4 irmãos exista  um pai que gerencie quem pode e quem não pode utilizar o carro. Esse pai utiliza para isso um critério bem simples: quem solicitou o carro primeiro terá o acesso ao carro primeiro. Veja que ele não tem inteligência para analisar qual a prioridade de cada filho. O que ele faz é simplesmente criar uma fila de solicitações de utilização do carro e liberar o carro nessa mesma ordem. Assim, quando o primeiro terminar de utilizar o carro, o segundo passará a utilizá-lo, e assim por diante.

Para que isso funcione, o primeiro filho diz: “Pai, me empresta o carro?” Como o carro está livre, o pai libera. Nesse mesmo momento, o segundo filho diz: “Pai, me empresta o carro?” Como o carro já está sendo utilizado pelo primeiro filho, o pai diz: “Não posso, o filho 1 está usando. Mas estou com o seu nome aqui e assim que ele devolver vou liberar para você”. O terceiro filho diz: “Pai, me empresta o carro?” O pai novamente diz: “Não posso, o filho 1 está usando e o filho 2 já está na sua frente”.

Uma coisa que precisa ficar clara é que os filhos que solicitaram o carro mas não o tiveram logo em seguida, ficam aguardando parados até a liberação do carro. Embora cada filho tenha sua própria atividade, quando eles precisam de um recurso compartilhado, então precisam ser serializados, ou seja, um depois do outro, e isso faz com que suas atividades, nesse momento, não sejam mais concorrentes. Por isso, quando precisar utilizar algum dos recursos de locking, faça de forma que o lock dure o menor tempo possível, para minimizar a serialização dos processamentos das threads.

Analogias à parte, vamos à prática:

Considere o objeto acima como o nosso recurso compartilhado. Ele é um objeto simples, com a funcionalidade de disparar uma mensagem visual.

Até agora apenas codificamos a criação e destruição da seção crítica e do objeto de mensagem. Assim, quando o formulário abrir eles serão criados e quando o formulário for fechado eles serão destruídos.

O código acima cria a serialização das threads, para utilização do mesmo recurso, no caso, o objeto que dispara a mensagem.

Todas as threads foram criadas utilizando-se o método CreateAnonymousThread. Assim, não precisei me preocupar com a complexidade da criação das classes das threads. Outra coisa que é importante no exemplo, é que não temos como saber quais das threads será executada primeiro. Em meus testes, o código seguiu conforme a sequência, mas isso não é garantido, porque a segunda thread poderia executar antes da primeira, por exemplo.

O importante, indiferentemente de quem obtenha o recurso primeiro, é que a execução será serializada, e não mais concorrente, porque existe uma competição para utilização de um recurso compartilhado.

Faça seus testes e veja o resultado.

Obs: Existe um texto escrito por Eric Grange a respeito de falhas no TCriticalSection e de como ele poderia serializar a execução das threads de forma que fique pior do que se estivesse tudo sem thread. Você pode ler a respeito AQUI.

Aparentemente, o TCriticalSection, por ser um objeto muito pequeno e de alocação dinâmica, pode, em uma situação onde existam várias instâncias diferentes da classe TCriticalSection, compartilhar o mesmo cache dentro do CPU, gerando um conflito de cache.

Por sorte, existe uma pequena “dica” para contornar essa situação.

O que está sendo feito acima é justamente forçar o objeto a ter mais de 96 bytes, que no caso existem apenas por existir, sem funcionalidade nenhuma para a classe em sí, mas que força o objeto a ser maior do que o “cache line” dos processadores atuais (isso pode aumentar conforme a necessidade posteriormente). Ainda, segundo Eric, isso dá um ganho de performance de até 7% maior ao atual, visto algumas otimizações do FastMM (projeto de um gerenciador de memória que foi adota posteriormente pelo Delphi, mas que não segue o projeto como um todo. Sobre as configurações de gerenciamento de memória do Delphi, veja Aqui).

TMonitor

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.TMonitor

Funciona semelhante ao TCriticalSection, mas com comportamento diferente. Você cria um objeto e passa o monitoramento desse objeto para a classe TMonitor e TMonitor fica responsável por serializar os acessos ao objeto:

O código acima já deve ser auto-explicativo à essa altura.

Como pode ver, a utilização é bem semelhante. Uma das vantagens do TMonitor é que você não precisa ter uma classe instanciada do TCriticalSection, visto que TMonitor é um record.

Obs: Houve uma discussão no passado a respeito da diferença entre TCriticalSection e TMonitor, considerando a performance e outros pontos. Pode acompanhar os resultados Aqui e Aqui, e faça seu próprio julgamento. Nos exemplos aqui acima, pude notar que o código “ocorreu melhor concorrentemente” utilizando-se o TMonitor. Em cada execução, uma das thread veio primeiro, ao contrário do TCriticalSection, que sempre executava – nos poucos testes que fiz – a primeira thread primeiro. Isso é só um comentário, e não um estudo aprofundado do assunto, até porque não fui fundo o suficiente para entender o porquê desse comportamento.

 

TInterlocked

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.SyncObjs.TInterlocked

Quando você precisa de alterações simples, como troca de valores entre variáveis – ou incrementar variáveis por exemplo – você pode (e deve) utilizar TInterlocked. Ele é mais performático que outros métodos de lock e sem a complexidade de ficar criando classes e gerenciando quem está com quem.

Imagine que você possua uma variável “Count” que deva ser incrementada em cada thread. Como cada thread ocorre paralelamente, ao incrementar o valor de uma variável simplesmente da forma convencional:

poderá ser um ponto de falha, porque em cada momento que a thread incrementa a variável,  ela precisa determinar o valor da própria variável. Imagine que Count tenha valor 10. Então a primeira coisa que a thread identifica é que a variável possui esse valor. Se no mesmo momento, outra thread alterou a variáve para 11, a primeira thread ainda “pensa” que a variável possui o valor 10, e seta novamente o valor dela para 11 (que a outra thread já havia feito), perdendo assim o valor do incremento. Esse é apenas um dos exemplos de problemas quando não utilizamos mecanismos de lock com multithreading.

Utilizando TInterlocked, poderíamos:

que fará o incremento da variável Count de forma segura. Simples não?

TInterlocked possui funcionalidades para incrementar, decrementar, adicionar, retirar, comparar, etc, etc. Vale a pena olhar a documentação e verificar onde sua necessidade se encaixa.

TMutex (Mutuamente Exclusivo)

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.SyncObjs.TMutex

Semelhante a um semáforo binário, um mutex é um mecanismo que cria uma lista de solicitações de processos que tentam acessar o mesmo recurso e determina apenas um que seguirá adiante na utilização do recurso. Seu funcionamento é semelhante ao da seção crítica, mas com algumas diferenças. Mutex são tratados nominalmente a nível de Kernel, o que garante uma disponibilidade entre aplicações, por outro lado, seções críticas são bem mais performáticas.

A codificação de um mutex é:

TSemaphore

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.SyncObjs.TSemaphore

O TSemaphore funciona semelhante ao TMutex, mas possui um contador interno, definido em sua criação. Conforme os acessos ao TSemaphore forem ocorrendo, eles vão sendo liberados enquanto o total de acesso não atingir o contador. Caso atinja, o semáforo barrará o recurso compartilhado.

Veja o exemplo:

Veja que troquei o tempo de espera para depois de executar a ação, para que fique mais claro visualmente o comportamento do semáforo.

Aqui nos meus testes, a primeira thread e a segunda executaram quase que juntas, enquanto a terceira teve que esperar as duas primeiras executarem.

Quando utilizar um semáforo? Sempre que você quiser limitar um recurso a determinado número de utilizações simultâneas. Um exemplo interessante que encontrei foi um no site Stackoverflow, onde ele fez a anologia de um semáforo a um segurança de uma boate. A boate tem um limite de X pessoas, e o segurança bloqueia quando chega nesse limite. Se alguma pessoa sai, o segurança libera o acesso daquela “vaga” que ficou livre.

Dentro de um ERP você poderia utilizar semáforos para limitar a quantidade de acessos simultâneos ao banco de dados, ou à um webservice, por exemplo.

Caso o contador seja definido para 1, ele se transforma em uma semáforo binário, e seu comportamento será semelhante ao do tipo TMutex.

Obs: Artigo sobre semáforo na EDN: https://edn.embarcadero.com/article/29908

TEvent

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.SyncObjs.TEvent

Um evento é uma sinalização. Quando utilizado com thread, essa sinalização pode determinar o início, o término, ou outro comportamento de uma thread. Imagine que você queira que a thread fique parada até que determinado fato aconteça, como por exemplo, a finalização de uma outra thread. Uma forma de comunicar para essa thread que a outra já acabou o que tinha que fazer, é utilizar os eventos. Talvez você queira saber quando todas as thread acabaram? Utilizamos eventos para isso também. Como dito, o evento é uma forma de sinalizar para as outras threads que algo aconteceu.

O que fizemos aqui em cima foi zerar o evento:

FEvento.ResetEvent; 

Isso o configura para que possamos utiliza-lo. Também zeramos uma variável global:

FQtdNoEvento := 0; 

Essa variável tem o objetivo de contar quantas threads estão em execução. Depois, criamos três threads, cada uma com um tempo diferente de execução. Como primeiro passo, todas as threads incrementam a variável global, como indicativo de que ela está sendo executada. Antes do final, cada thread decrementa o valor da variável, para indicar que finalizou, ou seja, já fez tudo o que precisava. Por fim, em cada thread é testado:

if FQtdNoEvento = 0 then FEvento.SetEvent;

Assim, se todas as threads já finalizaram, FEvento recebe a informação de “disparo”. Isso libera a thread principal, que estava com a execução parada em:

FEvento.WaitFor(10000);

O comando WaitFor trava a execução da thread enquanto o evento ainda não foi disparado (SetEvent). 10000 é o tempo de timeout, ou seja, a thread principal irá aguardar por 10000 milesegundo (10 segundos) o retorno. Se não obtiver um retorno em 10 segundos, irá seguir em frente. WaitFor pode retornar 4 coisas:

wrSignaled – Quando tudo ocorreu bem e o evento foi disparado (SetEvent).
wrTimeout – Quando o tempo definido já foi ultrapassado sem que SetEvento tenha sido chamado.
wrAbandoned – Quando o objeto do evento tenha sido destruído antes do tempo de timeout.
wrError -Quando algum erro ocorreu enquanto estava aguardando.

Ao invés de definir um tempo de timeout, você pode utilizar a constante INFINITE, que faz com que a thread aguarde indefinidamente. Muito cuidado ao utilizá-la, porque caso algum problema tenha ocorrido, o programa irá travar indefinidamente, porque pode nunca haver um retorno. Como boa prática, é recomendável a utilização de um valor para o timeout.

TSpinlock

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.SyncObjs.TSpinLock

Nos mecanismos de lock tradicionais e visto aqui anteriormente, o sistema operacional coloca as threads que “estão aguardando” em um estado de descanso. Isso faz com que aquelas threads não consumam tempo de CPU. É uma otimização inteligente realizada pelo sistema operacional para poupar processamento. Todavia, existem casos onde a performance é necessária. Existe uma forma de criar o lock sem colocar as threads em estado de descanso, fazendo com que as threads continuem em execução. Isso só é benéfico quando o tempo de duração do lock é muito curto, e quando performance é realmente necessária, pois de outra forma, as threads ficarão consumindo tempo de processamento desnecessariamente. Fique atento porque TSpinLock não interrompe a execução da thread, então pense bem antes de utilizá-lo.

Basicamente a thread fica em loop aguardando o momento em que ela consegue acesso ao recurso compartilhado.

TSpinLock possui como parâmetro de entrada de seu construtor uma variável booleana chamada EnableThreadTracking. Ele não permite que o lock entre duas vezes. Caso isso aconteça, quando EnableThreadTracking = True, irá ocorrer um erro. Se definido para false, poderá haver um deadlock. Assim, recomendo que utilize como true.

Ainda é possível utilizar o método TryEnter para checar quando a entrada no lock é possível (o método Enter também faz isso, mas ele aguarda por um tempo indeterminado – constante INFINITE – e entra quando possível). Assim, é possível fazer um loop que aguarde enquanto não é possivel entrar e tratar o comportamento do sistema.

Quer entender mais sobre esse mecanismo?

Os links possuem exemplos que não estão em Delphi, mas qualquer interessado com um pouco de empenho conseguirá entender:

Use com sabedoria.

TMultiReadExclusiveWriteSynchronizer

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.SysUtils.TMultiReadExclusiveWriteSynchronizer

Um dos grandes problemas das threads é na utilização dos recursos compartilhados – se você leu até aqui, já deve ter percebido isso. Por se tratar de processos concorrentes, tudo o que é compartilhado precisa ser controlado para que não ocorram problemas. Porém, imagine um cenário onde poucos processos alteram os valores, mas muitos outros apenas utilizam a informação desses recursos compartilhados. Quando você possui um valor e esse valor precisa ser “lidos” em vários processos concorrentes, não há problema que todos esses processos o utilizem ao mesmo tempo, concorda? O problema reside se algum desses processos queira alterar o valor. Mas somente utilizar o valor que já está la? Não tem risco algum.

TMultiReadExclusiveWriteSynchronizer foi criado justamente para isso. O seu funcionamento é muito simples. Quando uma thread deseja utilizar o recurso, ela determina a sua intenção: ler ou escrever. Se ela deseja apenas ler, o seu acesso é liberado, desde que ninguém esteja escrevendo. Assim, você pode ter diversas threads utilizando o mesmo recurso, se todas forem apenas ler o conteúdo, sem travar nenhuma das threads. Porém, se alguma thread utilizar TMultiReadExclusiveWriteSynchronizer como escrita, todas as threads que tentarem o lock novamente serão travadas, mesmo que sejam apenas para leitura, enquanto o processo de escrita não tenha terminado.

Isso é um ótimo mecanismo para agilizar aqueles processos que utilizam o recurso apenas para leitura, e quando o processo de escrita não seja tão frequente.

A primeira thread foi projetada para escrita. As demais, apenas para leitura. Execute o código acima veja que enquanto elas estão apenas lendo, elas executam concorrentemente sem problemas, entratante, quando a primeira thread pega o lock, todas aguardam a sua liberação.

Existe ainda a interface IReadWriteSync que pode ser utilizada ao invés da classe, se você preferir.

TThread.WaitFor

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Classes.TThread.WaitFor

O recurso já foi utilizado anteriormente quando explicamos os eventos, e aqui a lógica é a mesma. A utilização do WaitFor trava a execução de uma thread enquanto “aguarda” por algo que ocorre em outra thread. Aqui também vale lembrar que aguardar indefinidamente pode fazer com que seu programa trave, então utilize de forma que isso não ocorra.

WaitFor aguarda a execução da thread e retorna um valor inteiro, informado pela thread que está sendo aguardada, através de SetReturnValue:

A thread precisa ser manipulada de forma que você tenha o controle da destruição dela, pois se ela se autodestruir (T1.FreeOnTerminate := False;), você não terá acesso no momento da checagem se ela retornou com algum valor (if T1.WaitFor = 0 then).

O uso do waitfor possibilita o desenvolvimento de sistemas mais complexos onde existe uma sincronia entre os processos. Com ele, você consegue saber exatamente quando um processo terminou e utilizar isso para dar sequência em outras threads.

PPL – Parallel Program Language

Documentação: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Using_the_Parallel_Programming_Library

O Delphi XE7 trouxe melhorias significativas para o desenvolvimento de softwares multiprocessados e disponibilizou recursos de paralelismo para o desenvolvimento de sistemas. Existe ainda toda uma sobrecarga de recursos com a criação e gerenciamentos das threads. A PPL, por outro lado, é feita para utilizar os recursos de hardware multiprocessados e dar o poder da computação paralela aos sistemas atuais.

Thread Pool

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Threading.TThreadPool

TThreadPool é a classe que cuida do gerenciamento automático das threads em execução. Ela diminui a sobrecarga da criação de todas as threads individualizadas e ainda possui a inteligência de lidar eficientemente com a carga de todos os processadores. Essa classe pode ser reescrita pelo desenvolvedor, mas somente por uma boa razão, pois a classe nativa já da conta do recado.

Obs: Muitos desenvolvedores preferem utilizar o FastMM, que possui seu próprio pool de threads.

TTask

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Threading.TTask

Uma tarefa é a execução de um trecho de código de forma paralela. Imagine que o software vá exportar um arquivo texto. Você pode simplesmente delegar isso a uma tarefa e deixar sua thread principal livre para responder aos comendos do usuário. A ideia aqui por trás é a criação de um mecanismo simples e fácil que execute paralelamenrte trechos de código, semelhante à chamada de TThread.CreateAnonymousThread.

Aqui também vale lembrar que tudo o que foi dito a respeito de como pensar sua aplicação para trabalhar com threads se aplica também para trabalhar com tasks e qualquer outro mecanismo de paralelismo.

TFuture

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Threading.TFuture 

http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Threading.IFuture

TFuture é uma task que retorna valor. Você pode pensar nessa classe como uma função que roda de forma paralela (ao contrário da task, que pode ser entendido como uma procedure que executa de forma paralela). Ela foi implementada de uma forma graciosa: o sistema aguarda enquanto o resultado ainda não foi processado.

Sua declaração é bem simples. Graças ao uso dos tipos genéricos (generics), você pode, no momento da criação de TFuture, definir qual o tipo de retorno desejado:

MinhaFuncao: IFuture<String>;

O código acima declarou uma variável do tipo IFuture<string>, que pode ser entendido como uma função que irá retornar uma string.

Para utilarmos, basta:

O fato mais legal aqui é que nesse momento o método anônimo já passa a ser processado, mas ainda não utilizamos o valor de retorno. Mas isso não importa, porque em algum momento vamos utilizá-lo, e quando isso acontecer, ele já vai estar calculado.

A origem do nome vem justamente dessa característica, porque criamos a função para utilizarmos seu valor futuramente. Assim, adiantamos o processamento para uso futuro.

Abaixo um exemplo completo de utilização:

Veja agora que vou adicionar um Sleep(5000) antes de fazer a chamada do retorno. Veja que o comportamento visual muda, porque o tempo de processamento das tarefas já foi realizado:

Com isso você pode perceber claramente a principal característica desse tipo de função paralela.

Você pode querer, em determinadas situações, se certificar de que todas as tarefas paralelas foram executadas antes de realizar algum procedimento. Para isso, existe o método WaitForAll:

WaitForAll aguarda a execução de todas as funções paralelas para prosseguir com o processamento do restante.

Talvez você queira esperar a execução de uma tarefa apenas, seja qual delas que execute primeira. Para isso, você pode utilizar WaitForAny, da mesma forma que utilizou WaitForAll. Saiba apenas que o método ira aguardar a execução de qualquer uma, sendo que aquela que terminar primeiro, já libera o processamento para seguir adiante.

Acredito que essa seja umas das melhores melhorias já feitas com processamento paralelo dentro do Delphi, porque dá poder ao desenvolvimento de sistemas e facilidade ao desenvolvedor.

Loop “For” paralelo

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Threading.TParallel.For

No lançamento do Delphi XE7, esse foi um dos recursos mais “aclamados”. O loop for paralelo é o famoso loop “for” que todos conhecemos, mas com a possibilidade de execução paralela. Assim, cada iteração do loop é executado de forma paralela, dando poder de processamento e desempenho.

Não pense que você pode simplesmente refatorar seu código de modo a simplesmente trocar a instrução “loop” padrão para o “loop paralelo”. Todas as regras vistas acima sobre recursos compartilhados e problemas de paralelismo e concorrência se aplicam ao loop “for paralelo” também, então saiba que a utilização desse mecanismo precisa ser muito bem planejada e sua migração é complexa.

Como regra geral, tente fazer com que o código a ser executado dentro do loop tenha a menor dependência possível com recursos compartilhados. De fato, tente não usar recursos compartilhados dentro dele, para que a execução seje 100% independente.

Nem sempre a migração para o paralelismo é uma coisa boa. A necessidade de controle e gerenciamento da concorrência e do paralelismo gera uma sobrecarga no sistema e, caso o design da sua aplicação não saiba aproveitar de forma eficiente esses recursos, com certeza o sistema ficará mais lento do que se não estivesse utilizando esses recursos.

Abaixo encontra-se a implementação básica do “loop paralelo”:

Temos na instrução TParallel.&For, que o código dentro de procedure(AIndex: Integer) será executado 10 vezes, indo de 1 à 10. O loop for paralelo trabalha criando uma task para cada iteração que será executada.

Veja que fiz com ele fosse executado dentro de uma Task. Poderia não ter feito isso. O código interno dentro do loop ainda seria executado de forma paralela, porque esse mecanismo simplesmente cria uma task para cada iteração do loop e a executa paralelamente, mas preferi fazer dessa maneira porque o controle do loop passa a não ser mais da main thread (thread principal) e sim da task externa, fazendo com que a tela fique responsível.

Não existe uma ordem de execução das iteração. Parece estranho, não é? Mas tenha em mente que o código será executado paralelamente. Isso significa que você não sabe quando as iterações irão ocorrer, e qual a ordem das iterações. Pode ser que a iteração 7 ocorra antes da iteração 3. Por isso o seu código interno de execução não pode utilizar essa ordenação como regra de negócio.

Mas e se eu precisar de algum tipo de tratamento quanto a isso?

TLoopState

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Threading.TParallel.TLoopState

Conforme a documentação oficial, o loop paralelo possui diversas sobrevaragas de métodos dando a possibilidade de “encaixe” em diferentes situações. Uma dessas possibilidades é a utilização da classe TLoopState, que armazena as informações do “estado” do loop.

TLoopstate possui os métodos “break” e “stop”, que são utilizados para a parada da execução do loop. Ainda, possui a propriedade “stopped” para identificar quando o loop não deve mais ser executado. Lembre-se que, quando trabalhamos com uma thread, elas são “notificadas” da parada, e não abruptamente paradas. Assim, o código interno do processamento paralelo deve prever essas situações e se utilizar desses recursos para tratar as situações de forma elegante.

Vamos declarar uma variável para a tarefa de forma global no relatório e uma para armazenar o resultado do loop:

Agora vamos criar uma procedure (em um TButton) para implementar o funcionamento:

O procedimento acima apenas cria um loop com 100 iterações de forma paralela, sempre verificando o status da execução do loop.

O comando:

TThread.Sleep(500);

existe apenas para nos dar tempo (como usuários do sistema de exemplo) para a parada do loop, para testarmos o funcionamento.

Em outro botão temos:

o código altera o “status” da execução para cancelado. Lembra que no código anterior verificamos o status de cancelamento?

Em outro botão, temos:

Aqui apenas estamos conferindo se o loop foi interrompido ou não.

A lógica de execução desse exemplo deve ser:

  1. Execute o loop
  2. Verifique o status da parada

Com esse fluxo, você verá que todas as iterações do loop foram realizadas e o status final foi “Completou com sucesso!”.

Agora, para exemplificarmos um cenário onde ocorreu uma parada:

  1. Execute o loop
  2. Execute a parada do loop
  3. Verifique o status da parada

Nesse cenário, o retorno foi “Houve uma parada!” Consegue perceber também que o loop não para no momento do cancelamento, pois ainda existem iterações que já foram disparadas e estão sendo processadas no exato momento do cancelamento, então tenha isso em mente quando for construir seus sistemas.

Podíamos ainda utilizar “Break” e “Stop” do TLoopState para pararmos as outras iterações, como por exemplo:

Tanto Break como Stop param a execução das iterações dos loops, mas cada um tem sua característica singular. Para entender a diferença, é necessário lembrarmos que a execução dos loops não possui uma ordem exata, ou seja, o “Index 90” pode executar antes do “Index 10”. Stop para qualquer nova iteração do loop, não importando qual seu index, sendo que Break para novas iterações onde o Index dessas iterações forem maior do que o index onde ocorreu o break, ou seja, se o break ocorre na iteração de index 50, todas as iterações onde o index for menor que 50, ainda ocorrerão, mesmo que ainda não foram executadas. Isso te dá o poder de trabalhar melhor o fluxo de suas aplicações, não é?

TLoopResult 

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Threading.TParallel.TLoopResult

Simplesmente armazena as informações do resultado do loop paralelo. Sua utilização já foi exemplificada no exemplo anterior, e utilizamos suas informações para determinar quando houveram paradas do loop.

Tratamento de exceções nas Tasks

 

Recomendo a seguinte leitura para o tratamento de exceções com as tarefas, do blog do Robert Love: http://robstechcorner.blogspot.com/2015/02/tpl-ttask-exception-management.html

 

Problemas comuns com processamento paralelo

 

Recomendações

Diagnóstico de Performance com TStopWatch e TTimeSpan

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Diagnostics.TStopwatch

O Delphi possui uma função de diagnóstico muito últil para medir a performance do sistema. No momento de melhorar a performance de um trecho de código, geralmente o desenvolvedor faz algo como:

Isso basicamente pega a data antes da execução, pega a data depois da execução, e depois mostra a diferença dos tempos entre as datas.

Isso funciona na maioria dos casos.

Caso você precise de uma maior precisão na medida do tempo, você pode utilizar GetTickCount: https://reisthales.wordpress.com/2016/03/30/delphi-medindo-o-tempo-de-execucao-de-um-processo/

Contudo, o Delphi possui uma classe preparada para esse tipo de diagnóstico, chamada TStopWatch. Sua utilização segue abaixo:

Embora TStopWatch seja um record, ele precisa ser inicializado, seja pelo método Create, seja pelo método StartNew. Ambos criam e inicializam o StopWatch. A diferença é que Create cria com o contador de tempo parado, e StartNew já cria com o contador de tempo girando.

StopWatch.Start inicia a contagem do tempo e StopWatch.Stop para a contagem do tempo. Isso feito, você consegue obter os valores em:

StopWatch.ElapsedMilliseconds – Retorna o total de tempo em milesegundos.

System.Diagnostics.TStopwatch.ElapsedTicks – Retorna o total de tempo em milesegundos (também).

System.Diagnostics.TStopwatch.Elapsed – Retorna o total de tempo em formato TTimeSpan – o melhor a ser usado.

Existem ainda outras funcionalidades como StopWatch.Reset, que reinicia o contador do StopWatch.

TTimeSpan

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.TimeSpan.TTimeSpan

TTimeSpan é um record com funcionalidades específicas para tratamento de tempo. Assim que ele é retornado pelo método “Elapse”, você pode utilizar um de seus métodos:

Assim, você pode obter o retorno dessas informações facilmente em milesegundos, segundos, minutos, horas, dias, etc.

Pronto para melhor diagnosticar seu código?

TVirtualMethodInterceptor

Documentçãohttp://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TVirtualMethodInterceptor

Talvez você nunca tenha ouvido falar, mas o Delphi possui a classe TVirtualMethodInterceptor. Ela exite para ser algo parecido com que o .Net e Java possuem à respeito de proxies dinâmicos.

Considere o código abaixo:

Como pode ver, a classe TVirtualMethodInterceptor intercepta a execução do método “TTeste.Msg” e modifica o seu retorno. A interceptação é da chamada, e não do funcionamento interno do método.

Veja que isso funciona porque o método TTeste.Msg é virtual. Quando você executar esse código, perceberá que mais de uma execução da interceptação ocorre, porque no momento de destruir a classe, outros métodos virtuais da classe são chamados. Todavia, isso pode ser evitado simplesmente tratando quando executar pelo parâmetro “Method”:

O evento “OnBefore” possui 5 parâmetros:

  • Instance – Ponteiro para a instância do método que está sendo executado.
  • Method – Método que está sendo executado.
  • Args – Parâmetros do método que está sendo executado.
  • DoInvoke – Determina se o método será chamado ou não.
  • Result – Define o resultado do método.

Todos esses parâmetros são poderosos, mas chamo a atenção para os dois últimos. Com eles, é possível impedir a execução do método original e retornar um determinado valor. Assim, quem chamou o método não percebe, mas na verdade o método original não foi executado, sendo interceptado e tendo outro valor retornardo, continuando com o fluxo da aplicação.

Também é  possível implementar ações para executarem após a execução do método, através de “OnAfter“:

  • Instance – Ponteiro para a instância do método que está sendo executado.
  • Method – Método que está sendo executado.
  • Args – Parâmetros do método que está sendo executado.
  • Result – Define o resultado do método.

Por se tratar de um evento que é executado depois da execução do método, ele não possui a capacidade de impedir a execução, como o evento “OnBefore”.

De qualquer forma, ele ainda pode ser útil para implementação de algumas funcionalidades, como logs, por exemplo.

Conclusão

A utilização de proxies dinâmicos é muito poderoso, porque dá a capacidade de interferir no fluxo normal do sistema, interceptando a chamada do método e alterando os resultados. Além disso, dá a possibilidade de gerar eventos para esse método, onde funcionalidades adjacentes podem ser desenvolvidas.

Delphi Code Coverage e Delphi Code Coverage Wizard

Os testes unitários são fantásticos para descobrirmos os problemas de nosso código-fonte, porque suas rotinas exaurem as “unidades” em busca de problemas, certificando nosso código-fonte e nos dando a tranquilidade de seguirmos em frente, sabendo que o trabalho foi bem feito. Todavia, o que garante que testamos todos os pontos do sistema? Quanto maior o sistema, maior a probabilidade de esquecermos de algo, até porque, em grandes sistemas como ERPs, temos toneladas de classes e mais classes, funções e mais funções, que precisam ser devidamente analisadas.

Para nossa felicidade, existe algo que faz a análise da cobertura do código fonte. Entendendo melhor isso, faço a seguinte pergunta: e se houvesse algo que dissesse exatemente onde o seu código foi testado, e onde não foi? Parece genial, não é? Pois é exatamente isso que uma ferramenta de cobertura de código faz. Ela rastreia a execução do sistema e verifica cada linha do código-fonte que foi executada e nos retornar posteriormente uma análise demonstrando qual parte do sistema não foi executado. Isso, alinhado com um framework de testes unitários como o DUnitX, por exemplo, demonstra qual parte do seu sistema não foi testada. No Delphi, utilizamos para isso o Delphi Code Coverage.

 

Delphi Code Coverage

Site do Projeto: https://github.com/magicmonty/delphi-code-coverage

O Delphi Code Coverage é um programa bem simples. Ele utiliza as informações contidas no arquivo .map gerado pelo Delphi para identificar quais linhas do código-fonte a execução do programa percorreu.

Uma vez tudo configurado, a execução do Delphi Code Coverage gera um relatório com os percentuais entre os totais de linhas geral e aqueles que foram utilizados, além de identificar quais linhas não foram utilizadas. Assim, o desenvolvedor tem tudo o que precisa para aprimorar seus testes e fazer com que todas as linhas do código-fonte sejam testadas.

Configurações do Delphi

Como visto, é necessário que o arquivo .map do Delphi seja gerado para o correto funcionamento da ferramenta.

Para isso, vá em: Menu Project->Options->Delphi Compiler->Linking e altera a opção Map File para Detailed.

Map File
Arquivo .Map do Delphi

Configurando o Delphi Code Coverage

Dois arquivos são necessários para o funcionamento do Delphi Code Coverage:

  • dcov_paths.lst – Informa quais os caminhos do código-fonte (paths).
  • dcov_units.lst – Informa quais units, dentro do caminho informado em dcov_paths.lst, terão a cobertura de código.

Veja que isso é feito de propósito. Porque não apenas informar o caminho e considerar todas as units daquele caminho? Porque nem sempre todas as units deverão sofrer essa análise. O mesmo vale para informar em dcov_units.lst a unit do seu projeto. O Delphi Code Coverage é inteligente o suficiente para identificar as “units dentro das units”, declaradas nos “uses” do código-fonte, e realizar o rastreamento delas também. Assim, se você informar a unit do seu projeto, a ferramenta de cobertura de código vai atrás de todas as units informadas no projeto. Mas isso é interessante? Provavelmente não. Existem units que não devem passar pela análise porque dizem respeito à interfaces gráficas ou qualquer outra coisa que não envolva regras de negócio. Não estou dizendo que isso não precise ser testado, mas isso passa por outras etapas de testes. O que queremos aqui é verificar se as nossas regras de negócio estão sendo tratadas pelos testes unitários.

Veja exemplos dos conteúdos desses arquivos:

dcov_paths.lst

O conteúdo acima é bem simplista, apenas para compreender o correto funcionamento. Veja que é possível inclusive informar o caminho absoluto ou relativo, conforme sua necessidade.

dcov_units.lst

Outra vez bem simples. Apenas o nome das units, sem qualquer extensão.

Pronto! Já podemos utilizá-lo. Mas e agora? Como faço isso?

Como o Delphi Code Coverage  é um programa console, você deve utilizá-lo através do console do windows:

Vamos entender cada ponto:

  • CodeCoverage.exe – É o comando para execução do CodeCoverage.
  • -e “C:\Projetos\Fontes\ProjetoTeste.exe” – (e = Executable ou Executável)- Informa o caminho do executável do nosso projeto de teste.
  • -m “C:\Projetos\Fontes\ProjetoTeste.map” – (m = map) – Informa o caminho do arquivo .map gerado pelo Delphi.
  • -uf dcov_units.lst – (uf = unit file ou arquivo das units) – Caminho do arquivo dcov_units.lst, que é o arquivo que contém as units a serem analisadas.
  • -spf dcov_paths.lst – (spf = source paths file ou arquivo dos caminnhos do fonte) – Caminho do arquivo dcov_paths.lst, que é o que contém os caminhos das units informadas em  dcov_units.lst.
  • -od “C:\Projetos\Fontes\CodeCoverageOutput\Reports\” – (od = output directory ou diretório de saída) – Caminho do diretório de saída do Delphi Code Coverage. É o caminho onde serão gerados os relatórios.
  • -lt – (lt = logging text ou arquivo texto de log) – Informa o caminho do arquivo de log do programa. Se não for informado um caminho, será gerado o arquivo Delphi-Code-Coverage-Debug.log.
  • -emma e -meta – Essa opção gera o arquivo coverage.es no formato Emma no diretório de saída. Esse arquivo pode ser utilizado junto ao programa Emma, que é um programa muito conhecido de cobertura de código para Java (Quer saber mais sobre Emma? Acesse: http://emma.sourceforge.net). A opção -meta informa que devem ser gerados arquivos separados para a cobertura e os metadados (arquivos coverage.em e coverage.ec). Não é necessário se não for utilizar Emma.
  • -xml – Gera o relatório CodeCoverage_Summary.xml em formato XML no diretório de saída.
  • -html – Gera o relatório CodeCoverage_Summary.html em formato HTML no diretório de saída.

Ainda é possível utilizar outras opções como:

  • -dproj – Informa o caminho do projeto do Delphi.
  • -u unit1 unit2 – Informa uma lista de units para a criação dos relatórios (informadas na própria linha de comando).
  • -esm mask1 mask2 – Uma lista de arquivos para excluir da execução da cobertura do código.
  • -uf filename – Informa uma lista de units para a criação dos relatórios (informadas em um arquivo, onde filename é o caminho do arquivo).
  • -sd directory – (sd = source directory ou diretório dos fontes) – Informa o caminho do diretório dos fontes, onde “directory ” é o caminho.
  • -a param param2 -Lista de parâmetros passada para a aplicação no momento da execução.
  • -v – (v = verbose output ou saída detalhada) – Informa que deseja visualizar as saídas de forma detalhada.
  • -lapi – Informa que deseja utilizar WinAPI OutputDebugString para o debug.
  • -ife – Inclui prefixos de arquivos.
  • -efe – Exclui prefixos de arquivos.
  • -mns name dll [dll2] – Crie um namespace separado com o nome dado para as DLL listadas.
  • -uns dll_or_exe unitname [unitname2] – Crie um namespace separado (o namespace será o nome do módulo sem extensão) – somente – para as unidades listadas dentro do módulo.

Algo legal que se fazer é criar seu próprio .bat para executar seu comando, facilitando as próximas execuções da cobertura do código.

Analisando o relatório

Agora que executamos a cobertura de código, foi gerado o arquivo CodeCoverage_Summary.html:

Relatório do Delphi Code Coverage
Relatório do Delphi Code Coverage

Ele mostra os percentuais entre o total de linhas da unit e aquelas que tiveram execução.

Relatório de Análise da Unit UExemplo.pas
Relatório de Análise da Unit UExemplo.pas

Veja que as linhas em verde são onde a execução do programa passou. As linhas em azul indicam que não foram executadas. Assim, fica claro que precisamos desenvolver para elas testes que as utilizem e analisem seus resultados.

Simples não é?

 

Delphi Code Coverage Wizard

Site do Projeto: https://github.com/trident-job/delphi-code-coverage-wizard

Desenvolvido para facilitar a configuração do Delphi Code Coverage, o Delphi Code Coverage Wizard gera os arquivos necessários para o funcionamento da cobertura de código. Assim você não precisa se preocupar com todas as configurações do Delphi Code Coverage, porque o Wizard irá lhe auxiliar quanto a isso. Todavia, saiba que conforme sua exeperiência e necessidades na utilização dessa ferramenta de cobertura de código forem crescendo, maior aa tendência para não utilizar o wizard, visto que suas necessidades irão se modificando e se aperfeiçoando no uso da ferramenta.

Na execução do wizard, temos depois da tela de boas vindas:

Primeira tela do Wizard
Caminhos do executável e do arquivo map
Caminho dos Fontes e Units
Definição do caminho dos fontes e quais units serão analisadas
Caminhos de saída
Definições dos caminhos de saída dos arquivos e dos relatórios das análises, assim como configurações dos tipos de saídas esperados
Informação do tipo de caminho
Informa se deseja que os caminhos sejam relativos ao script
Etapa final
Etapa final, onde é informado se quer apenas gerar o script ou se quer já rodar a cobertura do código fonte.

Perceba que o Wizzard faz exatamente o que fizemos antes. A vantagem dele é justamente facilitar a criação de todas as configurações necessárias.

Valo notar que o Wizzard possui seu próprio arquivo do Delphi Code Coverage.

 

Outras Informações

Quer saber mais sobre testes unitários? Click Aqui!

Quer saber mais sobre o arquivo .map? Click Aqui!

Quer tentar outras ferramentas de cobertura de código para Delphi? Qua tal tentar o Discover for Delphi? Click Aqui!

Unit Test em Delphi com DUnitX, Delphi Mocks e TestInsight

Softwares de baixa qualidade são altamente frustantes para os usuários, por isso uma das etapas mais importantes no desenvolvimento de sistemas é a etapa de testes.

Quando falamos de software, falamos de diferentes tipos de testes possíveis de serem aplicados, em diferentes momentos do ciclo de vida do desenvolvimento. Temos os testes de integração, os testes de aceitação, testes de interface, entre outros. Um deles é o teste unitário.

Teste Unitário

O teste unitário visa testar a menor parte possível do seu código-fonte. O que será testado será justamente o código-fonte, e não o sistema como um todo, ou como interage com outros sistemas ou dispositivos. Então a primeira coisa a se definir é: o que é uma unidade?

Uma unidade é a menor parte do seu código que pode ser testada isoladamente. Dentro do Delphi, teríamos que a menor coisa funcional que pode ser testada é uma procedure ou função. Mas o que seria uma coisa isolada? Seria algo que não possui dependências, que funciona sozinha.

Imagine uma função que comunique-se com um webservice e retorne um valor desse webservice. Não poderíamos chamar o teste dessa função de teste unitário porque a função não está isolada se fizer uma comunicação com outro elemento, nesse caso, o webservice, porque dessa forma, a lógica interna da função depende obrigatoriamente da lógica do webservice. Nesse caso, faria mais sentido realizar um teste de integração. De qualquer forma, você vai ver que mesmo nesse cenário onde existem elementos dependentes (o que é grande parte de qualqer sistema, afinal, as coisas devem se comunicar!), existe uma forma de isolar cada elemento e testá-lo individualmente.

Legal, mas na prática, o que isso significa? Significa que você desenvolve programas que testam programas. Isso mesmo, os testes são manualmente desenvolvidos para testar o código.

Vantagens do teste unitário

  • O quanto antes, melhor!

Quando falamos de qualidade de software, sabemos que quanto antes encontrarmos as falhas dentro do ciclo de vida do sistema, melhor! Isso porque irá custar menos para ser resolvido, e os impactos com o tempo do projeto e desgaste com o cliente serão diminuídos.

  • Facilidade em encontrar falhas e eliminá-las de forma rápida.

Com o teste unitário, você tem uma enorme facilidade (depois do teste escrito) de verificar se a sua “unidade” está funcionando adequadamente, porque basta para isso simplesmente rodar o teste, e os resultados já lhe serão expostos. Assim, como você já vai saber onde está dando problema, e quais os valores que geram esses problemas, fica fácil simplesmente corrigi-los.

  • Cobrir todas as possibilidades do seu código (code coverage), para garantir que tudo funcione adequadamente.

A própria confecção natural do teste unitário fará com que você testes todas as possibilidades possíveis. Dessa forma, você estará testando tudo o que seu software faz (pelo menos a parte que importa), e com isso, cobrindo com maior qualidade e segurança o funcionamento do sistema.

  • Parte da documentação do seu código-fonte.

Ao crciar os testes unitários, você estará criando uma biblioteca de informação dizendo quais os valores esperados e quais os não esperados. Isso facilita a legibilidade e discernimento do código-fonte principalmente pelos novos membros da equipe.

  • Economia de tempo (e dinheiro!) .

Embora existe o tempo gasto para o desenvolvimento dos testes unitários, depois de feitos, eles economizam muito no tempo de testes e principalmente impedem o retrabalho. Além disso, você será capaz de testar inclusive o funcionamento dos códigos antigos. Caso precise realizar uma alteração em algum ponto do sistema, basta rodar os testes e testar todo o sistema.

  • Isolar o seu código de forma que você tenha certeza de que ele funcione corretamente.

Quando desenvolvemos testes unitários, temos a necessidade de desenvolver códigos com o menor grau de dependência, para facilitar os testes. Isso nos força a desenvolver códigos com um design melhor, que nos ajudarão inclusive em outros aspectos do desenvolvimento.

 

Como escrever bons testes?

  • Siga o princípio AAA – Arrange, Act e Assert.

O princípio AAA determina que um bom teste segue a seguinte ordem:

  1. Arrange – preparar todas as condições necessárias para a execução do teste.
  2. Act – executar o que será testado.
  3. Assert – validar as informações depois do teste.

Seguindo esse simples princípio, é possível separar o que será testado de todas as estapas de preparação e verificação do resultado, tornando claro “o que é o que”. Isso também é importante para os outros tópicos abaixo.

  • Apenas uma checagem por teste.

Faça com que seus testes verifiquem apenas uma única coisa, por teste. Fazendo isso, todos os testes serão classificados de forma melhor e simplificados em seu funcionamento.

  • Não utilize lógica em seus testes.

Testes não devem possuir condicionais – ifs e cases – ou qualquer outro tipo de regra de negócio. Isso apenas dificultaria os testes e retiraria sua objetividade.

  • Os testes precisam ser fáceis de serem executados.

Se você está montando um teste e ele está difícil de se concluir, com certeza você está fazendo errado. Siga os princípios AAA e tente dar o maior grau de objetividade para seus testes.

  • Faça os testes com os valores esperados primeiro, e depois os que tentam “encontrar” o problema do código.

Em sua série de testes, comece sempre com aqueles que testam os valores esperados, ou seja, os testes que checam o comportamento normal do sistema. Depois, parta para aqueles que verificam os valores que podem atingir o limite dos tipos utilizados (valor máximo de um número inteiro, por exemplo) e os que podem prejudicar o funcionamento do sistema (um pagamento com valor negativo, por exemplo).

  • Se for possível, realize todos os testes possíveis. Teste todas as possibilidades do seu código.

Quanto mais testes, maior a cobertura do sistema. Quanto mais você teste todas as possibilidades possíveis, melhor qualidade está sendo aplicado ao sistema.

  • Todos os testes precisam ser independentes.

Os testes não podem serguir uma ordem de execução para funcionarem. Todos os testes precisam ser independentes, até porque frameworks de testes como o DUnitX não possuem nada que garanta uma ordem certa de execução dos testes.

  • Todos os “exceptions” precisam ser testados.

Se você possui algum tratamento dentro das suas classes que gerem excessões você precisa se certificar que todas as exceções foram testadas.

  • Cria nomes claros e elucidativos para os seus testes.

A quantidade de testes diferentes tende a ser enorme, então garanta que o próprio nome dos testes já identifique para que eles servem. Nomenclaturas de testes como “ContarAQuantidadeDeItensDoPedidoDeVenda” devem ser encorajadas.

  • Rode seus testes constantemente.

Sempre execute seus testes para que esteja sempre sabendo quais não deram certo, na maior parte das situações. O TestInsight possui um recurso muito interessante que roda os testes quando identifica ociosidade da IDE (veremos mais à frente).

  • Apenas crie os testes necessários.

Desenvolver os testes tomará parte do seu tempo, então apenas crie testes para as áreas do sistema que realmente importam. Outras partes do sistema onde erros são de pequeno impacto, podem ser deixadas de lado. Mas sempre crie os testes para aquelas que impactam!

 

DUnitX (Test Framework)

Site do projeto: https://github.com/VSoftTechnologies/DUnitX

Com o Delphi 2010 e a evolução da RTTI do Delphi, foi possível evoluir os frameworks existentes. Assim, recomenda-se hoje a utilização do framework DUnitX ao invés do próprio DUnit, que já vinha com o Delphi de longas datas. As últimas edições do Rad Studio já vem com o DUnitX, mas para as outras versões, é necessário realizar o download das bibliotecas e realizar a instalação.

DUnitX utiliza os recursos de atributos do Delphi inseridos na versão Delphi 2010, e os principais são:

  • [TestFixture] – Define que a classe é uma classe de testes. O framework utiliza essa marcação para identificar o que utilizar para os testes.
  • [Setup] – Define o método responsável pela configuração de tudo o que é necessário para o funcionamento do teste. Lembra do princípio AAA? Esse é o responsável pelo “Arrange”, ou seja, preparar todas as condições necessárias para a execução do teste. É a primeira coisa a ser executada dentro do teste.
  • [TearDown] – É o contrário do Setup, ou seja, desfaz tudo o que foi criado para os testes. É a última coisa a ser executada dentro do teste.
  • [Test] – Define a procedure que testará a funcionalidade.
  • [TestCase(‘TestA’,’1,2′)] – Define o caso de teste.

Tudo isso ainda é muito abstrato. Isso é muito simples, mas só tomamos consiência disso quando colocamos em prática. Antes, apenas, temos que chamar TDUnitX.RegisterTestFixture para que as funcionalidades do DUnitX sejam carregadas.

No exemplo de código àcima, temos a classe TMinhaClasseDeTeste. Perceba que ela possui o atributo [TestFixture] que indica ao framework que ela é uma classe de testes. Dentro da classe temos os métodos “Setup” que possui o atributo [Setup], indicando que esse é o método de setup dos testes, e o método “TearDown”, que possui o atributo [TearDown], indicando que esse é o método de finalização dos testes. Os nomes dos métodos não precisam ser obrigatoriamente Setup e TearDown, porque o que realmente importa é o atributo. Essa lógica serve para o resto também. Temos a procedure “Test1”, com o atributo [Test], indicando que esse é um método de teste, e a procedure “Test2”, também com o atributo [Test] e adicionalmento com os atributos [TestCase()], que funcionam como casos de testes. Nesse caso, o próprio atributo já dispara os testes com as informações contidas nele.

Um ponto importante é verificar que na seção “initialization” chamamos  “TDUnitX.RegisterTestFixture(TMinhaClasseDeTeste)”, que é o comando responsável pelo registro da classe de teste.

Com a estrutura básica já conhecida, podemos realizar nossos próprios testes.

Vamos imaginar que estamos trabalhando em uma tela de cadastro de pessoas. Nessa tela, um dos campos é o CNPJ e CPF, ou seja, o código dos documentos pessoais. Temos que o formato padrão do CPF é XXX.XXX.XXX-XX e do CNPJ XX.XXX.XXX/XXXX-XX. Para nossa facilidade e comodidade no futuro, optamos por armazenar essas informações sem os caracteres “.”, “/” e “-“. Assim, optamos por armazenar apenas os números do CNPJ e CPF.

Para isso, vamos criar uma função que receba uma string e retorne uma string, mas que contenha somente números na string de retorno, não importando a string de entrada. Assim, poderíamos passar qualquer string e a função nos retornaria somente strings com a parte numérica.

Vamos desenvolver a função que retorne somente números:

Na função àcima foi intrudizo um erro proposital (variável “ErroInduzido”), para que possamos verificar como o Framework trabalha com isso. Veja que estamos simulando um problema real, porque quando estamos desenvolvendo não sabemos quando estamos criando erros ou não. Agora que já temos a função criada, vamos tratar dos testes.

A primeira coisa a se fazer é criar o projeto de testes no Delphi. Abaixo está um exemplo bem simples dele, que foi retirado do projeto padrão do DUnitX do Delphi Berlin (com algumas simplificações):

A primeira coisa que nos vem na cabeça é:  {$APPTYPE CONSOLE}?

Exato. DUnitX gera uma saída no console, o que não é o mais elegante possível, mas não devemos nos preocupar com isso. Existem outras formas de recuparar a saídas dos testes. O restante é apenas as units necessárias para o correto funcionamento do framework.

A Unit UExemplo já está adicionada no projeto àcima e não faz parte do framework. Isso porque é a unit que iremos testar.

A Unit UTestes já está adicionada àcima e não faz parte do framework. É a unit da classe de teste, que iremos desenvolver agora:

Veja que ela segue a estrutura básica já vista anteriormente. Aqui, temos a implementação do método de setup:

do método de teardown:

e do próprio teste em sí:

O código àcima apenas cria a classe que será testada e executa o método que será testado. O retorno desse método é armazenado na variável Aux.

Depois disso, temos a utilização do dispositivo de verificação do DUnitX, chamado Assert (conveniente, não?), onde simplesmente comparamos os valores passados com o retorno esperado.

Assert.isTrue verifica se “Aux = ‘024543886893’” é verdadeiro. Se for, o teste passou, caso contrário, não passou, e a mensagem no segundo parâmetro será mostrada para o usuário: “‘String de entrada A0-2,45%4$38&¨8689.3 deveria retornar 024543886893 mas retornou ‘ + Aux“. Desenvolva mensagens que sejam claras e informativas para o desenvolvedor, para que a correção do problema seja feita de forma fácil.

Vamos rodar o projeto e verificar o resultado.

Mensagem de erro do DUnitX
Mensagem de erro do DUnitX

Veja que ele apresentou a mensagem que 1 teste falhou. Isso porque introduzimos um erro proposital (ErroInduzido := ‘X’;) para que pudéssemos analizar como o DUnitX funciona nesses casos. DUnitX também nos mostrou a mensagem que codificamos caso o teste falhasse.

Agora vamos voltar e retirar o conteúdo do erro induzido, para vermos tudo funcionando. Na unit UExemplo, vamos reescrever o método SomenteNumeros para que fique assim:

O que fizemos foi simplesmente retirar o conteúdo da variável “ErroInduzido”.

Agora vamos rodar novamente e verificar como o DUnitX analisou nosso código:

Mensagem de sucesso do DUnitX
Mensagem de sucesso do DUnitX

Dessa vez nosso teste passou, e podemos dormir em paz sabendo que fizemos um bom trabalho.

Veja que ele segue os princípios AAA:

  • Arrange – preparar todas as condições necessárias para a execução do teste no método de setup.
  • Act – executar o que será testado em “Aux := FTratamentoStrings.SomenteNumeros(‘A0-2,45%4$38&¨8689.3’);“.
  • Assert – validar as informações depois do teste em “Assert.IsTrue(Aux = ‘024543886893’, ‘String de entrada A0-2,45%4$38&¨8689.3 deveria retornar 024543886893 mas retornou ‘ + Aux);“.

Veja também que o teste é bem direto, sem lógica, apenas passando os valores e comparando com o esperado.

Existe ainda a possibilidade de criar os próprios casos de teste diretamente nos atributos dos testes. Vamos reimplementar nossa rotina de teste para receber os valores por parâmetro:

e

Veja que o valor a ser testado está sendo passado como parâmetro assim como o valor esperado do teste.

Na implementação da classe, utilizamos o atributo  TestCase para criar nossos casos de teste. Sem alterar a lógica do teste, estamos realizando 2 testes -Caso 1 e Caso 2 – e identificando os próprios parâmetros de entrada. Atente que cada parâmetro é separado por vírgula, e que qualquer espaço entre as vírgulas também é considerado como parte do parêmetro, porque o framework não tem como saber se é parte do teste ou não.

Rode os testes e veja que os casos de testes são executados.

Existem ainda diversos outro tipos de verificações (contidas na unit DunitX.Assert) que podem ser feitos:

  • Pass
  • Fail
  • FailFmtNotImplemented
  • AreEqual
  • AreEqualMemory
  • AreNotEqual
  • AreNotEqualMemory
  • AreSame
  • AreNotSame
  • Contains
  • DoesNotContain
  • Implements
  • IsTrue
  • IsFalse
  • IsNull
  • IsNotNull
  • IsEmpty
  • IsNotEmpty
  • WillRaise
  • WillRaiseWithMessage
  • WillRaiseDescendant
  • WillRaiseAny
  • WillNotRaise
  • WillNotRaiseDescendant
  • WillNotRaiseAny
  • StartsWith
  • EndsWith
  • InheritsFrom
  • IsType
  • IsMatch

Não será comentado todos esses métodos, que devem ser explorados mais profundamente no site do projeto, mas vale a pena comentar sobre WillRaise – e suas derivações.

Como foi dito anteriormente, é boa prática validar que todos os exceptions do código sejam testados. Você consegue isso utilizando o método de Assert WillRaise.

Vamos criar mais um teste em nossa classe que está sendo testada:

O exemplo é bem simples, apenas para simular uma exceção esperada. Ainda não foi adicionado a exceção para mostrar o funcionamento do teste.

Agora em nossa classe de testes, vamos criar mais um teste para verificar a ocorrência da exceção.

Na interface da classe criamos:

Na implementação do método:

Vamos rodar novamente o teste e veja o que ocorre:

Falha nos testes com exceção.
Falha nos testes com exceção.

Agora vamos implementar a exceção no método:

Vamos rodar novamente e ver como o DUnitX tratou os testes:

Sucesso nos testes com exceção.
Sucesso nos testes com exceção.

Veja que é muito simples a utilização desses framework e que ele pode muito contribuir para a qualidade do seu código. Basta agora explorar ainda mais as suas funcionalidades.

E como tratamos as classes onde existam dependências de entrada como um TFileStream, por exemplo?

Para isso, precisaremos isolar nossa classe para que ela possa ser testada, e utilizaremos para isso o Delphi Mock, um framework de isolamento.

 

Delphi Mock (Isolation Framework)

Site do projeto: https://github.com/VSoftTechnologies/Delphi-Mocks

Delphi Mock é um isolation framework para Delphi, ou seja, uma biblioteca de funcionalidades que vão te ajudar a isolar uma unidade. Uma unidade somente poderá ser testada “unitariamente” se estiver isolada de todo o resto. E se, tendo uma classe que possui uma dependência com outro objeto do sistema, eu quisesse realizar um teste unitário? Teria que primeiramente isolá-la da própria dependência, ou seja, do próprio objeto de que ela depende.

Existem para isso classes que simulam as dependências, de forma que a unidade possa ser testada. Assim, se você precisa de uma classe de um webservice, por exemplo, será disponibilizado para o teste uma classe que irá simular (classe fake) o funcionamento do webservice.

Existem dois desses tipos de classes:

  • Stub – não possui funcionalidade nenhuma. Apenas existe como elemento da dependência.
  • Mock – Possui funcionalidade assim como existe como elemento da dependência. Pode ser utilizado lógica para tratar o retorno da dependência.

Parece confuso, mas não é. Ficará fácil entender esse conceito quando o aplicarmos.

Vamos imaginar um método cuja dependência seja um objeto de log:

Para os nossos testes, vamos imaginar que não seja importante saber se foi logado ou não. Nesse caso, como a dependência não interfere na lógica do método, vamos usar um stub para apenas satisfazer a dependência, e nada mais.

Em nossa classe de teste, vamos implementar:

Não vou entrar em detalhes sobre a implementação da classe de testes porque vimos isso aprofundado quando tratamos do DUnitX.

O importante aqui está no método ValidarMetodoEntrarNoSistema. Como para executar FUsuario.EntrarNoSistema eu precisava obrigatoriamente fornecedor o parâmetro de entrada do tipo ILog, como faria isso na classe de teste, sem prejudicar os testes? Simples! Utilizando o Delphi Mocks, facilmente supri o parâmetro de entrada com “TStub<ILog>.Create“. Mas o que isso faz? Nada de mais. Internamente ele criou um objeto através da generics que implementa a interface ILog e nada mais. Quando TUsuario.EntrarNoSistema chamar o log, ele simplesmente não espera nenhum retorno, apenas está programado para logar. Como o Delphi Mocks gerou esse objeto para nós, facilmente suprimos a dependência e pudemos realizar o teste.

Utilizamos um stub porque ele não produz nenhuma lógica vinculada, ele simplesmente existe para satisfazer a dependência. Se precisássemos de uma operação concreta do objeto do ILog, não seria um teste unitário!

Mas e no caso abaixo, onde preciso de uma informação da dependência?

Veja que agora o método precisa da informação do nome do usuário. Como isolar a classe para que possamos testá-la?

Basta utilizar a classe TMock em sua classe de testes:

Criamos o nosso mock sendo do tipo TMock<IUsuario> e configuramos-o para que retornasse o valor ‘João’ – Mock.Setup.WillReturn(‘João’).

Dessa forma, criamos uma classe fake e fizemos com que retornasse um valor válido para os testes, isolando assim a classe testada e permitindo o teste unitário da classe de usuários.

Legal né?

É possível explorar muito mais o framework e sua capacidade de retornar os dados necessários.

 

Test Insight

Site do projeto: https://bitbucket.org/sglienke/testinsight/wiki/Home

Quanto trabalhamos com o DUnitX, visualizamos os resultados através do console. Quem estava acostumado com o antigo DUnit sentiu falta de uma visualização gráfica dos resultados. A equipe do DUnitX já havia se programado para o desenvolvimento dessa ferramenta, mas optou por abandonar o projeto em virtude do TestInsight, que já realizou o trabalho muito bem.

O TestInsight é uma ferramenta visual para apuração dos resultados dos testes  unitários do Delphi, realizados pelo DUnitX, DUnit e DUnit2.

Uma vez configurado sua utilização, quando rodamos os testes unitários, o visual passa a ser conforme abaixo:

TestInsight by Type
TestInsight by Type

Muito mais elegante.

Além do visual, existem funcionalidades ótimas realizadas por esse plug-in. Quando você dá duplo-click em um teste, ele te redireciona automaticamente para o código-fonte em questão. Você pode visualizar os testes por tipo de resultado, conforme a imagem acima, ou por teste, conforme a imagem abaixo:

TestInsight by Fixture
TestInsight by Fixture

Na barra superior, o test Insight possui as opções de:

  • Limpar os resultados
  • Rodar todos os testes
  • Rodar apenas os testes selecionados
  • Rodas apenas os testes posteriores ao cursor no editor de texto do Delphi
  • Terminar os testes em execução
  • Mostrar ou ocultar os testes que geraram avisos
  • Mostrar ou ocultar os testes que obtiveram sucesso
  • Mostrar ou ocultar os testes que não foram executados
  • Mostrar uma barra de progresso das execuções dos testes
  • Filtrar os resultados dos testes
  • Rodar os testes continuamente quando detectar uma ociosidade
  • Rodar os testes quando o projeto for salvo.

Interessante ressaltar as duas últimas funcionalidades. Como visto na parte teórica sobre os testes,  rodar os testes sempre que possível é altamente recomendável para detectar qualquer problema no código, e o CodeInsight nos dá a opção de fazer isso sempre que salvamos o projeto e sempre que ele detecta uma ociosidade. Isso é ótimo.

O desenvolvedor Delphi possui nas mãos ótimas ferramentas para garantir a qualidade de seu trabalho. Qualidade é coisa séria, e as empresas e profissionais pecam em não dar a devida atenção à isso.

Pronto para utilizar os testes unitários em seus projeto?

Enumeradores (Enumerators) no Delphi

Geralmente o pessoal de Delphi conhece apenas o loop “for básico“, aquele “old school” :

 

outros até conhecem o loop “for-in“, que é utilizado para iterar elementos de um conjunto:

Com strings:

 

Com arrays:

 

Ele funciona também com outros tipos  (sets, por exemplo). Mas o que nem todo mundo sabe, é que é possível criar seus próprios enumeradores.

 

Enumeradores (Enumerator)

Você pode criar seu próprio enumerador para realizar iterações utilizando o loop “for-in”. Vamos imaginar uma classe de pedidos que armazene informações de seus itens. Assim, gostaríamos de fazer uma iteração entre os itens do pedido, utilizando o loop “for-in”. A primeira coisa a ser feita é criar:

Basicamente, criamos uma classe para simbolizar os itens do pedido. Ela possui apenas três informações:

  • CodProduto – O código do produto.
  • DescProduto – A descrição do produto.
  • Valor – Valor do produto.

Agora precisamos da classe do pedido:

Ela está bem simples, mas faremos algumas alterações daqui a pouco.

Precisamos criar uma classe enumeradora. Ela será responsável por armazenar toda a lógica de iteração do itens do pedido, tirando essa responsabilidade da classe de pedidos.

Uma coisa a saber sobre as classes enumeradoras, é que elas precisam obrigatoriamente possuir 2 métodos:

  • Uma função pública chamada MoveNext.
  • Uma propriedade read-only chamada Current.

A função MoveNext será chamada no loop para que o próximo elemento seja apresentado.

A propriedade será chamada para retornar o elemento atual da iteração.

Uma coisa a se observar aqui é que MoveNext sempre será chamado antes de Current, por isso, tome cuidado para que seu código trate esse comportamento.

Abaixo encontra-se a definição da classe enumeradora.

Agora que já temos nosso enumerador, vamos voltar à classe de pedidos e adicionar o último método para que tudo isso funcione.

  • Uma função pública chamada GetEnumerator.

GetEnumerator será o responsável por dizer ao compilador qual a classe responsavel pela iteração dos itens do pedido.