Testes Unitários Automatizados no Delphi

Nesse artigo abordamos os testes unitários, suas características através do Framework DUnit e sua aplicação prática.

Fique por dentro

Um dos maiores desafios no desenvolvimento de software é garantir a agilidade. Porém a palavra agilidade quando interpretada erroneamente pode causar consequências desastrosas. Por exemplo, em um cenário pontual, a solicitação de um novo recurso a um projeto, sem muito estudo, pode ser implementada e colocada em produção em questão de poucas horas. Mas, o que tornou essa implementação tão rápida? Esse questionamento inicial conduz a vários outros, por exemplo, já existiam bibliotecas prontas para essa funcionalidade? O software já estava prevendo essa funcionalidade antes mesmo de entrar em produção? Essa nova implementação não faz referência a outras rotinas, minimizando impactos? Ou a rotina não foi escrita da melhor maneira possível, não foi testada adequadamente e não foi fatorada novamente? Na maioria das vezes a pergunta que recebe uma resposta é a última, de forma negativa. Não testar ou melhorar um código pode produzir com o tempo um bug nesse novo recurso. Mas isso é só o início do problema.

Outro desenvolvedor poderá realizar a correção, e com isso, como garantir que o recurso funcionará da maneira como foi planejado? Quanto tempo seria necessário para corrigir e testar esse recurso? Como garantir que o bug não irá voltar em produção? E se os requisitos dessa funcionalidade mudarem, será fácil a adaptação? Essas são questões pertinentes que são levantadas quando não existem testes suficientes, nem processos de apoio durante o desenvolvimento. Nesse artigo abordamos os testes unitários, suas características através do framework DUnit e sua aplicação prática.

Em que situação o tema é útil Códigos que são constantemente alterados necessitam de testes constantes. Rotinas importantes para o negócio desenvolvido também requerem testes constantes. Para acelerar o resultado desses testes são aplicadas automações, no caso, testes unitários. Dessa forma tem-se uma resposta rápida que diz se determinado código importante ainda funciona depois de uma alteração solicitada.

XP – Programação Extrema

A Programação Extrema (ou somente XP) é uma metodologia ágil que se fundamenta em cinco valores básicos: Comunicação, Simplicidade, Feedback, Coragem e Respeito. Dentro desses valores vamos destacar três. A qualidade de um código fonte está diretamente ligada à sua simplicidade. Quanto mais complexo e mais poluído for o código fonte, mais trabalhosa é a sua manutenção e evolução. Um código fonte limpo é a melhor ferramenta para evolução de um software. Mas, como fazer um código fonte limpo?

Existem inúmeras maneiras que devem ser combinadas, como por exemplo, quebrar métodos grandes em métodos menores, quebrar classes grandes em classes menores, uso de padrões de projeto, boa nomenclatura de classes, métodos e variáveis, eliminação de duplicidade de código, etc. Manter um código fonte limpo não é uma tarefa fácil, exige um processo de fatoração (Nota 1) constante.

Nota 1: Martin Fowler define a fatoração como uma técnica disciplinada para a reestruturação de um corpo existente de código, alterando sua estrutura interna sem mudar seu comportamento externo. Cada fatoração é uma pequena transformação aplicada ao código-fonte, visando melhorar sua legibilidade, performance, organização, adequação a um padrão etc.

Alterar um mesmo código constantemente pode conduzir a um sério problema, a coragem. É realmente seguro reestruturar completamente um método de 40 linhas, quebrando em métodos menores e renomeando variáveis de escopo interno? E como ter certeza que está funcionando após a fatoração? É claro, a resposta óbvia é realizando uma bateria de testes. Mas aí vem a pergunta, como esses testes seriam executados? Manualmente? Quanto tempo esse processo demoraria? É garantido que serão testadas todas as possibilidades básicas do método? O feedback desses testes seria realmente confiável? Chegamos à conclusão que para escrever um código fonte limpo e por consequência simples, é preciso estar constantemente executando fatorações, mas como criar coragem para executar a fatoração e como ter um resultado rápido dessa fatoração.

Testes unitários

Existem inúmeros tipos de testes, por exemplo, teste operacional, teste de volume, teste de stress, teste de configuração, teste de aceitação do usuário, etc. Grande parte desses testes foge ao controle do desenvolvedor, porém existe um tipo de teste que é de responsabilidade total do desenvolvedor, o teste unitário.

O teste unitário consiste na fase em que se testa pequenas unidades de software, por exemplo, os métodos de uma classe. Os testes unitários são testes criados e implementados pelo desenvolvedor, o que significa que esses serão rotinas automatizadas que não necessariamente necessitam de uma intervenção humana. O conceito inicialmente pode parecer complexo, mas imagine um método que calcule a soma de dois números, como mostra a Listagem 1.

function Somar(n1, n2: real):real; var x : real; y : real; Begin x := n1; y := n2; Result := x + y; End;
Listagem 1. Método que realiza uma soma simples

O teste unitário desse método seria algo extremamente simples, como mostra o código a seguir:

Assert(Somar(2,2) = 4);

O método Assert testa se uma condição seja verdadeira, caso não seja, sobe uma exceção indicando onde o assert não foi atendido. Com isso já estamos, de forma automatizada, testando nosso método. Após qualquer alteração no método basta rodar a rotina que contém o Assert novamente e verificar se o método continua consistente. Feito nosso teste unitário, podemos sem medo fatorar esse método para torná-lo mais simples e mais claro, como mostra a Listagem 2.

function Somar(APrimeiroNumero, ASegundoNumero : real):real; begin Result := APrimeiroNumero + ASegundoNumero; end;
Listagem 2. Método Somar refatorado

Por mais ingênuo e simplista que seja esse exemplo, fica claro que com o teste unitário feito, o medo de alterar o código passou a ser muito menor. Dentro deste exemplo, imaginando que tivéssemos toda aplicação (ou boa parte dela) coberta por testes unitários não seria um problema o fato de o método Somar ser utilizado em diversas rotinas do software. Mas qual é o problema com o exemplo acima? Muito provável que após escrever o método Somar e executar seu teste (que provavelmente estava escrito em um formulário qualquer), esse código de teste é descartado, o que obrigaria escrevê-lo novamente caso o método sofra alguma alteração. Com certeza essa não é a melhor maneira de se fazer testes unitários em um software. Para isso existem frameworks especializados em testes unitários, como é o caso do xUnit.

Framework xUnit

Criado por Kent Beck, o xUnit inicialmente idealizado para SmallTalk (com o nome de SUnit) e popularizado posteriormente com sua versão para Java (com o nome de JUnit), consiste em um framework para geração e execução de testes unitários automatizados, que se consagrou como sendo o principal padrão de projeto de testes. O xUnit basicamente automatiza a rotina de execução de testes de uma forma simples e intuitiva tornando os testes vivos e evolutivos no mesmo ritmo do software.

Arquitetura

O framework xUnit possui em sua arquitetura os seguintes componentes básicos.

Funcionamento

Através do conceito de reflexão (conhecido como RTTI no Delphi) o framework xUnit carrega todos os casos de teste registrados para execução, os executa (com ou sem interação do usuário) e provê feedback dos resultado obtidos na execução dos testes (seja visual ou através de arquivos).

Em sua interface visual, esses feedbacks consistem basicamente no padrão barra vermelha/barra verde. Mas, o que isso significa? É simples e intuitivo, quando um teste falhar este fica em vermelho, quando ocorre sucesso o teste fica verde. Nos testes que falharam é apresentada uma mensagem do tipo “o resultado esperado era mas temos ”, entraremos nesses detalhes conhecendo o framework DUnit.

DUnit – A versão do xUnit para Delphi

Com o crescimento e popularização do xUnit, este framework passou a ganhar versões para diferentes plataformas de desenvolvimento, como é o caso do framework DUnit para Delphi. Idealizado por Juancarlo Añez, o DUnit é um projeto open source que consiste praticamente em uma cópia fiel do JUnit, adaptada para Pascal. Nas versões mais novas do Delphi, o DUnit já vem como complemento.

Demonstração

Para demonstrar o uso da DUnit faremos uma classe de exemplo e um projeto de testes para testar essa classe. O intuito da nossa classe de exemplo será prover funções para lidar com strings.

Não faremos nada que o Delphi já não faça para nós. Criamos um projeto Win32 normal com o nome de ProjUtils e neste projeto criamos uma unit para a nossa classe (o nome da unit pode ser unt_str_utils), listada na Listagem 3.

unit unt_str_utils; interface type TStrUtils = class private FStr: String; public Constructor Create(const AStr : string); function Equals(const AStr : string):boolean; function EqualsIgnoreCase(const AStr : string):boolean; function ToUpper():string; function ToLower():string; property str : String read FStr write FStr; end;
Listagem 3. Declaração da classe base para demonstração dos testes unitários

A ideia é criar métodos simples e implementá-los de forma que possam ser melhorados, para que faça sentido a execução dos testes e a refatoração. A Listagem 4 apresenta a implementação dos métodos da classe TStrUtils.

implementation uses SysUtils; constructor TStrUtils.Create(const AStr: string); begin Self.FStr := AStr; end; function TStrUtils.Equals(const AStr: string): boolean; begin result := AStr = Self.FStr; end; function TStrUtils.EqualsIgnoreCase(const AStr: string): boolean; begin Result := UpperCase(AStr) = Self.ToUpper; end; function TStrUtils.ToLower: string; begin Result := LowerCase(Self.FStr); end; function TStrUtils.ToUpper: string; begin Result := UpperCase(Self.FStr); end; //explicar a classe
Listagem 4. Implementação da classe TStrUtils

Como é possível de ver, simplesmente encapsulamos chamadas às funções e procedimentos existentes no Delphi para tratamento de strings.

Para testar essa classe, o mais comum seria criar um formulário VCL com um botão e um label e sair chamando os métodos públicos da classe, mas provavelmente estaríamos testando um método de cada vez e com certeza, após uma pequena bateria de testes iniciais, esse formulário seria descartado. Isso não é prático e de longe não é ágil. Vejamos o que o DUnit tem a nos oferecer.

Criando os testes

Sem fechar o projeto ProjUtils, crie um novo projeto de testes em File > New > Other > Unit Test > Test Project. Isso faz com que seja acionando o Wizard de criação de um novo projeto de testes, como apresentado na Figura 1. Este Wizard é divido em dois passos, no primeiro são feitas algumas configurações básicas, dentre elas, destacam-se três:

No segundo passo determinamos se a execução do teste será através de uma aplicação console ou através de uma aplicação visual (GUI).

Figura 1. Wizard de criação de um projeto de testes

Com o projeto de testes criado, agora já é possível criar um caso de testes para nossa classe TStrUtils. Para isso, com o recém criado grupo de projeto (ProjUtils e ProjUtilsTests) aberto e com o projeto ProjUtils marcado como projeto ativo, vamos em File > New > Other > Unit Test > Test< Case.

Saiba mais Confira nossa Guia Completo de Delphi

Isso aciona o Wizard de criação de um novo caso de testes (Figura 2). Seguindo a simplicidade do primeiro, basta informar o Source File que iremos testar (no caso nossa unit unt_str_utils.pas) e escolher de quais classes e/ou métodos será criado um esqueleto de testes. Vale ressaltar aqui que esse assistente interpreta somente classes, sendo assim, records com conjunto de métodos não serão enxergados. No segundo passo desse assistente é informado o projeto de testes que esse caso de teste será adicionado e o nome da unit de testes. Por padrão, o nome da unit que será testada precedida da palavra Test, por exemplo, TestUnt_str_utils.pas.

Figura 2. Wizard de criação de um caso de testes

Finalizando os passos desse wizard, será criada a unit de caso de testes com a seguinte estrutura vista na Listagem 5.

unit TestUnt_str_utils; interface uses TestFramework, unt_str_utils; type TestTStrUtils = class(TTestCase) strict private FStrUtils: TStrUtils; public procedure SetUp; override; procedure TearDown; override; published procedure TestEquals; procedure TestEqualsIgnoreCase; procedure TestToUpper; procedure TestToLower; end; implementation procedure TestTStrUtils.SetUp; begin FStrUtils := TStrUtils.Create; end; procedure TestTStrUtils.TearDown; begin FStrUtils.Free; FStrUtils := nil; end; procedure TestTStrUtils.TestEquals; var ReturnValue: Boolean; AStr: string; begin ReturnValue := FStrUtils.Equals(AStr); end; procedure TestTStrUtils.TestEqualsIgnoreCase; var ReturnValue: Boolean; AStr: string; begin ReturnValue := FStrUtils.EqualsIgnoreCase(AStr); end; procedure TestTStrUtils.TestToUpper; var ReturnValue: string; begin ReturnValue := FStrUtils.ToUpper; end; procedure TestTStrUtils.TestToLower; var ReturnValue: string; begin ReturnValue := FStrUtils.ToLower; end; initialization RegisterTest(TestTStrUtils.Suite); end.
Listagem 5. Esqueleto de testes criado pelo wizard do DUnit

Implementando os testes

O caso de testes já está pronto, porém, obviamente ele não testa absolutamente nada, sendo assim, vamos entender e começar a dar vida a ele. Vamos começar por onde todos nossos testes vão começar, o método SetUp().

Como dito anteriormente, antes de cada teste o método SetUp() é executado, seguindo nosso exemplo, antes do teste TestTStrUtils.TestToUpper() ser executado, o método SetUp() será executado. Qual a vantagem disto? Vamos analisar o método SetUp(), criado pelo wizard da DUnit (Listagem 6).

procedure TestTStrUtils.SetUp; begin FStrUtils := TStrUtils.Create; end;
Listagem 6. Método SetUp

Foi criada uma instância da classe TStrUtils, porém, levando em consideração a teoria do framework xUnit de que todo teste deve ser independente e não deve influenciar o resultado de outros testes, faz sentido que para cada teste seja criada uma nova instância do objeto a ser testado. Sendo assim, de maneira transparente o framework DUnit, antes de executar qualquer método, executa o método SetUp().

Sem mencionar que estamos evitando duplicidade de código, não sendo necessário instanciar o objeto em cada método de teste. Caso seja necessário além da criação da instância de um objeto fazer outras parametrizações no objeto para que este seja útil, é no SetUp() que isso deve ser feito.

Para agilizar alguns testes (e também conseguir compilar nosso projeto de testes) vamos fazer uma pequena alteração no método SetUp() para que este fique aderente a nossa classe TStrUtils, como mostra a Listagem 7.

procedure TestTStrUtils.SetUp; begin FStrUtils := TStrUtils.Create("Testes DUnit"); end;
Listagem 7. Adaptação do método SetUp

Já que para cada teste, uma instância do objeto a ser testado é criada, nada mais justo que a cada execução de teste, esse objeto seja liberado da memória. Este é o intuito do método TearDown(), segundo Listagem 8.

procedure TestTStrUtils.TearDown; begin FStrUtils.Free; FStrUtils := nil; end;
Listagem 8. Método TearDown

Sendo assim, o fluxo da execução do caso de teste seria desta forma algo como é mostrado na Listagem 9.

SetUp(); TestToUpper(); TearDown(); SetUp(); TestToLower(); TearDown();
Listagem 9. Fluxo da execução dos métodos de teste

Entendendo esse conceito básico, podemos escrever nosso primeiro teste unitário. Começaremos testando o método TStrUtils.Equals(). Esse método basicamente vai comparar se a string armazenada no atributo do objeto TStrUtils é igual a string passada como parâmetro para este método, retornando True para sucesso e False para falha. O teste básico para esse método poderia ser como mostra a Listagem 10.

procedure TestTStrUtils.TestEquals; var ReturnValue: Boolean; begin ReturnValue := FStrUtils.Equals("Testes Dunit"); CheckTrue(ReturnValue); end;
Listagem 10. Teste unitário do método TStrUtils.Equals()

Antes de avançarmos neste teste, vamos entender a presença de um novo elemento neste cenário, o Check. Assim como o Asserts, o Check basicamente avalia se uma condição seja verdadeira ou falsa, com a diferença que o Check é próprio do framework DUnit, sendo assim, caso uma condição não seja satisfeita, ele marca o teste como insucesso e permite a execução dos próximos testes, diferente do Asserts que interrompem a execução do código fonte. Neste teste foi utilizado o CheckTrue, porém, existe uma infinidade de outros Checks, sendo que alguns deles iremos explorar durante este estudo.

Voltando ao nosso teste, nota-se que este irá falhar (a letra “u” do DUnit está minúscula no teste), isso é bom, na verdade, isso é ótimo. Uma boa prática defendida pelos entusiastas dos testes automatizados é que o primeiro teste deve sempre falhar, essa é uma forma de garantir que seu teste é realmente confiável. Ao executar este teste, teremos uma barra vermelha (Figura 3).

Figura 3. Executando o projeto de teste com uma falha

Para conseguirmos uma barra verde e nos certificarmos que nosso método Equals() funciona, vamos consertar nosso teste, como mostra a Listagem 11.

procedure TestTStrUtils.TestEquals; var ReturnValue: Boolean; begin ReturnValue := FStrUtils.Equals("Testes DUnit"); CheckTrue(ReturnValue); end;
Listagem 11. Alteração no teste do método TStrUtils.Equals()

Agora ao executarmos nossos testes teremos uma agradável e confortável barra verde, como mostra a Figura 4.

Figura 4. Teste funcionando

Note que estamos executando apenas o teste que estamos dando atenção no momento, isso porque, caso sejam executados os outros testes da maneira como estão, todos dariam sucesso, pois em nenhum deles testamos alguma condição (utilizando o Check). Existe uma forma de inverter esse cenário. Nesta aplicação GUI da DUnit existe a opção Options > Fail TestCase if no Checks executed (em tradução livre, falhar o caso de teste se não forem executados checks), com essa opção marcada, todos os testes que não tiverem algum Check serão interpretados como falha.

Com o método Equals() devidamente testado e funcionando, podemos partir para o teste do método EqualsIgnoreCase(). Esse método faz a mesma coisa que o método Equals(), com a diferença que ele ignora o fato de o texto estar maiúsculo ou minúsculo, seu teste pode ser visto na Listagem 12.

procedure TestTStrUtils.TestEqualsIgnoreCase; var ReturnValue: Boolean; begin ReturnValue := FStrUtils.EqualsIgnoreCase("testes dunit"); CheckTrue(ReturnValue); ReturnValue := FStrUtils.EqualsIgnoreCase("Uma string qualquer!"); CheckFalse(ReturnValue); end;
Listagem 12. Teste unitário do método TStrUtils.EqualsIgnoreCase

Desta vez estamos testando também se o método retorna False quando deveria retornar. Sem segredos ou surpresas, ao executar os testes, continuou com uma barra verde. Vale ressaltar aqui que, diferente de testes em formulários com botões, estamos executando todos os testes com apenas um clique e estamos obtendo feedback individualizado de cada teste.

Faremos um teste agora que nos fará repensar nessa classe (TStrUtils) como um todo. Vamos testar o método ToUpper(). Mas dessa vez, vamos incrementar um pouco o básico, como mostra a Listagem 13.

procedure TestTStrUtils.TestToUpper; var ReturnValue: string; begin ReturnValue := FStrUtils.ToUpper; CheckEquals("TESTES DUNIT",ReturnValue); FStrUtils.str := "este é um teste com acentuação"; ReturnValue := FStrUtils.ToUpper; CheckEquals("ESTE É UM TESTE COM ACENTUAÇÃO",ReturnValue,"Testes com acentuação"); end;
Listagem 13. Teste unitário do método TStrUtils.ToUpper

Antes de analisarmos este teste, vejamos o CheckEquals que é o Check mais comum do DUnit. Basicamente esse método (assim como a grande maioria de todos os checks) é composto dos seguintes argumentos:

Existem inúmeras sobrecargas desse método para serem utilizadas com diversos tipos de dados. Voltando ao nosso teste, aparentemente ele está ok, porém, ao executarmos, teremos uma triste barra vermelha (Figura 5).

Figura 5. Barra vermelha

Ao ler a descrição da falha já fica mais claro o motivo da barra vermelha. O método ToUpper() não está considerando caracteres com acentuação. Isso é perfeito! Não o fato do método ToUpper() não estar funcionando, mas sim, o fato de termos encontrados a falha em desenvolvimento e não em produção. A resolução deste problema é algo muito simples, basta utilizar a versão do comando UpperCase que trata de acentos, a AnsiUpperCase (Listagem 14).

function TStrUtils.ToUpper: string; begin Result := AnsiUpperCase(Self.FStr); end;
Listagem 14. Correção no método TStrUtils.ToUpper

Pronto, barra verde novamente, mas essa barra vermelha anterior evidenciou uma fragilidade, nosso método EqualsIgnoreCase() funciona com strings que contenham acentos? Simples, vamos escrever um teste para verificar este cenário. Existem duas formas de criar esse novo teste, uma é testar esse cenário no teste já escrito e a outra forma é criar um teste novo, faremos da segunda forma para demonstrar que é possível evoluir a classe de caso de teste, como mostra a Listagem 15.

procedure TestTStrUtils.TestEqualsIgnoreCaseComAcento; var ReturnValue : Boolean; begin FStrUtils.str := "este é um teste com acentuação"; ReturnValue := FStrUtils.EqualsIgnoreCase ("Este é um Teste Com ACENTUAÇÃO"); CheckTrue(ReturnValue,"Testes com acentuação"); end;
Listagem 15. Teste unitário para textos com acento

Ao executar esse novo teste, comprovamos a nossa suspeita, barra vermelha, mais uma vez, ótimo! Faremos agora a correção, como mostra a Listagem 16. Mais uma foi necessário utilizar o AnsiUpperCase.

EqualsIgnoreCase function TStrUtils.EqualsIgnoreCase(const AStr: string): boolean; begin Result := AnsiUpperCase(AStr) = Self.ToUpper; end;
Listagem 16. Correção do método

Testes executados e uma barra verde como resultado significa que podemos partir para o próximo teste, o do método TStrUtils.ToLower(). Já sabendo a fragilidade com relação a acentos, faremos os testes já esperando este cenário (Listagem 17).

procedure TestTStrUtils.TestToLower; var ReturnValue: string; begin ReturnValue := FStrUtils.ToLower; CheckEquals("testes dunit",ReturnValue); FStrUtils.str := "ESTE É UM TESTE COM ACENTUAÇÃO"; ReturnValue := FStrUtils.ToLower; CheckEquals("este é um teste com acentuação",ReturnValue,"Testes com acentuação"); end;
Listagem 17. Teste unitário do método TStrUtils.ToLower

Ao executar esse teste, temos uma barra vermelha de resultado (Testes com acentuação, expected: but was: ), mas sem desanimar, basta realizar a já conhecida correção. No método TStrtUtils, no método ToLower fazemos uso da função AnsiLowerCase em substituição da LowerCase.

Pronto, toda nossa classe está coberta por testes e temos uma barra verde em todos os testes executados. Agora a classe está confiável o suficiente para entrar em produção.


Saiu na DevMedia!

  • Curso de iOS:
    Atualmente o iPhone, o iPad e o iPod touch são os principais dispositivos a serem considerados quando se pretende desenvolver aplicações e jogos para plataformas móveis.

Saiba mais sobre Delphi ;)

  • Guia do Programador Delphi:
    Neste guia de estudos você encontra os conteúdos que precisará para se tornar um programador Delphi completo. Confira a sequência de cursos e exemplos que te guiarão do básico ao avançado em Delphi.

Alguns vão dizer “estou praticamente dobrando o tempo do meu desenvolvimento já que estou escrevendo muito mais linhas de código”, mas não seria necessário testar essa classe de qualquer maneira antes de entrar em produção? E se o método EqualsIgnoreCase() tivesse a sua lógica alterada depois de anos em produção? Quanto tempo seria perdido primeiro para criar coragem de alterar e segundo para se certificar que essas alterações não iriam impactar nos resultados esperados do método? Tenho certeza que com os testes automatizados essas perguntas não ficariam sem respostas. Sem mencionar que o tempo/custo de uma correção em ambiente de produção é infinitamente mais alto do que em ambiente de desenvolvimento.

Outros testes

Vamos imaginar agora que nossa classe TStrUtils deve ter um novo comportamento, toda vez que os métodos ToLower() e ToUpper() forem chamados, deverá subir uma exceção caso o atributo FStr esteja vazio. Essa exceção (Nota 2) deverá ser do tipo EStrUtilsStringVazia.

Nota 2: Pode-se dividir o conjunto de exceções de um sistema em dois subconjuntos: exceções tratadas, que são aquelas que são esperadas e até desejadas e que por serem conhecidas ocorrem dentro de um try .. except para prover uma mensagem amigável ao usuário e talvez até uma saída alternativa. É comum este try..except estar dentro de um try.. finally maior, para liberar recursos que de outra forma não poderiam ser liberados dentro de um except. A instrução finally sempre é executada, mesmo fechando-se o programa, caso o fluxo esteja dentro de um try, o finally será executado.

O outro grupo de exceções é o grupo das exceções não tratadas, onde a princípio não se sabe como e porque elas ocorrem, e talvez apenas o usuário saiba fazer a “mágica” acontecer, mas não saiba explicar como, e nem deveria, pois na concepção dele está fazendo tudo corretamente.

Faremos essa simples alteração, começando com a criação da exceção. Basta criar uma classe que herde da classe base Exception, como mostra o código a seguir:

EStrUtilsStringVazia = class(exception);

E subiremos essa exceção nos métodos em questão, como mostra a Listagem 18.

function TStrUtils.ToLower: string; begin if Self.FStr = EmptyStr then raise EStrUtilsStringVazia.Create("A String está vazia"); Result := AnsiLowerCase(Self.FStr); end; function TStrUtils.ToUpper: string; begin if Self.FStr = EmptyStr then raise EStrUtilsStringVazia.Create("A String está vazia"); Result := AnsiUpperCase(Self.FStr); end;
Listagem 18. Alterações nos métodos ToLower e ToUpper

Feita esta alteração, a primeira coisa a se fazer é executar os testes unitários, que rodam sem problemas. Agora precisamos criar um teste para saber se esta nova lógica está realmente funcionando como o esperado. Para isso vamos utilizar um novo tipo de teste, como mostra a Listagem 19.

procedure TestTStrUtils.TestToUpperExceptionStringVazia; begin FStrUtils.str := EmptyStr; ExpectedException := EStrUtilsStringVazia; FStrUtils.ToUpper(); end; procedure TestTStrUtils.TestToLowerExceptionStringVazia; begin FStrUtils.str := EmptyStr; ExpectedException := EStrUtilsStringVazia; FStrUtils.ToLower(); end;
Listagem 19. Teste unitários para a exceção esperada

Antes de executarmos estes testes, note que estamos usando um elemento novo, o ExpectedException. Como o nome já diz, nesta propriedade é possível indicar qual é a exceção esperada na execução do teste. Algo extremamente útil para quem costuma programar desta forma (fazendo o controle de falha com exceções e não com códigos de erro). Ao indicarmos que estamos esperando uma determinada exception no teste, caso durante a execução deste ocorra a exceção esperada, a DUnit vai interpretar que o teste passou com sucesso.

Conclusão

Ao executar os testes, temos uma gratificante barra verde. Porém algumas coisas no código da classe TStrUtils não estão cheirando bem. A duplicidade de código na geração da exceção de string vazia realmente não está boa, pense, se amanhã a frase padrão desta exceção passasse a ser “Esta rotina não deve ser utilizada para uma string vazia” ou, caso a exceção deixe de ser EStrUtilsStringVazia para ser qualquer outra, já seriam dois lugares diferentes para se alterar, isso porque estamos lidando com um exemplo extremamente simples. Qual seria a solução ideal? Refatorar esse código.

Para alguns essa palavra causa calafrios, frases do tipo “Se mexer no que está funcionando estraga” são realmente muito comuns. Mas, em nosso cenário não existe motivos para temer, nosso código está protegido pelos testes unitários. Caso a refatoração estrague algo, basta jogar a alteração fora e recomeçar novamente. Os testes nos dirão se a alteração está terminada e está segura. Então, basta isolar o código que verifica a string vazia em um método e fazer uso dele onde for necessário. Então, basta executar novamente os testes para termos certeza de que tudo continua funcionando como deveria.

A refatoração, assim como os testes unitários, é um processo de qualidade de software indispensável. Assim, finalizamos sobre testes unitários automatizados, que este sirva de base para entender e aplicar o conceito de uma forma simples e básica.

Artigos relacionados