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.

 

Colocando tudo para funcionar

Estabelecido as declarações, vamos para a implementação, começando com a classe enumeradora.

A classe enumeradora possui um contador interno chamdo FIndex, que zeramos para começarmos a iteração, no momento da criação.

FLista é a lista de pedidos que serão iterados.

GetCurrent será a função utilizada para o retorno do item do pedido atual da iteração.

MoveNext é a função responsável pela iteração dos itens do pedido, ou seja, aquela que movimenta o indice para “getCurrent” possa retornar novos itens do pedido.

Nada mais a se fazer com a classe enumeradora. Sua responsabilidade já foi satisfeita, ou seja, a iteração dos itens dentro da lista.

Voltando à classe de pedidos, devemos implementar a função GetEnumerator, para que o compilador saiba qual será nossa classe enumeradora:

Veja que precisei criar os itens manualmente aqui, como forma elucidativa, mas provavelmente você teria uma outra classe para o retorno dos itens ou mesmo uma busca desses itens no banco de dados. O mais importante aqui é a instrução:

Result := TMeuEnumerador.Create(Lista);

Ela retorna a criação da nossa classe enumeradora.

Todo o mecanismo está criado. Podemos agora simplesmente fazer uso de nossa nova estrutura:

Veja como o loop ficou muito mais simples, mais “clean”. Essa é a vantagem desse tipo de estrutura, seu charme.

Pronto! Simples, eficiente e elegante. Execute e veja os resultados. Abaixo encontra-se o fonte completo do exemplo:

Pronto para utilizar enumeradores em seus projeto?

Reraising no Delphi

Quando utilizamos “try except” para tratar nossas exceções, encontramo-nos algumas vezes em uma situação onde teremos um tratamento interno da exceção, e depois um tratamento externo, seja da mesmo exceção, seja um tratamento para qualquer exceção que o sistema apresente.

Vamos imaginar, por exemplo, que precisamos tratar uma exceção internamente, mas gostaríamos de gerar o log de qualquer exceção que ocorra na aplicação. Se tratarmos as exceções internas, o tratamento da exceção externa que gera o log jamais será executado. Poderíamos duplicar a exceção e gerar uma nova exceção depois de tratarmos a primeira, mas o Delphi possui um recurso próprio para isso, chamado reraising. Ao invés de criar uma nova exceção, o Delphi irá rechamar a última exceção ocorrida, melhorando inclusive a performance, porque ele não cria novamente a exceção, apenas utiliza aquela que já foi gerada:

O método GerarLog é meramente figurativo, para simbolizar que um log estará sendo gerado. O importante no código àcima é a chamada de “raise” sem nenhum parâmetro adicional. Quando isso acontece, o compilador entende que deve ser chamada a última exceção gerada anteriormente.

Execute o código acima e depois execute-o sem a chamada de raise, e perceba a diferença. Utilizando o “reraising“, você possui um poder maior no controle do fluxo da sua aplicação.

Um pouco sobre o TValue

Desde os primórdios do Delphi, existe um tipo de dados conhecido como “Variant” que é utilizado para armazenar uma grande variedade de tipos (char, string, integer, date, ponteiro, etc, etc, etc) e foi sempre utilizado para armazenar esses valores em situações onde o tipo do conteúdo poderia variar.

Com o Delphi 2010, e as novas alterações da RTTI, foi introduzido um novo tipo de dados, o TValue, que trabalha semelhante ao tipo variant, embora tenha algumas características diferentes. Uma delas é que, diferente do tipo variant, não é possível alterar o tipo de dados do TValue depois que ele foi informado pela primeira vez, ou seja, se ele foi definido como inteiro, será inteiro até o final. O objetivo do tipo não é sofrer alterações dos tipos dos valores, mas simplesmente receber um valor qualquer e armazenar esse valor até o momento de ser utilizado novamente.

Segue a declaração do TValue (Documentação oficial: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TValue):

Veja que ele  possui uma forma de checar os valores como difetentes tipos de dados, inclusive como interfaces.

Embora ele não possa se tornar um outro tipo de dado depois de definido na primeira utilização, ele é capaz de realizar casts com o seu conteúdo:

É isso. Lembre-se que ele veio junto com as novas funcionalidades da RTTI, por isso, pode ser muito mais explorado ainda. Experimente!

Predicado

Conforme uma das definições do dicionário Michaelis (veja aqui), predicado é:

“Numa proposição ou num juízo, atributo de um sujeito, que pode ser afirmado ou negado”

e é exatamente isso: uma definição de falso ou verdadeiro.

E por que isso é importante? Porque isso deixa o código muito mais elegante (sobre isso, veja aqui).

O Delphi possui o próprio tipo TPredicate (Documentação oficial):

TPredicate é a declaração de uma referência de uma função genérica que retorna, obrigatoriamente, um valor booleano.

Você pode utilizar um predicado para qualquer coisa que lhe sirva, mas provavelmente ele será utilizado como uma forma de filtrar resultados. Muitos os substituem no lugar de “ifs”. Veja.

Imagine que seu cliente precise que você imprima na tela uma lista de produtos. Ele lhe passa que todos os produtos do fornecedor “A” precisam ser impressos. Você, para atendê-lo, faz da seguinte maneira:

Define o produto. Aqui, por razões meramente educativas, crei uma classe com algumas informações sobre o produto, para poder exemplificar o restante do exemplo. Não vamos nos ater muito à isso.

Depois de criar a classe de produto, você cria a lógica de exibição dos produtos:

Perfeito. Você consegue atender seu cliente. Porém, como o cliente é imprevisível, você descobre que pouco tempo antes do lançamento do software você precisa adicionar mais uma coisa. O cliente precisa que os produtos com valor menor que R$ 100,00 também saiam. Você pensa: sem problemas. Basta adicionar essa verificação e pronto:

Resolvido. Agora é só lançar o produto. Só que…o cliente precisa de mais uma alteração. Agora ele quer que saiam também todos os produtos em promoção. Nesse ponto, você percebe duas coisas. A primeira, é que existe um padrão, que é a impressão de produtos, conforme alguma espécie de classificação. A segunda, é que você terá que ficar alterando o software constantemente. Você resolve então refatorar seu código e deixá-lo um pouco mais flexível. Afinal de contas, pode ser que o cliente precise que sejam impressos produtos de determinados locais também.

O que fizemos aqui foi isolar a lógica de impressão do produto da condição de ele ser impresso. Assim, quando as condições de impressão forem sendo adicionadas, teremos a certeza de que não estaremos alterando o mecanismo de impressão, além de facilitar a leitura e novas implementações de novas condições de impressão.

É claro que o código acima poderia ser melhorado, mas ele foi feito para que fosse possível identificar, de forma simples, os benefícios de usar predicados.

Essa é a forma geralmente indicada pelos amantes da orientação à objetos, que inclusive optam por abolir os “IFs” em certas situações, embora existam também outras correntes de pensadores (para saber mais a respeito, click aqui!).

Uma das tecnologias que mais utilizam predicados é o LINQ:

O código acima está em C#. Perceba a a utilização do predicado em:

E se você pensa que LINQ não existe em Delphi, você não soube olhar direito: https://www.devart.com/entitydac/

Utilize esse recurso para tornar o seu código o melhor possível.

A poesia do codificador

Quando comecei com programação profissionalmente, a única coisa que  eu queria era que o sistema funcionasse, não importasse como. O tempo era gasto com esse único objetivo, porque eu ainda estava em uma época de confirmação das minhas capacidades como desenvolvedor. Era uma época de provar para mim mesmo o que eu podia ou não podia fazer.

Hoje, olhando para os novos programadores que sou responsável, vejo muito disso. Existe uma insegurança em quem começa a desenvolver e tem a responsabilidade de entregar um produto. Essa dúvida está alicerçada na crença de que não possuem conhecimento suficiente para fazerem o que estão fazendo. Questionamentos simplistas como “melhor adicionar isso na esquerda ou na direita” surgem porque possuem a necessidade de criar os limites mentais para terem aprovação daquilo que estão fazendo, e assim medirem seu próprio desempenho, e construírem uma base sólida para sua formação. É o período da infância em todo novo aprendizado, no eterno ciclo de evolução.

Mas o conhecimento é conseguido pouco-a-pouco, a confiança instaurada em sua personalidade, e eis que o amadurecimento surge suavemente como o nascer do sol em uma noite escura, acalentando as mente para as novas possibilidades. As dificuldades de outrora tampouco são lembradas, porque o profissional já atingiu o patamar necessário para sua estabilização. Agora não existe mais insegurança. Existe apenas a continuação do caminho do aprendizado.

Esse é um ponto muito interessante. Nesse momento, o desenvolvedor já não se preocupa se vai conseguir ou não. Ele sabe que vai, porque sua fé é baseada em experiências passadas. A questão é apenas “como” ele vai fazer, e nesse momento algo muito legal acontece: ele passa a ser crítico. Passa a pensar em tudo o que faz, com olhar analítico, para fazer tudo da melhor forma.

O desenvolvimento de sistema já não é mais preocupação. Torna-se um passa-tempo, um jogo mental. O desafio é superar a sí mesmo, tornando tudo mais simples, flexível e performático, e o desenvolvedor para de escrever programas e passa a criar uma obra, como um pintor em uma tela. É possível ver beleza no código-fonte, e quem desenvolve a muito tempo sabe disso. Os recursos bem empregados, o algorítmo otimizado, as abstrações de forma coerentes, a teoria da orientação à objetos (para aquele que não concordam com essa afirmação, veja isso) tornando-se viva na implementação, tudo isso contribui para um código bonito.

Espero muito que você tenha essa experiência, de desenvolver sistemas de forma otimizada. É muito gratificante sair da “obrigação” do desenvolvimento funcionar e atingir a esfera da criatividade.

 

 

Utilizando Exit para retorno da função

Tradicionalmente o Delphi possui a possibilidade de retornar o valor da função através do próprio nome da função, ou através da variável “Result”.

Porém, umas das novidades que saiu no Delphi 2009, mas que poucas pessoas hoje conhecem (ou pelo menos comentam), é a capacidade de retornar o valor de uma função através do comando “Exit”.

Isso é útil quando você precisa, depois de alimentar o retorno da função, chamar Exit para sair do escopo atual:

O exemplo abaixo (apenas educativo!):

Pode ser simplificado assim:

Até a próxima!

 

Paradigma da Programação Orientada à Objetos (POO)

Quando comecei com programação, lá no anos 90, o paradigma da orientação à objetos já era algo muito discutido. Ele foi criado na década de 60 (para saber um pouco da história, acesse http://www.webgoal.com.br/origem-da-orientacao-a-objetos) , mas vejo que até hoje as pessoas tem dúvidas, se questionam a respeito e algumas vezes vão contra ele. Existe a legião de pessoas que idolatram a orientação à objetos, e vejo recentemente uma grande tendência contra ela, que vem crescendo à cada dia. Não sei o que o futuro nos aguarda, mas pode ser que venhamos à condená-la. Assim são as coisas com a evolução. O que posso dizer, humildemente, é que eu a adoro.

A orientação à objetos tem uma característica que considero fantástica: a forma que ela consegue organizar as coisas.

Durante anos estudei programação e análise de sistemas e, no início, mas durante um período que durou alguns anos, me questionava muito se eu realmente entendia sobre orientação à objetos. Entendia de classes, de herança, de polimorfismo, de sobrecarga de métodos e todas as outras coisas relacionadas, mas não era capaz de responder à simples questão: porque as pessoas precisam disso se conseguem desenvolver sistemas sem seguir a orientação à objetos? Não pode ser simplesmente pela reutilização do código, porque uma função também pode ser reutilizada. Porque criar uma estrutura se posso ir diretamente ao ponto e resolver o problema? Felizmente, a resposta para essa pergunta me apareceu futuramente.

Somente quando comecei a trabalhar com grandes projetos, o que aconteceu depois que entrei na faculdade (sim, comecei a desenvolver muito antes disso) é que fui entender a importância da facilidade de manutenção e de comunicação com outras pessoas da equipe, referente ao código-fonte.

A grande maravilha da orientação à objetos para mim não é relacionado aos seus pilares: abstração, encapsulamento, polimorfismo e herança (não despreso esses recursos!). É a forma como ela organiza as informações que é mágico. Você pode ter milhões de arquivos no seu computador, e todos vão funcionar e você eventualmente vai encontrar todos sempre que possível, mas mesmo assim, você os organiza em pastas, não é? Por que? Porque dessa forma, fica muita mais fácil trabalhar com eles. É a mesma coisa! Ah, mas eu poderia utilizar namespaces para organizar meu código… sim, você poderia! Eu utilizo os dois (classes e namespaces), e sou feliz assim! Quer utilizar namespaces apenas, e classificar suas funções? Vai em frente! Porque não importa o que você faça, o importante é que dê certo! Não existe certo ou errado em informática, desde que  as pessoas consigam aquilo que elas busquem.

Muitos argumentam que OO não é performático. Comparado a que? Procedural? Sim, pode ser verdade. E daí? Ah, então não devemos utilizar OO para nenhum projeto? Responda-me uma coisa: quando as pessoas vão para o fundo do mar, elas vão de submarino ou de avião? Mas o avião não é mais veloz (é, forcei um pouco no exemplo!)?  O que quero dizer é que não existe uma tecnologia que resolve todos os problemas. Cada coisa tem seu papel. Se vou realizar um processamento em lote, provavelmente farei sem utilizar OO, para que tenha o máximo de performance. Provavelmente farei tudo da forma mais direta possível, porque o problema exige isso. Mas, se ao mesmo tempo, tiver um deadline muito curto e já exista uma classe preparada para o serviço, só que utiliza ORM, etc, etc, etc, talvez utilize a OO simplesmente porque tenho que entregar o projeto, e sem isso, não conseguiria atingir o prazo. É melhor meia vitória do que nenhuma. Por outro lado, prefiro utilizar OO sempre que possível, porque isso me dá um egrenciamento do código que eu preciso nas manutenções, que é essencial para o desenvolvedor. Outras formas de desenvolver sistemas podem gerar uma melhor performance do programa, mas e quanto à performance da equipe? Isso também importa.

As dificuldades em desenvolvimento de sistemas são relacionadas à prazos, regras de negócios, relacionamento com os clientes, processos internos dentro da fábrica de software. A tecnologia é só a forma de chegar lá, é o veículo para atingir os objetivos. É como ter que ir para outra cidade, podendo escolher ir de carro, ônibus ou avião. Mas o problema real ainda é ir para outra cidade. O que geralmente vejo são as pessoas discutindo que é melhor ir de carro, outros de avião. Poucos discutem sobre: por que preciso ir para outra cidade? O que vou fazer lá? Essas são as questões, porque sem isso, nada adianta qual veículo você vai usar.

Costumo falar que informática é igual time de futebol. Fulano torce para aquela tecnologia, siclano para aquela linguagem de programação específica. Isso só demonstra imaturidade profissional. O bom profissional sabe que essas coisas são passageiras e que o importante é resolver o problema, seja como for. É a satisfação do cliente que importa, e ele não está preocupado com isso. Os envolvidos nos projetos geralmente não se preocupam com a tecnologia. Os pedidos estão caindo? Minha empresa está faturando? Como estão as vendas? Está formando filas nos caixas? Nada disso tem a ver com tecnologia. Isso tem a ver com pessoas. Foco nas pessoas. A tecnologia é o veículo para a resolução dos problemas. Se o faturamento está ótimo, importa se foi feito utilizando POO ou da forma procedural? Claro que não! Importa que funciona. E pode ter certeza que dá para se fazer bem feito das duas maneiras, ou de qualquer outra.

Outra vantagem de se utilizar OO que me ajuda muito, é algo que obtenho também dos padrões de projeto. Facilidade de comunicação com a equipe. Quando as pessoas estão integradas com o código, é fácil simplesmente dizer: utilize esse ou aquele objeto para resolver isso. Faça tal sobrecarga, etc, etc. É simplesmente fácil de se comunicar, porque os objetos são abstrações das estruturas e dos comportamentos, e quando falamos de comportamento, estamos utilizando uma linguagem comum a todos.

Se sua equipe está acostumado com a forma procedural, não mude! Não tire o know-how de como ter sucesso (a não ser que você não esteja tendo)! Se sua equipe está acostumada à OO, não mude! Não tire o know-how dela. Mudar os processos internos é traumatico e retira o conhecimento daqueles que já o detém. Cade equipe precisa encontrar a própria forma de trabalhar. Eu encontrei a minha. Espero que você também encontre a sua.

Mapped File (ou Arquivos Mapeados na Memória)

Foi por acaso que descobri como trabalhar com arquivos mapeados na mémoria (mapped files). Estava lidando com um projeto onde gostaria de trocar informações com os registradores de desenpenho do monitor de performance do Windows (Perfmon) e o exemplo que encontrei fazia o uso desse mecanismo para trocar informações entre a DLL que seria utilizada pelo Perfmon e o software que seria monitorado.

Para aqueles não acostumados com o termo, arquivos mapeados na memória (ou geralmente conhecidos como mapped files) são respresentações de arquivos que existem diretamente na memória, e podem ou não existir no disco, ou seja, você consegue escrever e ler informações diretamente na memória como se o estivesse fazendo em um arquivo no disco rígido.

Isso geralmente é utilizado para integrar dois processos diferentes, visto que a memória passa a ser um ponto de contato entre os dois processos. Foi exatamente essa a razão para utilizar no exemplo do Perfmon. Contudo, existem várias outras razões que justifiquem esse uso, seja por performance (afinal, acesso ao disco nunca foi o melhor amigo da performance) quanto, por exemplo, evitar problemas com direitos de acesso em disco, nos casos onde a informações será gerada de forma temporária. Cada caso ditará a necessidade ou não desse recurso.

A primeira coisa a saber, é que todos os processos que utilizarão esse “arquivo” na memória devem referencia-lo pelo mesmo nome. Isso é que irá determinar que estão lidando com o mesmo espaço de memória.

Para nos utilizarmos disso, a primeira coisa a ser feita é conhecer as instruções responsáveis pela manipulação da memória. Saiba que o Delphi utiliza-se para isso a própria API do Windows, então quem quiser qualquer referência precisa procurar no próprio site da Microsoft (http://msdn.microsoft.com/pt-br/).

CreateFileMapping (https://msdn.microsoft.com/en-us/library/windows/desktop/aa366537(v=vs.85).aspx)


A função CreateFileMapping é a responsável por criar ou abrir (caso um arquivo físico seja informado) o arquivo na memória – afinal, é exatamente isso que queremos fazer, não é?.

Ela possui 6 parâmetros de entrada e uma saída:

  • hFile – Handle do arquivo, se ele já existir. 0 se você quer criar um novo.
  • lpAttributes – Estamos falando de arquivos, certo? Os atributos aqui, são os atributos de arquivos (ReadOnly, Hidden, etc. https://msdn.microsoft.com/en-us/library/windows/desktop/aa379560(v=vs.85).aspx)
  • flProtect – Forma de proteção da memória que iremos utilizar. Fique atento que nem todas as formas de proteção estão disponíveis para serem utilizadas com a função CreateFileMapping. Você pode encontrar quais poderão ser utilizadas e quais não, diretamente em https://msdn.microsoft.com/pt-br/library/windows/desktop/aa366786(v=vs.85).aspx
  • dwMaximumSizeHigh e dwMaximumSizeLow – Relacionam-se aos tamanhos máximos do arquivo dentro da memória.
  • lpName – Nome de referência do arquivo que utilizaremos para identificá-lo.
  • Result – O retorno da função será o handle do arquivo na memória.

É possível ainda, quando sabemos que o arquivo já existe, de utilizar a função OpenFileMapping (https://msdn.microsoft.com/pt-br/library/windows/desktop/aa366791(v=vs.85).aspx).

É importante adicionar que a função CreateFileMapping, caso tenha algum problema, grava as informações de erros que poderão ser utilizadas posteriormente através da função do RaiseLastOSError (http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.SysUtils.RaiseLastOSError), que utilizaremos para identificar se a criação ocorreu normalmente ou não.

Obs: Após a criação do arquivo, é necessário chamar a função CloseHandle, que irá liberar o handle.


MapViewOfFile (https://msdn.microsoft.com/pt-br/library/windows/desktop/aa366761(v=vs.85).aspx)

Uma vez que já criamos o arquivo na memória, devemos fazer a chamada da MapViewOfFile para podermos manipular o conteúdo desse arquivo. Existe uma diferença importante quando se usa a MapViewOfFile. Quando chamamos a CreateFileMapping, estamos “alocando” aquela quantidade de memória, mas não necessariamente faremos o uso daquela mesma quantidade quando formos utilizá-la. Assim, a função MapViewOfFile determina a “porção” daquela memória alocada que iremos utilizar, ou seja, qual será o espaço dentro daquela memória, que iremos utilizar (mais em https://stackoverflow.com/questions/32299112/mapviewoffile-offset-how-to-use-it).

Ela possui 5 parâmetros de entrada:

  • hFileMappingObject – O handle do arquivo (aquele mesmo retornado pela CreateFileMapping).
  • dwDesiredAccess – O tipo de acesso ao arquivo mapeado (verificar o link da função para identificar os tipos possíveis)
  • dwFileOffsetHighdwFileOffsetLow – É a janela do offset, que determina o ponto de início do espaço de utilização da memória.
  • dwNumberOfBytesToMap – O número de bytes a ser mapeados.
  • Result – O retorno da função é o endereço da memória de início do mapeamento.

Mais em: https://stackoverflow.com/questions/9889557/mapping-large-files-using-mapviewoffile

Aqui também precisaremos fazer o uso da função RaiseLastOSError para identificarmos se tudo ocorreu bem.

Obs: Após a criação do mapeamento, é necessário chamar a função UnmapViewOfFile para liberar o mapeamento.

Basicamente isso é o necessário. Vamos ver um exemplo:

Gravação da informação na memória

Esse é o programa que irá gravar as informações na memória. É um formulário simples com um botão na tela, que grava as informações na memória conforme ele é clicado. Preste atenção apenas que o arquivo é criado no FormCreate e destruído no FormDestroy, porque, de outra forma, ao clicar no botão o arquivo seria criado e destruído muito rápido, não dando tempo para a aplicação que irá ler o conteúdo obter o valor correto. Veja que ele faz uso das funcões àcima descritas. Ele tende a ser autoexplicativo.


Leitura da informação da memória

Perceba que ele faz uso da função OpenFileMapping para a leitura. Basicamente, acessa o arquivo mapeado na memória e pega o seu conteúdo. Muito simples. 🙂

Download do código fonte do exemplo: Aqui

TryStrToInt (Conversão de tipos de dados)

A SysUtils é algo que sempre me surpreende. Confesso que não conhecia a função TryStrToInt (iremos falar mais sobre ela abaixo) mesmo durante longos anos de desenvolvimento. Talvez isso tenha sido uma das minhas maiores falhas, mas aprender sempre é algo que me satisfaz muito. Lendo um livro do Nick Hodges, ele comentou sobre formas erradas de validação da conversão de números inteiros, para exemplificar a maneira como as pessoas utilização exceções (try…except) de formas erradas, e lá estava eu, nú sobre a análise perspicaz de um dos maiores nomes do mundo delphi (ele é diretor de gerenciamento de produtos da Embarcadero Technologies). Depois de me recompor, fui aprender mais sobre o assunto.

A função TryStrToInt é muito simples. O seu retorno é um boolean, que indica se a conversão é possível. Ela recebe como primeiro parâmetro a string que será verificada. Como segundo parâmetro, você deve passar uma variável (é um parâmetro de saída/out) que armazenará o número inteiro possível da conversão (entenderemos melhor isso).

Abaixo encontra-se um pequeno exemplo:

Existem duas strings que serão convertidas: NumeroValido e NumeroInvalido.

Número válido possui uma string válida para conversão para inteiros: 12345

Número inválildo possui uma string inválida para a conversão para inteiros: 98765A321 (perceba a letra A no meio da string).

Quando utilizo a string válida, a função TryStrToInt retorna True, e False para a string inválida.

O segundo parâmetro, que chamei de Retorno (porque trata-se de um parâmetro de saída/out), para a string válida, é a própria string convertida, ou seja, é o número inteiro 12345. Na segunda tentativa, quando utilizamos a string inválida, a variável Retorno possui o valor 98765, ou seja, o número convertido até onde foi possível, que foi até o momento que não se encontrou um número inválido, descartando-se o restante, à partir daí.

Copie o código àcima e tente em seu ambiente, para que tudo fique mais claro.

Olhando internamente a codificação do TryStrToInt, é possível perceber que ele faz uso de uma antiga função chamada Val (engraçado, essa função estava perdida em algum lugar da minha mente, pois me lembro dela, embora nunca a tivesse utilizado) que serve para conversão de string em números.

Vale lembrar que junto com a TryStrToInt, existem várias outras funções para outros diferentes tipos de dados (TryStrToBool, TryStrToFloat, TryStrToCurr, etc) que poderão ser utilizados, dependendo da situação.

Try…except nunca mais (calma, apenas para conversão safety-mode de strings!)