A Arte de Depurar (Parte I)
Publicado em: 16 de novembro de 2006
Por André Furtado

Clique aqui para fazer o download do código-fonte deste artigo.

Objetivos:

Este artigo tem o objetivo de discutir conceitos e técnicas de depuração, ilustrando a discussão na prática através do Visual Studio .NET 2005. Enquanto novatos na ferramenta e/ou nos conceitos serão iniciados no processo, veteranos irão encontrar dicas úteis para aumentar sua produtividade e expandir seus conhecimentos além das tarefas básicas da arte de depurar.

Nesta página

Introdução
Depurar por quê?
Depurar o quê?
(Seja pró-ativo e evite o "code-and-fix"!)
Quando depurar?
Ativando o modo depuração
Conclusão
Sobre o Autor

Introdução

"Depuração" pode ser um assunto que não chame muito a atenção daqueles que já tiveram um mínimo contato com o conceito. "O que existiria de diferente além de espalhar breakpoints pelo meu código e depois acompanhar o passo-a-passo da execução da aplicação?", você poderia perguntar.

O Visual Studio .NET 2005 (VS.NET), entretanto, acompanha recursos bastante interessantes de depuração que valem a pena ser conferidos. Alguns são herdados das versões anteriores da IDE , enquanto outros se apresentam como novidade. Neste artigo, dividido em diferentes partes, abordarei alguns desses recursos, não me limitando a explicar o apenas como usá-los mas também quando e por que usá-los. Nesta primeira parte, discutiremos alguns conceitos básicos do processo e como realizar minimamente uma sessão de depuração com sucesso.

Depurar por quê?

Por mais qualificado que seja um desenvolvedor, ele nunca irá se livrar da introdução acidental de erros em seu código. A tarefa de corrigir um erro, entretanto, demanda que sua fonte seja localizada primeiro, o que por muitas vezes pode ser mais difícil do que realizar a correção.

Para aumentar a produtividade na procura pela fonte de um erro, as IDEs oferecem funcionalidades de depuração, que permitem que a execução de uma aplicação seja controlada passo-a-passo pelo desenvolvedor e que seja identificado, em tempo real, o valor de variáveis e a linha de código sendo executada em um dado momento.

Por que isso é útil? Se não fosse pela capacidade da depuração de inspecionar, a qualquer momento, o status interno de uma aplicação, você precisaria alterar o código-fonte original para registrar os fluxos de execução da aplicação e exibir os valores das variáveis desejadas, seja imprimindo no prompt de comando, seja através de caixas de diálogo, seja através da escrita em um arquivo. Já me deparei, inclusive, com aplicações Windows Forms que colocavam no título de seus formulários os valores a serem inspecionados. Além de pouco intuitiva, essa abordagem não funciona caso o tipo de dados da variável inspecionada seja mais complexo ou detalhado, como uma classe ou estrutura em XML. Sem falar que, dessa maneira, a experiência de uso da aplicação durante seus testes torna-se diferente do que seria realmente esperado pelo usuário final, pois a aplicação está sendo visualmente modificada.

Leitores com um conhecimento um pouco mais avançado podem perguntar: "então você está dizendo que mecanismos de tracing não são úteis ou se tornam obsoletos devido à depuração?". A resposta é não ! Tracers fazem mais sentido quando se deseja deixar um "rastro" (que por acaso é uma das traduções do inglês trace) do que foi executado pela aplicação, para posterior análise, por muitas vezes em um contexto no qual a aplicação já está em operação. Por outro lado, a depuração tem foco ainda na etapa de desenvolvimento e é um processo mais atrelado à IDE. Ambos os processos, entretanto, podem ser utilizados para a identificação de bugs.

Depurar o quê?

Espero ter deixado claro, até então, que a depuração é um processo que possui como alvo o código-fonte de uma aplicação qualquer. Dessa forma, vamos definir nesta seção uma aplicação que será o alvo de nossos esforços de depuração.

A aplicação será um Windows Application ("aplicação Windows Forms"), na linguagem C#, que permite ao usuário realizar algumas operações matemáticas, simulando uma calculadora. Seguindo uma boa prática de programação, entretanto, a lógica matemática de nossa calculadora estará situada em uma biblioteca reusável (uma dll), que poderia ser consumida, por exemplo, por outros tipos de interface gráfica, como uma aplicação web. Para implementar tal biblioteca, utilizaremos um projeto do VS.NET do tipo Class Library, desta vez implementado na linguagem Visual Basic NET (VB.NET) para tornar mais interessante o exemplo. Crie no VS.NET, portanto, uma solução chamada DemoDepuracao e insira os dois projetos mencionados acima, de maneira similar ao exibido na Figura 1.

Cc517980.ArteDeDepurar_1(pt-br,MSDN.10).jpg

Figura 1. Solução-alvo do processo de depuração

A classe OperacoesBasicas do projeto FuncoesMatematicas contém, por enquanto, apenas a implementação do método estático (shared)Dividir, como mostrado abaixo. Abstraia a simplicidade (ou até mesmo a pouca utilidade) deste método, que foi criado apenas para propósitos didáticos.

			Public Class OperacoesBasicas
			   Public Shared Function Dividir(ByVal n1 As Integer, ByVal n2 As Integer) As Integer
			      Dim resultado As Integer
			      resultado = n1 / n2
			      Return resultado
			   End Function
			End Class
			

O único formulário da aplicação (FrmPrincipal) é bem simples, como mostra a Figura 2. Assumirei neste artigo nomes intuitivos para cada um dos controles da interface, prefixando caixas de texto com "txt" e botões com "btn", por exemplo. Dessa forma, em FrmPrincipal temos os seguintes controles: txtNumero1, txtNumero2, btnDividir e txtResultado.

Cc517980.ArteDeDepurar_2(pt-br,MSDN.10).jpg

Figura 2. Interface principal do projeto Calculadora

Como esperado, o tratamento do evento de clique do botão btnDividir transforma o input do usuário em tipos de dados int e invoca a biblioteca matemática FuncoesMatematicas (adicionada como uma referência ao projeto Calculadora) para realizar a operação de divisão, exibindo o resultado em txtResultado. Tal comportamento é implementado como mostra o código abaixo. Tratamentos de erros de conversão estão omitidos por motivos de simplificação.

			private void btnDividir_Click(object sender, EventArgs e) {
			   int n1 = int.Parse(txtNumero1.Text);
			   int n2 = int.Parse(txtNumero2.Text);
			   int resultado =
			      FuncoesMatematicas.OperacoesBasicas.Dividir(n1, n2);
			   txtResultado.Text = resultado.ToString();
			}
			

Compile a solução (menu Buid | Build Solution ou CTRL+SHIFT+B) e verifique que nenhum erro é encontrado.

(Seja pró-ativo e evite o "code-and-fix"!)

Neste momento, já poderíamos executar a aplicação e iniciar a exploração dos recursos de depuração no VS.NET. Permita-me, entretanto, abrir um parêntese que considero enriquecedor no contexto dessa discussão.

Uma prática de desenvolvimento, infelizmente muito comum, consiste em programar código e, ignorando a realização de testes unitários ou qualquer revisão, já colocá-lo em execução para a descoberta de eventuais problemas (bugs). Essa má prática é conhecida como "code-and-fix" e evita que o desenvolvedor tenha um importante momento de reflexão e auto-crítica sobre seu próprio código. O code-and-fix é uma abordagem focada em remediar problemas eventualmente encontrados, enquanto o ideal seria o desenvolvedor ter uma abordagem pró-ativa para evitar futuros bugs na aplicação. Lembre-se: é melhor prevenir um bug do que corrigir um bug, até porque a tentativa de correção de um bug pode introduzir novos bugs na aplicação.

A aplicação Calculadora, embora trivial, é útil para ilustrar a discussão. Um desenvolvedor baseado no code-and-fix poderia executar a aplicação, testar alguns valores e dar-se por satisfeito caso não sejam encontrados erros na execução. Um desenvolvedor mais consciente, entretanto, irá analisar seu código a priori, verificando se a funcionalidade implementada satisfaz corretamente o domínio dos possíveis de valores de entrada. No nosso caso, esse desenvolvedor identificaria uma deficiência da aplicação em relação ao tratamento do caso do divisor (n2) do método Dividir ser 0 (zero), o que torna a operação de divisão entre dois inteiros impossível.

Obviamente, a garantia na identificação de 100% dos bugs de uma aplicação antes de sua execução é impossível. A captura desses bugs que passaram despercebidos pelo programador é feita em uma etapa posterior, geralmente por um testador, que executa casos de teste mais complexos, completos e integrados do que os testes unitários.

No cenário da próxima seção, vamos assumir que o bug da divisão por zero passou desapercebido pelo programador. Entretanto, que fique claro que isso não faz parte de uma abordagem code-and-fix, e sim de um cenário com propósitos meramente didáticos. Fecha parêntese.

Quando depurar?

Uma vez que a compilação da solução Calculadora foi bem-sucedida, sua execução pode ser disparada. Através do menu Debug do VS.NET, entretanto, você irá observar que isso pode ser feito de duas maneiras: no modo depuração (Start Debbugging ou F5) ou sem depuração (Start Without Debugging ou CTRL+F5). Embora seja meio irônico a opção de iniciar aexecução sem depuração estar presente em um menu justamente chamado Debug ("Depurar"), a discussão principal é: qual das duas opções escolher?

Vamos entender melhor cada opção antes de encontrar a resposta. Ao escolher a execução no modo depuração, a aplicação será iniciada e, em paralelo, o VS.NET será reconfigurado para este modo (algumas janelas específicas serão exibidas, o depurador da IDE será acoplado ao processo da aplicação em execução, etc.). Vale lembrar que isso demanda um certo tempo de processamento, ao contrário da opção de execução sem depuração, que dispara a aplicação de maneira totalmente desatrelada do VS.NET

O que escolher então? Na verdade não existe resposta correta, estando em jogo a experiência e preferências de cada desenvolvedor. Pessoalmente, utilizo e recomendo a seguinte metodologia:

  • Passo 1: Ao testar uma aplicação sem desconfiar de algum bug em específico, não ative o modo depuração. Isso permite um teste mais próximo da experiência prática que o usuário final irá ter ao utilizar a aplicação.

  • Passo 2: No caso de um bug ser encontrado durante a execução da aplicação, interrompa sua execução, mas ainda não a reinicie no modo depuração. Verifique se você é capaz de resolver o problema apenas estudando o código-fonte para encontrar o que houve de errado.

    • Passo 2a: Caso consiga, corrija o código e execute a aplicação novamente sem o modo depuração. O processo é reiniciado (passo 1).

    • Passo 2b: Caso não consiga encontrar o erro apenas estudando o código, reinicie a execução da aplicação, agora sim no modo depuração, de modo a solicitar que a IDE mostre onde seu pensamento lógico errou (uma certa variável assumiu um valor inesperado ou o programa entrou por um certo fluxo de execução inesperado, por exemplo). De acordo com esse feedback, realize novas alterações no código para corrigir o bug.

      • Caso considere o bug corrigido, volte a testar sem o modo depuração e o processo se reinicia (passo 1).

      • Caso suspeite que o bug ainda persiste (i.e., você não foi capaz de identificar sua fonte ou resolvê-lo com o último feedback da depuração), volte a executar a aplicação no modo depuração, tentando obter um melhor feedback do processo (passo 2b).

Seguindo a metodologia proposta acima, execute a aplicação sem o modo depuração CTRL+F5. Após testar a calculadora com alguns valores, iremos descobrir que o teste falha para o caso no segundo número (o divisor) ser zero: um erro de overflowaritmético (OverFlowException) é lançado para o usuário, como mostra a Figura 3. Mais uma vez por motivos didáticos, vamos supor que não fomos capazes de identificar e/ou corrigir o problema apenas analisando o código, como propõe a metodologia acima. Nesse caso, precisaremos recorrer ao auxílio do modo depuração para encontrarmos a fonte do bug.

Cc517980.ArteDeDepurar_3(pt-br,MSDN.10).jpg

Figura 3. Bug encontrado na aplicação, ainda não no modo depuração

Ativando o modo depuração

Em busca da fonte do erro reportado acima, você irá agora executar a aplicação no modo depuração. Confira se a opção Debug está selecionada no combobox de configurações da solução, presente na toolbar padrão do VS.NET, como mostra a Figura 4. Em seguida, pressione F5 ou acesse a opção do menu | Start DebuggingDebug.

Cc517980.ArteDeDepurar_4(pt-br,MSDN.10).jpg

Figura 4. Configuração corrente da solução deve ser Debug

Quando a aplicação iniciar sua execução no modo depuração, abrindo a aplicação Calculadora (Figura 2), você irá observar algumas mudanças no próprio VS.NET, que passará a exibir uma série de novas janelas de ferramentas agrupadas (como mostra a Figura 5) e ocultará temporariamente outras janelas, como a janela de propriedades. Essas janelas estão relacionadas às diversas funcionalidades de depuração oferecidas pela IDE e serão exploradas nas próximas partes deste artigo.

Cc517980.ArteDeDepurar_5(pt-br,MSDN.10).jpg

Figura 5. Janelas de ferramentas exibidas pelo VS.NET no modo depuração

Repita o cenário em que a aplicação apresentou um bug durante seu teste anterior, fazendo o divisor ("número 2") ser zero e clicando no botão que solicita a divisão. Dessa vez, o .NET Framework não irá apenas estourar umaexceção para o usuário. Inteligentemente, ele agora trabalhará em conjunto com o depurador do VS.NET, que está acoplado à aplicação, e a você será exibido o ponto exato em que o erro aconteceu, como mostra a Figura 6. Tal funcionalidade, embora simples, representa um enorme ganho de produtividade para o desenvolvedor, que pode localizar mais facilmente a fonte de bugs sem precisar inspecionar milhares de linhas de código.

Cc517980.ArteDeDepurar_6(pt-br,MSDN.10).jpg

Figura 6. Depurador do VS.NET informando fonte do erro

Uma vez que a fonte do erro foi identificado, você pode parar o processo de depuração (e consequentemente encerrar a execução da aplicação) clicando no botão de stop indicado na Figura 7. Corrija a aplicação e reinicie sua execução, de modo a checar se o bug foi realmente eliminado.

Cc517980.ArteDeDepurar_7(pt-br,MSDN.10).jpg

Figura 7. Interrompendo a depuração (e a aplicação

A correção do bug (tratamento do caso do divisor ser zero) pode ser feita de diferentes maneiras e fica por conta do leitor. Através do feedback da Figura 6, você passa a entender que o método Dividir não pode ser chamado com o parâmetro n2 valendo zero. Dessa forma, uma opção seria evitar tal chamada no projeto Calculadora, exibindo uma caixa de diálogo informando que o valor digitado pelo usuário é inválido. Isso resulta no seguinte código para o tratamento do evento de clique no botão btnDividir:

				private void btnDividir_Click(object sender, EventArgs e) {
				   int n1 = int.Parse(txtNumero1.Text);
				   int n2 = int.Parse(txtNumero2.Text);
				   if (n2 == 0) {
				      MessageBox.Show("divisor não pode ser zero!");
				   } else {
				      int resultado =
					 FuncoesMatematicas.OperacoesBasicas.Dividir(n1, n2);
				      txtResultado.Text = resultado.ToString();
				   }
				}
				

Conclusão

Neste artigo, cobrimos o básico da arte de depurar, discutindo sobre alguns conceitos e abordando tarefas que todo desenvolvedor precisa realizar minimamente para depurar uma aplicação. Possivelmente para os desenvolvedores de média ou avançada experiência, este artigo não trouxe muitas novidades em termos de recursos práticos do VS.NET. Entretanto, ao entendermos a teoria do "por que" e "quando" depurar, nos tornamos programadores mais produtivos e pró-ativos. Tão importante quanto isso, este artigo abriu o caminho para realizar discussões mais interessantes e explorar recursos mais avançados de depuração na IDE. Fiquem sintonizados nos próximos artigos!

André Furtado

Sobre o Autor

André é engenheiro de software pelo Centro de Inovação Microsoft de Recife, bacharel e mestre em Ciência da Computação pela UFPE, Microsoft Student Partner, Certified MSF Practitioner, MCP, Certified IBM-DB2 Specialist, SCJP 1.4, campeão mundial / nacional da competição Imagine Cup 2005 e um dos líderes do grupo de usuários Sharp Shooters .NET.

Page view tracker