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.
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;
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;
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.
- Test Case - Qualquer classe de teste que o desenvolvedor crie deverá ser uma subclasse de Test Case;
- Test Suite - O xUnit executa suítes de testes, que nada mais são do que um conjunto de casos de testes;
- SetUp - Para cada método que será executado em nosso caso de testes, o método SetUp é executado antes. Geralmente o SetUp é utilizado parar criar a instância da classe que será testada;
- TearDown - Ao contrário do SetUp, o TearDown é executado ao final de cada método do caso de teste que está sendo executado, independente se o método do caso de teste seja executado com sucesso ou não. Geralmente usado para liberar da memória a instância criada no método SetUp.
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;
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
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:
- Source Project: O projeto que será testado, entre outras palavras, o projeto que possui as classes que serão testadas através de casos de testes que serão criados;
- Project Name: É o nome do projeto de teste, o padrão é sempre o nome do projeto de origem mais a palavra Tests, exemplo ProjUtilsTests;
- Location: Onde o projeto de testes será salvo.
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).
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 DelphiIsso 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.
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.
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;
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;
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;
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();
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;
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).
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;
Agora ao executarmos nossos testes teremos uma agradável e confortável barra verde, como mostra a Figura 4.
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;
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;
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:
- Expected: é o que esperamos que o método retorne;
- Actual: é o que o método realmente retornou;
- Msg: Uma mensagem que será apresentada caso o teste falhe.
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).
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;
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;
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;
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;
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.
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;
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;
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.