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?

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?