Skip to content

Posts

4 Práticas para Reduzir Sua Taxa de Defeitos

17 de novembro de 2015 • 9 min de leitura

4 Práticas para Reduzir Sua Taxa de Defeitos

Escrever software é uma batalha entre complexidade e simplicidade. Encontrar o equilíbrio entre os dois é difícil. O trade-off é entre métodos longos e não mantíveis e muita abstração. Inclinar-se demais em qualquer direção prejudica a legibilidade do código e aumenta a probabilidade de defeitos.

Os defeitos são evitáveis? A NASA tenta, mas eles também fazem carradas de testes. Seu software é literalmente crítico para a missão – uma única chance. Para a maioria das organizações, este não é o caso e grandes quantidades de testes são custosas e impraticáveis. Embora não haja substituto para testes, é possível escrever código resistente a defeitos, sem testes.

Em 20 anos codificando e arquitetando aplicações, identifiquei quatro práticas para reduzir defeitos. As duas primeiras práticas limitam a introdução de defeitos e as duas últimas práticas expõem defeitos. Cada prática é um tópico vasto por si só sobre o qual muitos livros foram escritos. Destilei cada prática em alguns parágrafos e forneci links para informações adicionais quando possível.

1. Escreva Código Simples

Simples deveria ser fácil, mas não é. Escrever código simples é difícil.

Alguns lerão isso e pensarão que isso significa usar recursos simples da linguagem, mas este não é o caso — código simples não é código burro.

Para manter a objetividade, estou usando complexidade ciclomática como medida. Existem outras maneiras de medir complexidade e outros tipos de complexidade, espero explorar esses tópicos em artigos futuros.

A Microsoft define complexidade ciclomática como:

A complexidade ciclomática mede o número de caminhos
linearmente independentes através do método, que é determinado
pelo número e complexidade de ramificações condicionais. Uma baixa
complexidade ciclomática geralmente indica um método que é fácil
de entender, testar e manter.

O que é uma baixa complexidade ciclomática? A Microsoft recomenda manter a complexidade ciclomática abaixo de 25.

Para ser honesto, achei a recomendação da Microsoft de complexidade ciclomática de 25 muito alta. Para manutenibilidade e complexidade, descobri que o tamanho ideal do método é entre 1 a 10 linhas com uma complexidade ciclomática entre 1 e 5.

Bill Wagner em Effective C#, Second Edition escreveu sobre tamanho de método:

Lembre-se de que traduzir seu código C# em código executável pela máquina é um processo de duas etapas. O compilador C# gera IL que é entregue em assemblies. O compilador JIT gera código de máquina para cada método (ou grupo de métodos, quando inlining está envolvido), conforme necessário. Funções pequenas tornam muito mais fácil para o compilador JIT amortizar esse custo. Funções pequenas também são mais propensas a serem candidatas para inlining. Não é apenas pequenez: Fluxo de controle mais simples importa tanto quanto. Menos ramificações de controle dentro de funções tornam mais fácil para o compilador JIT registrar variáveis. Não é apenas boa prática escrever código mais claro; é como você cria código mais eficiente em tempo de execução.

Para colocar a complexidade ciclomática em perspectiva, o seguinte método tem uma complexidade ciclomática de 12.

public string ComplexityOf12(int status)
{
    var isTrue = true;
    var myString = "Chuck";

    if (isTrue)
    {
        if (isTrue)
        {
            myString = string.Empty;
            isTrue = false;

            for (var index = 0; index < 10; index++)
            {
                isTrue |= Convert.ToBoolean(new Random().Next());
            }

            if (status == 1 || status == 3)
            {
                switch (status)
                {
                    case 3:
                        return "Bye";
                    case 1:
                        if (status % 1 == 0)
                        {
                            myString = "Super";
                        }
                        break;
                }

                return myString;
            }
        }
    }

    if (!isTrue)
    {
        myString = "New";
    }

    switch (status)
    {
        case 300:
            myString = "3001";
            break;
        case 400:
            myString = "4003";
            break;

    }

    return myString;
}

Uma hipótese de complexidade geralmente aceita postula que existe uma correlação positiva entre complexidade e defeitos.

A linha anterior é um pouco complicada. Nos termos mais simples — manter o código simples reduz sua taxa de defeitos.

2. Escreva Código Testável

Estudos mostraram que escrever código testável, sem escrever os testes reais, reduz as incidências de defeitos. Isso é tão importante e profundo que precisa ser repetido: Escrever código testável, sem escrever os testes reais, reduz as incidências de defeitos.

Isso levanta a questão, o que é código testável?

Defino código testável como código que pode ser testado isoladamente. Isso significa que todas as dependências podem ser mockadas a partir de um teste. Um exemplo de dependência é uma consulta ao banco de dados. Em um teste, os dados são mockados (falsificados) e uma asserção do comportamento esperado é feita. Se a asserção for verdadeira, o teste passa, se não, falha.

Escrever código testável pode parecer difícil, mas, na verdade, é fácil quando seguindo a Inversão de Controle (Injeção de Dependência) e os princípios S.O.L.I.D. Você ficará surpreso com a facilidade e se perguntará por que demorou tanto para começar a escrever dessa forma.

3. Revisões de Código

Uma das práticas mais impactantes que uma equipe de desenvolvimento pode adotar são as revisões de código.

As Revisões de Código facilitam o compartilhamento de conhecimento entre desenvolvedores. Falando por experiência, discutir abertamente código com outros desenvolvedores teve o maior impacto nas minhas habilidades de escrita de código.

No livro Code Complete, por Steve McConnell, Steve fornece numerosos estudos de caso sobre os benefícios das revisões de código:

  • Um estudo de uma organização na AT&T com mais de 200 pessoas relatou um aumento de 14% na produtividade e uma diminuição de 90% nos defeitos após a organização introduzir revisões.
    • A Aetna Insurance Company encontrou 82% dos erros em um programa usando inspeções e conseguiu diminuir seus recursos de desenvolvimento em 20%.
    • Em um grupo de 11 programas desenvolvidos pelo mesmo grupo de pessoas, os primeiros 5 foram desenvolvidos sem revisões. Os 6 restantes foram desenvolvidos com revisões. Após todos os programas serem lançados em produção, os primeiros 5 tiveram uma média de 4,5 erros por 100 linhas de código. Os 6 que foram inspecionados tiveram uma média de apenas 0,82 erros por 100. As revisões reduziram os erros em mais de 80%.

Se esses números não te convencem a adotar revisões de código, então você está destinado a derivar para um buraco negro enquanto canta Johnny Paycheck’s Take This Job and Shove It.

4. Testes Unitários

Admito, quando estou contra um prazo, testes são a primeira coisa a ser abandonada. Mas os benefícios dos testes não podem ser negados, como os seguintes estudos ilustram.

A Microsoft realizou um estudo sobre a Efetividade dos Testes Unitários. Eles descobriram que codificar a versão 2 (a versão 1 não tinha testes) com testes automatizados imediatamente reduziu defeitos em 20%, mas a um custo adicional de 30%.

Outro estudo analisou o Desenvolvimento Orientado por Testes (TDD). Eles observaram um aumento na qualidade do código, mais de duas vezes, comparado a projetos similares não usando TDD. Projetos TDD levaram em média 15% mais tempo para desenvolver. Um efeito colateral do TDD foi que os testes serviram como documentação para as bibliotecas e APIs.

Por último, em um estudo sobre Cobertura de Testes e Defeitos Pós-Verificação:

… Descobrimos que em ambos os projetos o aumento na cobertura de
testes está associado com diminuição em problemas reportados em campo
quando ajustado pelo número de mudanças pré-lançamento…

Um Exemplo

O seguinte código tem uma complexidade ciclomática de 4.

    public void SendUserHadJoinedEmailToAdministrator(DataAccess.Database.Schema.dbo.Agency agency, User savedUser)
    {
        AgencySettingsRepository agencySettingsRepository = new AgencySettingsRepository();
        var agencySettings = agencySettingsRepository.GetById(agency.Id);

        if (agencySettings != null)
        {
            var newAuthAdmin = agencySettings.NewUserAuthorizationContact;

            if (newAuthAdmin.IsNotNull())
            {
                EmailService emailService = new EmailService();

                emailService.SendTemplate(new[] { newAuthAdmin.Email }, GroverConstants.EmailTemplate.NewUserAdminNotification, s =>
                {
                    s.Add(new EmailToken { Token = "Domain", Value = _settings.Domain });
                    s.Add(new EmailToken
                    {
                        Token = "Subject",
                        Value =
                    string.Format("New User {0} has joined {1} on myGrover.", savedUser.FullName(), agency.Name)
                    });
                    s.Add(new EmailToken { Token = "Name", Value = savedUser.FullName() });

                    return s;
                });
            }
        }
    }

Vamos examinar a testabilidade do código acima.

Este é código simples?

Sim, é, a complexidade ciclomática está abaixo de 5.

Existem dependências?

Sim. Existem 2 serviços AgencySettingsRepository e EmailService.

Os serviços são mockáveis?

Não, sua criação está oculta dentro do método.

O código é testável?

Não, este código não é testável porque não podemos mockar AgencySettingsRepository e EmailService.

Exemplo de Código Refatorado

Como podemos tornar este código testável?

Injetamos (usando injeção por construtor) AgencySettingsRepository e EmailService como dependências. Isso nos permite mocká-los a partir de um teste e testar isoladamente.

Abaixo está a versão refatorada.

Note como os serviços são injetados no construtor. Isso nos permite controlar qual implementação é passada para o construtor SendMail. É então fácil passar dados fictícios e interceptar as chamadas dos métodos de serviço.

public class SendEmail
{
    private IAgencySettingsRepository _agencySettingsRepository;
    private IEmailService _emailService;


    public SendEmail(IAgencySettingsRepository agencySettingsRepository, IEmailService emailService)
    {
        _agencySettingsRepository = agencySettingsRepository;
        _emailService = emailService;
    }

    public void SendUserHadJoinedEmailToAdministrator(DataAccess.Database.Schema.dbo.Agency agency, User savedUser)
    {
        var agencySettings = _agencySettingsRepository.GetById(agency.Id);

        if (agencySettings != null)
        {
            var newAuthAdmin = agencySettings.NewUserAuthorizationContact;

            if (newAuthAdmin.IsNotNull())
            {
                _emailService.SendTemplate(new[] { newAuthAdmin.Email },
                GroverConstants.EmailTemplate.NewUserAdminNotification, s =>
                {
                    s.Add(new EmailToken { Token = "Domain", Value = _settings.Domain });
                    s.Add(new EmailToken
                    {
                        Token = "Subject",
                        Value = string.Format("New User {0} has joined {1} on myGrover.", savedUser.FullName(), agency.Name)
                    });
                    s.Add(new EmailToken { Token = "Name", Value = savedUser.FullName() });

                    return s;
                });
            }
        }
    }
}

Exemplo de Teste

Abaixo está um exemplo de teste isolado. Estamos usando o framework de mock FakeItEasy.

    [Test]
    public void TestEmailService()
    {
        //Given

        //Using FakeItEasy mocking framework
        var repository = A<IAgencySettingsRepository>.Fake();
        var service = A<IEmailService>.Fake();

        var agency = new Agency { Name = "Acme Inc." };
        var user = new User { FirstName = "Chuck", LastName = "Conway", Email = "chuck.conway@fakedomain.com" }

        //When

        var sendEmail = new SendEmail(repository, service);
        sendEmail.SendUserHadJoinedEmailToAdministrator(agency, user);


        //Then
        //An exception is thrown when this is not called.
        A.CallTo(() => service.SendTemplate(A<Agency>.Ignore, A<User>.Ignore)).MustHaveHappened();

    }

Conclusão

Escrever código resistente a defeitos é surpreendentemente fácil. Não me entenda mal, você nunca escreverá código livre de defeitos (se descobrir como, me avise!), mas seguindo as 4 práticas descritas neste artigo você verá uma diminuição nos defeitos encontrados no seu código.

Para recapitular, Escrever Código Simples é manter a complexidade ciclomática em torno de 5 e o tamanho do método pequeno. Escrever Código Testável é facilmente alcançado quando seguindo a Inversão de Controle e os Princípios S.O.L.I.D. Revisões de Código ajudam você e a equipe a entender o domínio e o código que você escreveu — apenas ter que explicar seu código revelará problemas. E por último, Testes Unitários podem melhorar drasticamente a qualidade do seu código e fornecer documentação para futuros desenvolvedores.

Autor: Chuck Conway é especialista em engenharia de software e IA Generativa. Conecte-se com ele nas redes sociais: X (@chuckconway) ou visite-o no YouTube.

↑ Voltar ao topo

Você também pode gostar