Prévia do material em texto
Introdução à Lógica de Programação
UNIDADE 1
Etapas : Algoritmo é uma sequência lógica de instruções que podem ser
executadas. É importante destacar que qualquer tarefa que siga um
certo padrão.
Etapas :
1. Algoritmo é uma sequência lógica de instruções que podem ser
executadas. É importante destacar que qualquer tarefa que siga um certo
padrão pode ser representada por um algoritmo; entretanto, para montá-
lo é necessário dividir a tarefa em três fases fundamentais.
2. Etapas de um algoritmo?
Entrada: São as informações que iniciam o algoritmo. Processamento: São
os passos necessários para atingir a meta.
Saída: São os resultados do processamento. de ser julgada.”
3. Exemplo de Algoritmo?
Problema: Calcular a média final dos alunos da 8ª Série. Os alunos
realizarão quatro provas: P1, P2, P3 e P4.
4. Construção de um Algoritmo
Para montar o algoritmo proposto, fazem-se três perguntas:
1) Quais são os dados de entrada?
Resp.: P1, P2, P3 e P4.
2) Qual será o processamento a ser utilizado?
Resp.: Somar todos os dados de entrada e dividi-los por 4
3) Qual será o dado de saída?
Resp.: A média final
5. Algoritmo: Recebe a nota da prova P1 Recebe a nota de prova P2
Some todas as notas e divida o resultado por 4
Mostre o resultado da divisão
6. Verificação do Algoritmo:
Ao desenvolver um algoritmo, em seguida ele deverá sempre ser testado
para verificar o seu bom funcionamento. Esta verificação chama-se: Teste
de Mesa, onde é simulada a execução das instruções do algoritmo para
provar se os passos utilizados levarão ao resultado esperado ou não.
7. Do exemplo anterior: Nota da Prova P1 Nota da Prova P2
Dá-se valores à tabela abaixo:
8. O que é um diagrama de bloco?
Uma forma eficiente de representar os passos lógicos de uma
determinada tarefa é a utilização de um diagrama de blocos porque ele
segue um padrão, fazendo com que o seu entendimento ( mesmo não
estando a par do problema em questão) torne-se bastante facilitado.
9. Diagrama de Blocos
Através do uso do diagrama pode-se definir uma sequência de símbolos,
com significado bem definido, assim a sua principal função é a de facilitar
a visualização dos passos de execução de uma tarefa.
10. Simbologia Padrão
Em um diagrama de blocos existem diferentes símbolos e no quadro a
seguir, são mostrados alguns dos principais símbolos utilizados:
No interior do símbolo é escrito uma expressão matemática ou lógica,
uma ação, um índice e etc., o que for relevante mostrar, pois somente os
símbolos vazios não significarão nada. Veja o exemplo na Tabela.
11. Simbologia ?
Cada uma destas formas se aplica a uma determinada ação como está
indicado. Existem outras formas que podem ser aplicadas.
12. Simbologia:
13. Exemplo de Diagrama de Blocos
Percebe-se que no primeiro exemplo (da bala) uma sequência lógica foi
seguida, utilizando somente as informações diretas, porém no segundo
exemplo (da média) foi utilizado um cálculo e a seguir, foi exibido o seu
resultado final.
14.
15. Há diversas formas de representação de algoritmos que diferem entre
si pela quantidade de detalhes de implementação que fornecem ou,
inversamente, pelo grau de abstração que possibilitam com relação à
implementação do algoritmo em termos de uma linguagem de
programação específica.
Dentre as principais formas de representação de algoritmos destacam-se:
a descrição narrativa, o fluxograma convencional e o pseudocódigo (ou
linguagem estruturada).
16. ATIVIDADES DE APRENDIZAGEM
1) Construa um diagrama de blocos que:
• Leia a cotação do dólar
• Leia um valor em dólares
• Converta esse valor para Real
• Mostre o resultado
2) Desenvolva um diagrama que:
• Leia 4 (quatro) números
• Calcule o quadrado para cada um
• Somem todos e
• Mostre o resultado
17. ATIVIDADES DE APRENDIZAGEM
3) Construa um algoritmo para pagamento de comissão de vendedores de
peças, levando-se em consideração que sua comissão será de 5% do total
da venda e que você tem os seguintes dados:
• Identificação do vendedor
• Código da peça
• Preço unitário da peça
• Quantidade vendida
A seguir, construa o diagrama de blocos do algoritmo desenvolvido, e ao
final, faça um teste de mesa.
18. ATIVIDADES DE APRENDIZAGEM
4) Identifique os dados de entrada, processamento e saída no algoritmo
abaixo:
• Receba código da peça
• Receba valor da peça
• Receba Quantidade de peças
• Calcule o valor total da peça (Quantidade * Valor da peça)
Mostre o código da peça e seu valor total
TESTE DE SOFTWARE: INTRODUÇÃO, CONCEITOS BÁSICOS E TIPOS DE
TESTES
Você sabe o que é um teste de software e quais são os principais tipos de
teste de software? Neste artigo você vai tirar todas as suas dúvidas.
Por mais que se planeje a construção de um software, erros são passíveis
de ocorrer. Pode ser um bug num game, uma falha que feche um
programa ou um erro que impossibilite você salvar um arquivo.
Quem já passou por esse tipo de situação sabe como é chato quando
ficamos na mão por culpa de um programa com falhas. O teste de
software serve justamente para tentar encontrar possíveis erros que um
programa recém-desenvolvido possa apresentar, de modo a conseguir
corrigi-lo antes que seja lançado no mercado, ficando disponível para uso
do público.
O teste de software geralmente é a última etapa na construção de um
programa, visando checar o seu nível de qualidade. Os defeitos que um
teste busca identificar incluem erro de compatibilidade, de algum
algoritmo, de requisitos que não podem ser complementados, limitação
de hardware etc. A lista é grande e aumenta com o tamanho do
programa.
QUAIS OS TIPOS DE TESTE DE SOFTWARE?
Existem diferentes tipos de testes que podem ser aplicados num software
para identificar suas falhas, sendo as principais:
– Teste da caixa branca – utiliza o aspecto interno do programa/sistema,
o código fonte, para avaliar seus componentes. Ele também é conhecido
como teste orientado à lógica ou estrutural. Podem ser analisados itens
como: fluxo dos dados, condição, ciclos etc. Na hora de implementá-lo é
preciso verificar a criticidade, a complexidade, a estrutura e o nível de
qualidade que se pretende obter do programa, envolvendo confiança e
segurança;
– Teste da caixa preta – diferente do teste anterior, que prioriza os
aspectos internos, o teste da caixa preta verifica aspectos externos. Os
requisitos funcionais do sistema são avaliados. Não se observa o modo de
funcionamento, sua operação, tendo como foco as funções que deverão
ser desempenhadas pelo programa. Desse modo, avalia-se se um grupo
de entrada de dados resultou nas saídas pretendidas, levando-se em
consideração a especificação do programa. Ou seja, o que se esperava
que o software deveria fazer. É conhecido também como técnica
funcional;
– Teste da caixa cinza – esse tipo de teste une os dois anteriores, por isso
o termo “cinza”. Avalia tanto os aspectos internos quanto os externos, de
entrada e saída. Pode utilizar-se de engenharia reversa;
– Teste de regressão – esse consiste em realizar testes a cada versão de
um software, onde se modificam-se funcionalidades. Desse modo, evita-
se que erros que foram corrigidos antes no software antes voltem a
aparecer na hora de se incrementar algo novo a ele.
– Teste de unidade – testa-se unidades menores de um software, de
modo isolado, para ver se todas funcionam adequadamente;
– Teste de integração – depois das unidades testadas, realiza-se uma
verificação se elas funcionam juntas, integradas. Pode ocorrer delas
apresentarem incompatibilidades ao funcionarem em conjunto, mesmo
após terem sido aprovadas no teste de unidade;
– Teste de carga – esse teste é feito para avaliar os limites de uso do
software, o quanto ele suporta em volume de informações, tráfego etc.
sem que apresente erros;
– Teste de usabilidade – esse teste é feito por um pequenogrupo de
usuários para ver se o software satisfaz as suas necessidades. Nesse teste
analisa-se como o usuário usa o sistema, verificando onde ele tem mais
dificuldade. Ouve-se também suas impressões, porém é preciso
confrontá-las com as observações do avaliador;
– Teste de stress – aqui leva-se o software ao seu limite de potência e
funcionamento, para mais ou para menos, de modo a avaliar em qual
ponto ele deixa de funcionar adequadamente. Isso é feito para verificar se
suas especificações máximas ou mínimas de uso estão corretas.
O QUE É UM PLANO DE TESTE DE SOFTWARE?
Um plano de teste é feito para colaborar com o desenvolvimento de um
software. É por meio desse plano que os componentes técnicos,
funcionais, estruturais etc. serão verificados e validados, de modo a
garantir o bom funcionamento do programa junto ao usuário final. Sendo
assim, um plano de teste de software tem como foco garantir a
confiabilidade e segurança de um software, identificando possíveis erros
e falhas durante a sua confecção, ou mesmo depois.
Ele deve ser planejado em conjunto com a proposta do software, sendo
aplicado em cada etapa do projeto e não somente no final.
FERRAMENTAS DE TESTE DE SOFTWARE
Para garantir a qualidade de um programa, as desenvolvedoras realizam
testes nele. Isso é necessário para que falhas sejam detectadas antes que
o software seja colocado no mercado. Sabe aquele programa que vive
travando, não roda direito ou que faz o PC ficar lento? Esse,
provavelmente, deve ter passado pelo processo de desenvolvimento com
essas imperfeições. Então, para evitar que isso aconteça, as empresas
contratam profissionais (os testadores de software ou analistas de testes)
para identificarem esses problemas e relatarem para que os
desenvolvedores os corrijam. Mas, para fazer isso eles precisam realizar
uma bateria de testes diferentes, que envolvem desde análise da
estrutura interna do software até a avaliação da interface.
O QUE SÃO FERRAMENTAS DE TESTE DE SOFTWARE?
Para que esses testes possam ser realizados de modo mais rápido e com
maior abrangência, existem ferramentas que automatizam alguns deles
ou auxiliam na execução de outros. Essas são as ferramentas de teste de
software.
O QUE É E O QUE FAZ UM TESTADOR DE SOFTWARE?
Com a expansão do mercado de tecnologia da informação (TI), uma nova
profissão surgiu e tem crescido nos últimos anos: o testador de software
ou analista de testes. Esse profissional é contratado para encontrar erros,
falhas, bugs e outros tipos de problemas que não foram detectados
durante a confecção de um software.
O mercado para esse tipo de profissão é amplo. Abrange desde a
prestação de serviços de testes de softwares para programas gerenciais
até aplicativos de smartphones voltados para o público. E a expectativa é
de que ele fique cada vez maior, à medida em que clientes de
desenvolvedoras de softwares passam a solicitar a avaliação desse
profissional nos programas encomendados.
AUTOMAÇÃO DE TESTE DE SOFTWARE
Num mundo cada vez mais interligado pela tecnologia, os planos de testes
de softwares têm um peso importante, pois muitos negócios dependem
de que esses estejam funcionando corretamente.
Qualquer falha num programa de gerenciamento financeiro pode
acarretar prejuízos grandes em termos monetários. Um erro num
software de um equipamento médico pode custar a vida uma pessoa ou
dificultar o atendimento a alguém que precisa.
E não é só os casos mais extremos que precisam de testes de software de
qualidade, pois uma simples falha de um programa de uso comum que
ocorra com frequência pode fazer com que usuário dele abra queixa ou
não o recomende para outros usuários, migrando para a concorrência.
Quem perde é quem desenvolveu o software.
PROCESSO DE TESTE DE SOFTWARE
O Processo de Testes de Software representa uma estruturação de
etapas, atividades, artefatos, papéis e responsabilidades que buscam a
padronização dos trabalhos e ampliar a organização e controle dos
projetos de testes.
O Processo de Teste, como qualquer outro processo deve ser revisto
continuamente, de forma a ampliar sua atuação e possibilitar aos
profissionais uma maior visibilidade e organização dos seus trabalhos, o
que resulta numa maior agilidade e controle operacional dos projetos de
testes.
ETAPA 1: PLANEJAMENTO DOS TESTES
Esta etapa caracteriza-se pela definição de uma proposta de testes
baseada nas expectativas do Cliente em relação à prazos, custos e
qualidade esperada, possibilitando dimensionar a equipe e estabelecer
um esforço de acordo com as necessidades apontadas pelo Cliente.
DINÂMICA DAS MACRO-ATIVIDADES
Este diagrama representa a seqüência das “macro-atividades” a serem
executadas na etapa de “Planejamento dos Testes”.
DETALHAMENTO DAS MACRO-ATIVIDADES
Esta lista representa o conjunto de atividades que deverão ser executadas
para que cada macro-atividade seja considerada finalizada, funcionando
como um “check-list” de execução da etapa de “Planejamento dos
Testes”.
ESTUDO DO PROJETO:
• Estudar as modificações solicitadas pelo Cliente (novos requisitos);
• Estudar as modificações de arquiteturas dos aplicativos;
• Estudar as lições aprendidas dos Projetos Anteriores;
• Avaliar expectativas de custos, prazos e qualidade exigidas pelo
Cliente;
• Avaliar os riscos envolvidos nos Projetos e seus impactos neste
processo;
AVALIAÇÃO DE IMPACTO:
• Avaliar se o projeto exige a criação de casos de testes
“progressivos”;
• Avaliar se o projeto exige modificações em casos de testes
“regressivos”
• Avaliar se o projeto exige adequações na automação dos testes;
• Avaliar se o projeto exige adequação nas atuais ferramentas
empregadas;
• Avaliar se o projeto exige a aquisição/construção de novas
ferramentas;
• Avaliar se o projeto exige modificações na estruturação do
ambiente;
ANÁLISE INTERNA DE ESFORÇO
• Levantar métricas históricas para auxiliar na elaboração das
estimativas de esforço;
• Estimar esforço interno para absorção dos impactos da Arquitetura
dos Testes;
• Demonstrar esforço externo para absorção dos impactos da
Arquitetura dos Testes;
ANÁLISE EXTERNA DE ESFORÇO:
• Avaliar disponibilidade de espaço físico e infra-estrutura para os
Terceiros;
• Especificar as necessidades de adequações que serão repassadas a
Terceiros;
• Especificar métricas de qualidade e produtividades esperadas;
• Especificar SLA’s de serviço e multas contratuais;
• Estabelecer concorrência e obter a melhor proposta (opcional);
• Receber Proposta de Trabalho (Cronograma, Prazos e Custos da
Terceirização);
• Definição de Cenários Possíveis (Duração, Esforço, Custo e
Qualidade):
• Levantar Lista de Projetos em Andamento e a serem Iniciados;
• Avaliar a disponibilidade de recursos internos para alocação no
Projeto;
• Identificar Cenários Diversos (Terceirização, Redução de Escopo,
Repriorização de Projetos);
• Definir Cronograma-Macro para cada cenário identificado;
• Definir Riscos para cada cenário identificado e Planos de Ação
Esperados;
• Estabelecer Propostas e Aguardar aprovação da Diretoria;
• Aprovação do Planejamento:
• Obter o Aceite das Propostas de Cenários Aprovados pela Diretoria;
• Obter o Aceite de uma das Propostas pelo Cliente;
• Divulgar do Cenário Aprovado do Projeto aos colaboradores e
terceiros;
• Obter a Assinatura do CONTRATO-MESTE e elaborar os ANEXOS; (no
caso de terceirização)
• Alocar Espaço Físico dos Terceiros; (no caso de terceirização)
• Comunicar a Finalização da Etapa de Planejamento dos Testes;
(externo)
• Definição das Responsabilidades
• Neste diagrama, está a representação dos papéis e
responsabilidades para cada grupo de atividades envolvido na etapa
de “Planejamento dos Testes”.
DEFINIÇÃO DE CENÁRIOS POSSÍVEIS (DURAÇÃO, ESFORÇO, CUSTO E
QUALIDADE):
• Levantar Lista de Projetos em Andamento e a serem Iniciados;
• Avaliar a disponibilidade de recursos internos para alocação no
Projeto;• Identificar Cenários Diversos (Terceirização, Redução de Escopo,
Repriorização de Projetos);
• Definir Cronograma-Macro para cada cenário identificado;
• Definir Riscos para cada cenário identificado e Planos de Ação
Esperados;
• Estabelecer Propostas e Aguardar aprovação da Diretoria;
APROVAÇÃO DO PLANEJAMENTO:
• Obter o Aceite das Propostas de Cenários Aprovados pela Diretoria;
• Obter o Aceite de uma das Propostas pelo Cliente;
• Divulgar do Cenário Aprovado do Projeto aos colaboradores e
terceiros;
• Obter a Assinatura do CONTRATO-MESTE e elaborar os ANEXOS; (no
caso de terceirização)
• Alocar Espaço Físico dos Terceiros; (no caso de terceirização)
• Comunicar a Finalização da Etapa de Planejamento dos Testes;
(externo)
DEFINIÇÃO DAS RESPONSABILIDADES
Neste diagrama, está a representação dos papéis e responsabilidades
para cada grupo de atividades envolvido na etapa de “Planejamento dos
Testes”.
MAPEAMENTO DOS ARTEFATOS
Nesta representação gráfica, estão destacados os “artefatos de entrada”
exigidos como premissa para que cada macro-atividade possa ser
realizada. Também são destacados os “artefatos de saída” produzidos
como resultado da atividade.
ETAPA 2: ESPECIFICAÇÃO DOS TESTES
Esta etapa é caracterizada pela identificação dos casos de testes que
deverão ser construídos e modificados em função das mudanças
solicitadas pelo Cliente, bem como pelo próprio aperfeiçoamento do
processo de testes (ampliação da cobertura).
DINÂMICA DAS MACRO-ATIVIDADES
Este diagrama representa a seqüência das “macro-atividades” a serem
executadas na etapa de “Especificação dos Testes”.
DETALHAMENTO DAS MACRO-ATIVIDADES
Esta lista representa o conjunto de atividades que deverão ser executadas
para que cada macro-atividade seja considerada finalizada, funcionando
como um “check-list” de execução da etapa de “Especificação dos Testes”.
ESTUDO DOS REQUISITOS:
• Estudar os requisitos funcionais e não funcionais solicitadas pelo
Cliente (novos requisitos);
• Estudar as modificações de requisitos solicitados pelo Cliente
(mudanças de requisitos);
• Revisar os artefatos e identificar “inconsistências” dos requisitos;
• Estabelecer o Aceite dos Documentos fornecidos e “feedback” da
qualidade dos mesmos;
• Estudar as lições aprendidas da Etapa “Especificação de Testes”;
ESPECIFICAR AS ADAPTAÇÕES DA ARQUITETURA DOS TESTES:
• Especificar as adequações nas atuais ferramentas empregadas;
• Especificar as novas ferramentas exigidas pelo projeto;
• Especificar as modificações estruturais na organização do ambiente;
• Especificar as adequações na automação da preparação do
ambiente (script de teste);
• Especificar as adequações na automação da execução dos testes
(script de teste);
• Especificar as adequações na automação da análise dos resultados
(script de teste);
IDENTIFICAÇÃO DOS CASOS DE TESTES
• Identificar cada solicitação de mudança requisitada pelo Cliente;
• Identificar todos os Casos de Uso envolvidos em cada solicitação;
• Identificar Casos de Uso não cobertos adequadamente por Casos de
Testes; (legado)
• Identificar todos o Fluxos do Caso de Uso (Básico, Alternativo e
Exceção);
• Identificar os casos de testes que garantam cada Fluxo do Caso de
Uso;
REFINAMENTO DOS CASOS DE TESTES:
• Estabelecer dinâmica com os Analistas de Testes que possuem
conhecimento horizontal;
• Apresentação de um quadro-geral do impacto das mudanças nos
respectivos aplicativos;
• Cada Analista de Testes apresenta seus casos de testes por
aplicativo;
• O grupo de Analistas de Testes criticam e sugerem melhorias nos
casos de testes;
• O grupo de Analista de Testes avalia o nível de cobertura alcançado;
• Novas reuniões serão realizadas até que seja alcançado o patamar
ideal de casos de testes;
ACEITE DOS CASOS DE TESTES:
• Identificar Áreas-Chaves para apresentação dos casos de testes
(Clientes Internos e Externos)
• Apresentar os casos de testes “progressivos” que serão aplicados
nos testes;
• Apresentar os casos de testes “regressivos” que serão aplicados nos
testes;
• Realizar refinamento dos casos de testes apresentados (“regressivos
e progressivos”);
• Estabelecer o acordo Mútuo de Responsabilidade sobre o Nível de
Qualidade do Software;
REFINAMENTO DO PROJETO DE TESTES:
• Reavaliar as estimativas de esforço e duração do Processo de Teste;
(se necessário)
• Estabelecer um Cronograma-Detalhado, baseado no Cronograma-
Macro já elaborado;
• Reavaliar riscos do Projeto em função de um maior detalhamento
sobre os requisitos;
• Negociar eventuais modificações em relação à duração, prazo e
custo do projeto de testes;
• Comunicar a Finalização da Etapa de “Especificação dos Testes”;
(externo)
DEFINIÇÃO DAS RESPONSABILIDADES
Neste diagrama, está a representação dos papéis e responsabilidades para
cada grupo de atividades envolvido na etapa de “Especificação dos
Testes”.
MAPEAMENTO DOS ARTEFATOS
Nesta representação, estão destacados os “artefatos de entrada” exigidos
como premissa para que cada macro-atividade possa ser realizada.
Também são destacados os “artefatos de saída” produzidos como
resultado da atividade.
CALCULANDO O ROI (RETORNO DE INVESTIMENTOS) EM TESTE DE
SOFTWARE
Convido você agora a se aprofundar e calcular junto comigo os custos dos
defeitos para um software e o retorno do investimento na realização de
teste de software.
Primeiramente devemos identificar custos, incidências e percentuais de
correção de defeitos nas fases do ciclo de desenvolvimento do software,
assim conseguiremos realizar um cálculo de valores baseado na realidade.
Em primeiro lugar, é preciso observar que o custo de correção de defeitos
encontrados em qualquer pacote de software cresce exponencialmente a
cada fase da criação ou existência deste software, o que pode ser
representado, segundo o autor Ron Patton, da seguinte forma [8] como
mostra a figura abaixo.
Aumento exponencial dos custos com defeitos segundo Patton [8]
O custo de encontrar defeitos e removê-los na fase de especificação é
baixíssimo, na ordem de grandeza de dezenas de centavos (de uma
moeda qualquer). Na fase de design estes custos já crescem para a ordem
de grandeza de unidades de moeda, e assim por diante, até chegarem a
custos na casa das centenas quando o software já está em produção. O
custo baixo de se encontrar defeitos nas fases de especificação e design
se justifica pelo baixo — ou, em alguns casos, desprezível — retrabalho
resultante da descoberta e correção destes defeitos nestas fases iniciais.
Seria o equivalente a amassar o guardanapo e começar a rabiscar
novamente. Defeitos encontrados na fase de codificação já são mais caros
porque muitas vezes exigem testes realizados pelos desenvolvedores (o
que toma tempo), correção ou mesmo descarte de código desenvolvido e,
nos piores casos, um retrocesso às fases de especificação e design.
Já os defeitos encontrados na fase de produção, além de todos os custos
anteriores, implicam em custos de atendimento ao cliente, replicação dos
defeitos em laboratório, chegando até mesmo aos temidos recalls. Em
suma, o raciocínio de Patton é claro: quanto mais cedo forem
encontrados os defeitos, menos custarão, tanto para os desenvolvedores
quanto para os clientes.
https://blog.onedaytesting.com.br/wp-content/uploads/2016/02/bug-momentos.png
Quanto à eficiência dos testes na descoberta de defeitos de software,
dados obtidos a partir de projetos da própria Sofist mostram que quando
o cliente atribui tarefas de testes para parte de seu time de
desenvolvimento, este consegue descobrir no máximo 40% dos defeitos
do software em questão.
CALCULANDO O RETORNO SOBRE O INVESTIMENTO (ROI)
Tomando por base empresas que seguem o modelo CMMI, temos que
aquelas que se enquadram no nível 1 tipicamente entregam software com
7,5 defeitos a cada 1.000 linhas de código, enquanto as empresas com
certificação CMMI nível 5 produzem softwarecom melhor qualidade,
reduzindo os defeitos para uma média de 1 a cada 1.000 linhas de código
[9].
Nesta situação, tomemos por hipótese um pacote de software com 1
milhão de linhas de código — nada incomum em projetos atuais — que
tenha sido codificado com 1.000 (mil) defeitos inadmissíveis, isto é, que
terão que ser corrigidos a qualquer custo, seja em que fase da vida do
software forem encontrados. Assumindo, de acordo com o raciocínio de
Patton, que defeitos encontrados durante a fase de desenvolvimento
tenham custo de R$ 1,00, durante a fase de testes tenham um custo de
R$ 10,00 e que os mesmos defeitos encontrados com o software em
produção tenham um custo de R$ 100,00, podemos construir um cenário
interessante.
https://www.sofist.com.br/
ANÁLISE DE 3 SITUAÇÕES:
1. O software vai para o mercado sem ter sido formalmente testado;
2. O software passa por um processo de testes realizados por pessoal
interno, interinamente designado pela empresa para realizá-los;
3. O software passa por um processo testes realizados por equipe
especializada terceirizada;
PREMISSAS DA ANÁLISE
• 10% dos defeitos serão encontrados na fase de codificação, 0%, 40%
ou 85% dos defeitos serão encontrados na fase de testes
(respectivamente nos cenários 1, 2 e 3) e o restante será encontrado
na fase de produção;
• O salário considerado no caso dos testes internos é de R$3.000,00
mensais, com custo para a empresa (acrescido dos benefícios) de
R$5.000,00 mensais;
• Ainda no caso de testes internos, considera-se a atuação parcial de
um gerente de testes com salário de R$5.000,00 (R$10.000,00, com
benefícios), com 50% de seu tempo designado para o processo de
testes;
• Os testes terceirizados são conduzidos por uma equipe que custa
R$10.000,00 por mês, durante três meses;
• Considera-se ROI como sendo a razão entre os valores
economizados (em relação à abordagem sem testes) e o valor dos
investimentos em cada uma das opções de testes avaliadas neste
caso;
RESULTADOS – TABELA ROI
De acordo com este raciocínio, podemos compor a tabela apresentada
acima. A tabela mostra com clareza que os investimentos em testes de
software, sejam estes realizados por pessoal interno ou por equipe
especializada terceirizada, trazem vantagens financeiras, constituindo-se
em investimentos que geram reduções relevantes dos custos de
desenvolvimento do produto.
Os resultados, como pode ser claramente visto na imagem acima,
apresentam uma justificativa sólida para a realização dos testes.
ESTILO DE CÓDIGO – BOAS PRÁTICAS DE PROGRAMAÇÃO EM
LINGUAGEM C
No mundo dinâmico do desenvolvimento de software para sistemas
embarcados, um código ou algoritmo não pode simplesmente ser
considerado como correto por realizar aquilo que se deseja. Outros
aspectos relacionados à legibilidade, manutenção e segurança devem ser
considerados durante sua implementação.
Um desenvolvedor de software em geral, inclusive embarcado,
idealmente deve prezar por manter seu código dentro de padrões
definidos ou pelo senso comum, pelo projeto em que se está contribuindo
na comunidade, ou por regras da própria empresa em que se trabalha.
Este artigo considera as boas práticas de programação para qualquer
nível, desde desenvolvedores que estão entrando no mundo embarcado
até sêniors que procuram melhorar sua performance no desenvolvimento
de código.
Mas o que seriam estes estilos de código, para que servem e o que
podemos conseguir de melhorias ao adotar boas práticas de
programação?
Bem, de forma geral, o estilo de código pode ser considerado uma
questão filosófica diretamente ligada aos gostos do desenvolvedor.
Porém, tanto para desenvolvedores embarcados quanto para propósitos
gerais, existem alguns estilos adotados (e comprovados de alguma forma)
que podem beneficiar tanto o próprio desenvolvedor quanto outros que
porventura poderão vir a utilizar o código ou algoritmo desenvolvido.
Iremos neste artigo exemplificar como não programar na linguagem C, ou
seja, uma pequena paródia ao que se deveria ser feito em comparação a
erros comuns da linguagem, citando pensamentos que o programador faz
durante o desenvolvimento do código.
“ISSO NUNCA VAI ACONTECER”
Desenvolvedor se depara em situações que “nunca irão acontecer”, e, por
conta disso, algumas verificações no código deixam de existir. Um
exemplo famoso para este tipo de suposição aconteceu em 2014 com o
tão conhecido YouTube, onde o vídeo Gangnam Style causou o overflow
da variável que conta o número de visualizações de vídeo (int32_t até
então). Este caso não foi tão grave, mas em uma situação crítica,
suposições deste tipo devem garantir que o que não pode acontecer
realmente não irá acontecer. Uma boa forma de garantir que tais
suposições realmente assegurem que não possam acontecer é
utilizar asserts, exemplo:
C
1
2
3
4
void write_string(char *str) {
assert(str != NULL);
/* codigo aqui */
}
É um exemplo bem simples, mas vamos imaginar que esta função seja
uma função interna de uma lib, e portanto, quem a desenvolveu está
garantindo que nunca vai acontecer um caso em que uma string nula será
passada. Porém, aqui é o caso perfeito de se prevenir contra aquilo que
“nunca irá acontecer”, portanto, o uso do assert é bem recomendado.
P.S.: Claro, se for uma lib que será utilizada por outros clientes, é natural
existir verificações dos argumentos da função, mas não iremos entrar
neste caso, pois é o caso normal de verificação de possíveis erros durante
o desenvolvimento de código.
Pensando no próprio caso do YouTube, a suposição de que nenhum vídeo
seria visualizado mais vezes do que um signed int de 32 bits pudesse
armazenar, também pode ser verificado:
C
1
2
3
4
int increment_counter() {
assert(count < LONG_MAX);
++count;
}
Outro caso simples, mas pensando em uma aplicação de um sistema
crítico, onde o desenvolvedor assumiu que o nível do tanque nunca irá
ultrapassar um valor limite. Esta suposição pode não ser sempre verdade,
e em caso de overflow, a variável irá começar a contar do zero, e sua
planta estará transbordando, mas seu sistema de controle irá dizer que o
nível está baixo, ou normal.
Assertions são bons candidatos para se garantir que coisas que “nunca
irão” acontecer realmente nunca irão acontecer.
BOAS PRÁTICAS DE PROGRAMAÇÃO – FORÇAR EVITAR FALSOS
POSITIVOS
Existem alguns casos de desenvolvimento em que estamos realizando
testes unitários em nosso código e, ao forçar algum erro, temos falsos
positivos. Ou seja, ao testar o retorno de uma função para algum erro,
devemos garantir que estamos testando contra o erro que foi forçado e
não testando apenas contra algum erro genérico.
Suponha que você esteja validando a funcionalidade seu método que
valida uma senha e você quer ver se forçando um erro de ponteiro
inválido o método estará retornando erro. Os possíveis retornos de erro
são:
C
1
2
3
#define ERR_TAMANHO_INVALIDO (-1)
#define ERR_PONTEIRO_INVALIDO (-2)
#define ERR_SENHA_INVALIDA (-3)
E no seu teste unitário você está testando, sem se dar conta, de que está
acontecendo um possível falso positivo. Ou seja, você conseguiu forçar o
erro, porém, você não está testando contra o erro específico que você
está procurando, mas sim contra qualquer erro. Abaixo é exibido um
exemplo simples:
C
1 assert(valida_senha(ponteiro_do_buffer, tamanho_buffer) < 0);
Se sua função possuir algum bug, e se você testar contra o erro que de
fato deveria ser testado, pode ser que o erro não seja o mesmo que você
espera por algum problema de implementação. Para evitar estes falsos
positivos, é ideal sempre testar contra exatamente aquilo que se está
esperando. Abaixo segue o exemplo correto, onde o método é testado
contra todos seus possíveis códigos de erro.
C
1
2
3
assert(valida_senha(ponteiro_do_buffer, tamanho_buffer) == ERR_TAMANHO_INVALIDO);
assert(valida_senha(ponteiro_do_buffer,tamanho_buffer) == ERR_PONTEIRO_INVALIDO);
assert(valida_senha(ponteiro_do_buffer, tamanho_buffer) == ERR_SENHA_INVALIDA);
FUNÇÕES QUE INICIALIZAM DEVEM SEGUIR O CONCEITO DE
ATOMICIDADE
Quando criamos alguma função do tipo inicia_recurso() que inicializa
algum recurso ou módulo, devemos nos preocupar nos aspectos de
atomicidade. Isto é, a função inicia_recurso() deve possuir apenas dois
estados: (1) executado com sucesso; (2) não executado. Ou seja, se ela
falhar em sua execução, ela deve retornar ao estado em que se ela nunca
tivesse sido executada/invocada. É necessário que a função internamente
garanta que em caso de falha, ela desaloque qualquer recurso que tenha
sido alocado durante sua execução. Assim ela retorna em um estado
seguro, mesmo em caso de falha.
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct ctx {
char *nome;
int log;
} ctx_t;
int inicia_log(char *nome) {
int index_log;
Index_log = cria_arquivo_log(nome);
If (index_log <= 0) {
syslog (LOG_INFO, "Arquivo de log nao iniciado");
} else {
syslog (LOG_INFO, "Sucesso ao criar arquivo de log");
}
return index_log;
}
bool inicia_daemon(ctx_t *ctx) {
static const char *string = "daemon_app";
ctx->nome = malloc(strlen(string));
if (ctx->nome == NULL) {
return false;
}
strncpy(ctx->nome, string, strlen(string));
ctx->log = inicia_log(ctx->nome);
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
if(ctx->log) {
free(ctx->nome);
ctx->nome = NULL;
return false;
}
return true;
}
int main()
{
ctx_t ctx;
bool resp;
resp = inicia_daemon(&ctx);
return resp ? 0 : -1;
}
No exemplo acima, percebemos que, em caso de falhas intermediárias da
função inicia_daemon(), ela mesmo se encarregará de desalocar todos os
recursos que foram alocados até então, inclusive limpando o ponteiro que
havia sido atribuído em ctx->nome. Desta forma é possível garantir que
em qualquer falha de métodos inicializadores, eles serão executados
atomicamente pelo seu código.
PONTO ÚNICO DE RETORNO
Este estilo de código requer bastante conhecimento do código para que
seja seguro utilizar este estilo de programação pois, para ser efetivo, o
código deve permitir o erro em cascata, isto é, a propagação de erros
dentro da função não irá quebrar o código. No exemplo anterior, não
poderíamos implementar este método pois a alocação de memória para
armazenar o nome do daemon irá determinar se a próxima instrução
pode ou não ser executada. Desta forma, aplicando o ponto único de
retorno neste código iria nos gerar o tão temível segmentation fault por
acesso indevido à região de memória. Em algumas situações ele é
bastante útil para que possamos entender o que aconteceu de errado no
fluxo de código. Abaixo segue um exemplo simples de como pode-se
realizar erro em cascata tirando vantagem do ponto único de retorno:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include “valida_campos.h” /* header das funcoes valida_(*) */
#define NO_ERROR (0)
#define ERR_VAL_MAC (1 << 1)
#define ERR_VAL_SERIAL (1 << 2)
#define ERR_VAL_IP (1 << 3)
char valida_conexao(char **buff) {
char ret_val = NO_ERROR;
if(valida_mac(buff[0]) != NO_ERROR)
ret_val |= ERR_VAL_MAC;
if(valida_serial(buff[1]) != NO_ERROR)
ret_val |= ERR_VAL_SERIAL;
if(valida_ip(buff[2]) != NO_ERROR)
ret_val |= ERR_VAL_IP;
return ret_val;
}
int main() {
char mascara_erro = 0;
char **valores = { "AA:BB:CC:DD:EE:FF", "123456", "127.0.0.1"};
mascara_erro = valida_conexao(valores);
verifica_erro(mascara_erro);
return 0;
36 }
BOAS PRÁTICAS DE PROGRAMAÇÃO – UTILIZAÇÃO DE GOTO NEM
SEMPRE É TEMÍVEL
Muitos são totalmente contra a utilização de GOTOs nos código, porém
vários artigos propõem sua utilização (inclusive muitos goto são vistos no
Linux kernel) de forma correta. No próprio exemplo anterior, quando não
é possível realizar o cascateamento de erro por conta de alocação de
memória por exemplo, a utilização de goto é bem empregada. Vamos
supor que os métodos valida_* realizam alocações de memória, e que
estas alocações são dependentes entre métodos, isto é, a alocação que é
realizada internamente por validate_mac irá determinar que
a validate_serial possa ser executada ou não.
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
char valida_conexao(char **buff) {
char ret_val = NO_ERROR;
if(valida_mac(buff[0]) != NO_ERROR) {
ret_val = ERR_VAL_MAC;
goto invalid_mac;
}
if(valida_serial(buff[1]) != NO_ERROR) {
ret_val = ERR_VAL_SERIAL;
goto invalid_serial;
}
if(valida_ip(buff[2]) != NO_ERROR) {
ret_val = ERR_VAL_IP;
goto invalid_ip;
}
invalid_ip:
free_ip(buff[2]);
22
23
24
25
26
27
28
invalid_serial:
free_serial(buff[1]);
invalid_mac:
free_mac(buff[0]);
return ret_val;
}
Neste exemplo, vimos que os gotos seguem a ordem inversa das
execuções das funções que validam os respectivos valores. Essa inversão
ocorre pois, se a verificação falha no seu último passo, todos os passos
anteriores devem ser desfeitos. Assim, a desalocação de memória
realizada pelos passos intermediários também é desfeita, evitando
vazamento de memória.
OS PERIGOS DE NÃO SE UTILIZAR CHAVE
Muitos desenvolvedores não utilizam chaves em declarações simples pois
não há necessidade de proteger com chaves uma única linha de código.
No Linux kernel e em muitos outros códigos open-source utilizam este
paradigma de declarações com blocos de código de única linha não são
protegidos com chaves. Porém, este tipo de regra é bastante perigosa
para o desenvolvimento de sistemas embarcados, e aqui vão alguns
exemplos que pode custar ao desenvolvedor horas de depuração, ou
custar até mesmo uma nova planta.
C
1
2
3
4
5
6
7
8
9
10
11
int loop_principal() {
int ret = NO_ERROR;
if (verifica_nivel(tanque) >= VALOR_MAXIMO) // verifica nivel maximo
dispara_alarme();
ret |= loop_medicao();
ret |= motor_ligado();
return ret;
}
Supondo então que o desenvolvedor apagou alguma parte do
comentário, e sem perceber, a linha de código dispara_alarme() acaba
subindo de linha, e fazendo parte do comentário.
C
1
2
3
4
5
6
7
8
9
int loop_principal() {
int ret = NO_ERROR;
if (verifica_nivel(tanque) >= VALOR_MAXIMO) // verifica nivel dispara_alarme();
ret |= loop_medicao();
ret |= motor_ligado();
return ret;
}
Este é um dos erros mais inocentes que pode acontecer, mas como visto
no exemplo, pode-se nunca mais disparar o alarme e além disso,
o loop_medicao() pode vir a ser executado somente quando o nível está
igual ou maior que valor limite. Este é um pequeno erro, onde o
desenvolvedor resolveu apagar uma pequena parte do comentário e, por
causa de um descuido, a linha abaixo do comentário subiu e fez parte do
próprio comentário.
Por mais insignificante que este descuido possa parecer, suas
consequências são graves e sim, este tipo de erro pode acontecer com
mais frequência do que imaginamos.Para evitar isso, procure comentar
código sempre com blocos de comentários /* comentario aqui */, desta
forma você está garantindo que apenas o que está dentro do bloco será
seu comentário.
PROGRAMAÇÃO EM ÚNICA LINHA
Quando aprendemos a utilizar os operadores ternários, acabamos
querendo realizar inúmeras operações em uma única linha. Parece mais
eficiente visualmente falando, porém, isso afeta diretamente a
legibilidade do código e, por consequência, afeta o desempenho durante
alguma depuração de possível bug.
C
1
2
3
int adicionar_atualizar_usuario(char *nome, char flag_adiciona) {
return (cria_usuario(nome, grupo(flag_adiciona == 'Y')) ? move_usuario() : remove_usuario());
}
É de se admitir que em uma única linha de código, conseguimos escrever
o que se precisaria de talvez 4 ou 5 linhas. Talvez este tipo de código
segue a filosofia de que quanto menos linhas de código, menos bugs, mas
vamos comparar com o código abaixo, onde o mesmo código acima foi
escrito, porém em linhas separadas prezando pela legibilidade.
C
1
2
3
4
5
6
7
8
9
10
int adicionar_atualizar_usuario(char *nome, char flag_adiciona) {
int ret = 0;
bool adiciona = (flag_adiciona == 'Y');
ret = cria_usuario(nome, grupo(adiciona));
adiciona ? move_usuario : remove_usuario();
return ret;
}
Legibilidade é fator fundamental para o desenvolvimento, e por mais
claro que as instruções dessa linha possam ser, qualquer outro
desenvolvedor terá que gastar alguns minutos tentando entender o fluxo
de execução desta linha.
COMENTAR O ÓBVIO
Os famosos fall-through de switch cases são geradores de dor de cabeça
para quem está lendo ou revisando algum código. Erros em switch cases
são bastante comuns pelo esquecimento de breaks que acabam fazendo
com que um case passe por dentro de outro, porém, algumas vezes o
desenvolvedor realmente deseja isso. Contudo, se nos depararmos com
esta situação, e nenhum comentário é vinculado a isso, certamente
iremos achar que o desenvolvedor esqueceu de colocar o break e
podemos sem querer querendo alterar toda a lógica do código. Por isso, é
muito importante comentar inclusive o óbvio, para evitar problemas
simples, mas que geram resultados catastróficos.
C
1
2
3
4
5
6
7
8
9
10
11
12
switch(selecao) {
case A:
rotaciona_90x();
break;
case B:
rotaciona_90y();
break;
case C:
rotaciona_90y();
default:
rotaciona_90z();
}
Olhando o código acima, podemos pensar que o desenvolvedor esqueceu
de colocar o break no case C. Então, decidimos colocar o break que está
faltando. Isso irá evitar que o rotaciona_90z() seja executado, e o braço
do robô irá agir de maneira errada. Ou seja, por interpretação errada de
outro desenvolvedor, agora o sistema está com bug. Para evitar isso, um
simples comentário bastaria para evidenciar a necessidade do fall-
through.
C
1
2
3
4
5
case C:
rotaciona_90y();
/* Fall-through: precisamos rotacionar o eixo Z */
default:
rotaciona_90z();
BOAS PRÁTICAS DE PROGRAMAÇÃO – NÃO ECONOMIZE TEMPO AO
DOCUMENTAR CÓDIGO
Quando se desenvolve código em empresa ou para a comunidade, é
importante zelar pela documentação do código para que outros possam
entender o que ele realiza sem precisar de grande conhecimento de suas
entranhas. Ser claro ao documentar é tão importante quanto se
preocupar em desenvolver um código limpo e eficiente, portanto seja
explícito quanto aos parâmetros de entrada e saída de sua função/rotina,
etc. Se sua função requer ponteiros, comente também se os ponteiros são
desalocados em caso de falha, entre outros recursos que a função possa
alocar.
C
1
2
3
4
5
6
7
8
9
/**
* \brief Valida se o nivel esta de acordo
* \param endereco_tanque Ponteiro para o tanque desejado
* \param parametros Parametros do tanque solicitado
* \param nivel Nivel maximo permitido
* \return Retorna TRUE em caso de nivel OK.
* Retorna FALSE caso o nivel esteja acima
*/
bool validar_nivel(char *endereco_tanque, char **parametros, int nivel);
Neste exemplo acima, a documentação não diz qualquer informação
sobre como os ponteiros são tratados. Fica assim a cargo do
desenvolvedor de analisar o código (se possível), ou ir na tentativa e erro
para descobrir se os ponteiros devem ser desalocados externamente ou
não.
Agora, um exemplo com uma boa documentação é mostrada:
1
2
3
4
5
6
7
8
9
10
11
/**
* \brief Valida se o nivel esta de acordo
* \param endereco_tanque Ponteiro para o tanque desejado [IN]
* \param parametros Parametros do tanque solicitado [IN][OUT]
* \param nivel Nivel maximo permitido
* \return Retorna TRUE em caso de nivel OK, e as informacoes do tanque sao
* armazenadas no ponteiro "parametros".
* Retorna FALSE caso o nivel esteja acima do especificado.
*
* \info A memoria alocada "por parametros" deve ser desalocada pelo usuario.
*/
12 bool validar_nivel(char *endereco_tanque, char **parametros, int nivel);
Assim, um código bem documentado possui muito mais valor. Embora
seja preciso dedicar tempo para documentação, o próprio desenvolvedor
se beneficia de seus comentários, não precisando ele mesmo retornar ao
código para lembrar seu funcionamento interno.
(EXTRA) PRECAUÇÕES EM CÓDIGO SEGURO
Uma questão importante relacionado ao estilo de código é buscar por
código seguro. Ao utilizarmos funções da stdlib por exemplo, podemos
verificar as maneiras corretas e seguras de utilizá-las. Existem
comunidades especializadas em investigar e definir padrões de
programação que garantem a segurança do código.
Um website para consulta de exemplos de código seguro é encontrado
em aqui. O CERT Secure Coding Standards é formado por grupos de
desenvolvedores que tentam explorar vulnerabilidades da linguagem C e
C++, dando exemplos reais e soluções “ideais” para serem utilizadas.
Portanto, sempre que formos utilizar alguma função conhecida, como
atoi(), é extremamente recomendável procurar no CERT sobre ela e ver o
que eles têm a dizer, quais são suas vulnerabilidades e o que pode se
utilizar em sua substituição.
O padrão MISRA C define um conjunto de regras para a linguagem de
programação em C que visa garantir padrões seguros de
desenvolvimento. Este padrão foi desenvolvido para o desenvolvimento
de sistemas embarcados para automóveis, porém ganhou popularidade e
atualmente é adotado nas mais diversas áreas de desenvolvimento por
conta de seus benefícios.
Existem inúmeros outros lugares e comunidades que procuram
exemplificar e apresentar soluções de situações reais em que códigos
podem ser melhorados tanto em questão de performance quanto em
questão de segurança. Estar atento a estes exemplos torna-se bastante
útil para que o desenvolvedor desenvolva melhor sua capacidade de
desenvolvimento de código nestes aspectos.
CONCLUSÃO – BOAS PRÁTICAS DE PROGRAMAÇÃO EM LINGUAGEM C
https://www.securecoding.cert.org/
Tentamos abordar aqui situações simples, mas que exemplificam a
realidade e os cuidados necessários quando estamos desenvolvendo
código. É uma boa iniciativa pensarmos de maneira não tão pragmática,
mas um pouco filosófica em se desenvolver sistemas embarcados. O estilo
de código não está relacionado a apenas a forma com que você escreve o
código, mas também a forma em que ele é desenvolvido e as precauções
utilizadas para torná-lo mais seguro. Portanto, o desenvolvimento de
código também envolve questões filosóficas que determinam a maneira
em que adotamos para desenvolver código e, consequentemente isso
atinge nossa produtividade de maneira positiva ou negativa dependendo
do que utilizamos como desenvolvimento. Se basear por estilos utilizados
pela comunidade é um bom exercício, inclusive para quem está iniciandono desenvolvimento de software e gostaria de estar mais familiarizado
com o “mundo real” de desenvolvimento.