top of page

Testes de Software


ree


Oi, faz um tempo que não apareço por aqui, né? Desculpa, algumas coisas entraram no caminho, mas agora estou voltando com tudo (eu acho) e com um tópico que sempre cria polêmica entre nós programadores, os tão temidos testes. Esse post não será exclusivamente sobre Salesforce, e os tópicos específicos estarão no fim do post caso queira pular, mas sugiro que leia todo o post, afinal os conceitos serão construídos com base no que veremos a seguir!


Porque testar?

"Pra que testar isso? É muito simples!" - Todo dev, pelo menos uma vez na vida

A resposta para essa pergunta é bastante simples e complexa ao mesmo tempo. Para os desenvolvedores Salesforce a resposta simples é "Um mínimo de 75% de code coverage é necessário para deploy.", para outras stacks, acaba variando entre equipes e empresas.

A resposta mais complexa é que mesmo que seja uma função bastante simples, como uma função que recebe dois números e os soma, testar garante integridade. Uma vez que essa função é chamada dentro de outra função, dentro de uma terceira função, perdemos a visibilidade dela, e um erro nela pode causar bugs muito difíceis de encontrar. Agora tire isso desse contexto reducionista e coloque em um contexto de uma biblioteca. Entende o como isso cresce e fica mais complexo?


Porque escrever bons testes? O que é um bom teste?

Primeiramente, o que é um bom teste?

Eu costumo dizer que um bom teste, não necessariamente é aquele que cobre 100% da classe que está sendo testada, mas sim, aquele que não cobre só o "happy path" onde tudo é perfeito e nada da errado. Um bom teste é aquele que cobre exceções, erros e até edge cases quando conhecidos. Cobrir 100% é bom? Claro que é, mas não é uma tarefa simples e não é uma métrica perfeita de segurança.

Isso posto, já temos uma das respostas do porque escrever bons testes. Com testes bem escritos sempre teremos certezas de que aquele pedaço de código está correto e livre de bugs.

Outra resposta é garantir qualidade. Muitas vezes terceirizamos essa responsabilidade para o QA da equipe, mas e quando não temos esse profissional? Ou pior, e quando essa pessoa está atolada em tarefas, como ajudar ela a não perder tempo? Um bom teste, ajuda a garantir que os testes de QA sejam mais assertivos e mais a fundo, porque o simples já é testado pelo próprio código. (Não que isso libere o QA da responsabilidade de testar o básico, mas isso é outra história.)

Provavelmente existem mil outras respostas, mas a última que vou citar aqui é que um teste bem escrito te ajuda a entender o código. Seja ele do seu colega ou seu, ler código depois de algum tempo sempre é mais difícil. Um bom teste te mostra o que acontece no código e como, o código será responsável por te mostrar parte do como e a razão.


"Quando eu escrevi esse código, só eu e Deus sabíamos o que ele fazia. Hoje, só Deus sabe."Meme entre desenvolvedores

Requisitos

Os requisitos normalmente são levantados no momento que a história é detalhada, assumindo um modelo de SCRUM. Quando levantamos o que será necessário fazer para concluir a história, estamos levantando requisitos que irão nos ajudar a desenvolver os testes posteriormente. Testes não se limitam a testes unitários que fazemos para garantir funcionalidade dos métodos de uma classe, existem diferentes tipos de testes, que serão abordados mais a frente, que garantem que todos os requisitos foram satisfeitos. Por essa razão, ressalto a importância da comunicação aberta com o profissional de QA, e a participação deste em cerimônias de detalhamento/refinamento.


O que testar?

Resposta curta: tudo.

Resposta longa: tudo o que for possível de ser testado que esteja no escopo da sua tarefa. Se ela é apenas criar uma classe, sem implementar nada além disso, então apenas o teste unitário deve bastar. Se sua tarefa envolve a implementação dessa classe, teste os critérios de aceite da mesma, documente um teste manual, que comprove a funcionalidade da implementação. Uma série de novas implementações que pode introduzir bugs em funcionalidades já desenvolvidas? Testes de regressão também serão necessários.

Mas Jônatas, não tenho tempo de fazer tudo isso.

Em parte eu concordo, na verdade sou o primeiro a admitir que se você abrir meu github não verá tantos testes assim. Mas ao mesmo tempo, entendo que quando bem alinhado com a equipe, testes consomem tempo para serem escritos, e consomem bastante tempo para serem bem planejados e bem escritos, mas eles economizam uma quantidade absurda (para não dizer imensurável) de tempo em resolução de problemas no futuro. Pense comigo por um momento, se você, em ambiente de desenvolvimento, cria uma regressão, sem saber, e isso passa para produção, quanto tempo você vai precisar para encontrar o erro, a causa do erro e corrigir ele? Pior, e se for um daqueles casos raros que acontecem sempre onde um o comportamento do erro é inconsistente? Agora, sabendo que uma regressão pode ser criada (normalmente o arquiteto, tech lead, ou de certa forma o PO deve saber dessa possibilidade e informar a equipe, o que não exime os outros de levantarem a suspeita quando pertinente) e escrevendo um teste para isso, se quebrar em dev, a alteração não passa para os próximos ambientes e evita essa dor de cabeça.


Tipos de Testes

Existe diversos tipos e níveis de testes. Não abordarei todos aqui, mas a seção de referências desse post pode te ajudar. Vou abordar aqui os mais comuns no nosso dia a dia como dev.


Unit Testing

Também conhecido como "component test", "module test" ou "program test" esse é o primeiro nível de testes. É o nível mais comum para nós desenvolvedores, simplesmente criamos um teste que resta a nosso código. Ele garante que nossa função de soma não subtraia, por exemplo.

Geralmente os testes unitários são feitos paralelamente ao código que eles devem testar. Podem ser escritos antes, e seguir uma metodologia de Test Driven Development ou Behaviour Driven Development, ou depois do código estar pronto.

Esses testes nos ajudam a encontrar bugs e corrigir antes que passe para a fase de QA e geralmente os bugs encontrados nessa situação são mais "informais" e não são registrados em ferramentas de gestão de tarefas como Jira.


Integration Testing

Aqui já saímos um pouco do universo do desenvolvedor e entramos no QA. Esse tipo de teste abrange a interação (integração) de duas ou mais unidades de software. Por exemplo. Você quer abrir o organograma da sua empresa, você:

  1. Faz a busca por uma pessoa específica

  2. Clica no botão do organograma no card da pessoa.

  3. Assim é algo demasiadamente simples, mas temos um sistema de busca em ação, integrado ao carregamento de um card, que é populado pela busca feita anteriormente. Ao abrir o organograma uma nova tela é carregada, novas informações devem ser buscadas e impressas na tela em um formato específico.


Existe um mapeamento que o responsável por esse teste deve fazer para entender cada passo que deve ser feito e cada critério que deve ser atendido. E reforçando o que foi dito anteriormente, o teste de integração, nada mais é do que o teste de múltiplas unidades, garantido que todas trabalhem em harmonia. Normalmente esses testes são planejados e desenvolvidos por profissionais de QA, ou uma ação conjunta de QA e desenvolvddor.

Testes de integração são divididos em dois níveis:


Teste de integração de componentes

São testes que testam um conjunto de componentes, por exemplo uma página de produtos em um site de compras e a interação com o "comprar agora" e o carrinho.


Teste de integração de sistemas

Esse nível foca na interação entre diferentes sistemas, como o consumo de uma API e a impressão correta de sua resposta na tela.


System Testing

O teste de sistema é o terceiro nível de testes e visa testar todo o sistema em diferentes ambientes. Seu objetivo é garantir que o sistema atende os requisitos propostos. Usualmente é o último teste relativo ao desenvolvimento para verificar se este atende as especificações e está tão livre de defeitos quanto possível.

Esses testes devem investigar requisitos funcionais e não funcionais da aplicação e geralmente são feitos por um tester independe de um desenvolvedor. Para evitar imprevistos, esses testes devem ser feitos em um ambiente tão próximo do real quanto possível, aí que entra nosso querido ambiente de Homologação/UAT.

Esse nível se define em alguns passos:

  1. Baseado em como o produto deve ser usado, são criados casos de uso.

  2. Para cada caso de uso, um ou mais cenários são definidos.

  3. Casis de teste são escritos para casa cenário.

Acceptance Testing

O último tipo de teste que cobriremos aqui é o quarto nível de testes e o último a ser feito. Uma vez que os testes anteriores são aprovados, agora é a vez do cliente/usuário testar. Esse nível é dividido em diversos tipos, que não iremos abordar aqui, mas novamente sugiro que leia as referências desse post caso se interesse pelo assunto.

Quando falamos de testes de aceitação, podemos ser de certa forma simplistas. O foco desses testes é a área de negócios, garantir que o usuário/PO aceita o que foi desenvolvido com todos os critérios atendidos e aceitos.


Especificidades Salesforce

A Salesforce é bastante única em diversos sentidos, e quando falamos de testes, não é diferente. Como dito anteriormente, para deploy de uma classe é necessário pelo menos 75% de cobertura. Além disso, é necessário que os testes sejam anotados com @isTest, sinalizando para a Salesforce que esse arquivo deve poder ser rodado como teste e não deve consumir limite de caracteres. Falando em limites, outro ponto importante é que devemos usar os métodos de startTest e stopTest, para não consumirmos limites da nossa org e também para resetar a contagem de limites.

Vale ressaltar que o ambiente utilizado nas classes de teste não tem massa de dados e é totalmente efêmero, que quer dizer que ele não pode usar seus dados salvos (como contas e contatos) e a massa de dados criada durante a execução não será commitada no banco de dados da org.

Agora para um ponto complicado acerca desses testes, você não é obrigado a usar uma factory para criar os dados utilizados no teste. Você pode manualmente criar eles e inserir no banco de dados de testes, mas é muito mais fácil e rápido utilizar uma factory. Não só são classes que evitam retrabalho, elas também garantem algum nível de consistência. No futuro teremos uma postagem sobre design patterns e nela vou mostrar como esses padrões podem nos auxiliar no desenvolvimento de testes, mas por ora, o que importa, é que você crie a massa de dados em um local anotado com @testSetup, e que mantenha seus dados nessa seção, para garantir integridade. Dessa forma você garante que a cada novo método de teste, o ambiente seja refeito do zero, impedindo que erros de uma execução anterior causem problemas na execução atual.

Existe também a possibilidade de anotar seus testes com seeAllData(true), eu apenas falo dessa opção para deixar o post mais completo, e você saber que existe, mas seu uso não é nada indicado. Essa anotação permite que seu teste veja os dados da sua org e os utilize. Pessoalmente nunca vi uma razão para utilizar esse método. As poucas onde seria possível utilizar como justificativa, construir um mock da resposta era mais aconselhável.

E uma regra geral para testes, não use valores hard coded. Podemos ter um contato "Dino da Silva Sauro" em todos os nossos ambientes e utilizamos ele para testes, porém a ID dele não é a mesma em ambientes diferentes então chumbar valores no seu teste pode quebrar ele.


Como efetuar os testes

Dentro das nossas orgs, temos algumas maneiras de rodar nossas classes de testes, a primeira sendo pelo developer console, com uma classe de testes aberta, ou seguindo o caminho:

Test > New run > selecione os testes e clique em Run

Também é possível criar uma "Test Suite" com múltiplas classes e a rodar pelo mesmo menu.

Também podemos rodar os testes pela definição da classe no setup, ou utilizando o VSCode.


Testes manuais

É importante, antes de liberar para QA, fazer um teste da sua funcionalidade de forma manual. Clique nos novos botões, invoque as novas funções seja lá como for que estiverem configuradas, mas lembre-se de testar se o comportamento do seu desenvolvimento é o esperado. Se possível, documente esse teste com uma gravação de tela ou com prints da tela. Para gravação eu utilizo o OBS Studio, e para prints utilizo o Scribe. O post não é patrocinado por nenhuma dessas ferramentas, mas são bastante simples de se utilizar e bem eficientes.

Você pode se perguntar o porque documentar seus testes, afinal sua função é desenvolver o código e não fazer o trabalho de QA, a razão é bem simples, segurança. Você pode ter feito seu trabalho corretamente, mas alguém sem querer o quebrou e você não soube antes da review, você pode provar que fez todo o passo a passo corretamente, ou estudar seus erros e mais importante, na minha opinião, enviar para o QA, junto com um possível KT sobre o que você fez, como e porque, para que os testes sejam mais assertivos. Eu te garanto, é um trabalho simples, que consome pouco tempo, mas te expõe de uma maneira boa, como uma pessoa responsável e precavida e também ajuda a melhorar a qualidade da sua entrega.


Demonstração

Aqui vou demonstrar um pouco como é uma classe de testes, e como seria o modelo de desenvolvimento utilizando TDD. Por ser só um exemplo, o projeto é bastante simples, apenas quatro funções, para as quatro operações básicas. Todas devem receber 2 números como parâmetro e retornar o resultado correto. Como o principio é o mesmo dentro e fora da Salesforce, vou utilizar JavaScript, com a biblioteca de testes Jest, para simplificar.


Definição de teste utilizando Jest
Definição de teste utilizando Jest

Aqui temos o primeiro passo, uma implementação de uma classe de testes, que falha, visto que não existe um código a ser testado. No Jest, sendo mais simples do que correto, o "Describe" descreve o que deverá ser testado, nesse caso "Calculator". "It" descreve o que cada teste deve fazer, nesse caso, sendo somar, subtrair, multiplicar ou dividir dois números.


Expansão do teste com Jest
Expansão do teste com Jest

Aqui temos a definição completa dos testes, cada um deles tem um "expect" que "espera" que o retorno da função sendo testada seja um determinado valor.


Classe a ser testada
Classe a ser testada

E aqui temos uma definição bastante simples da classe de calculadora com a implementação de cada método.


Terminal demonstrando sucesso no teste
Terminal demonstrando sucesso no teste

Por fim, aqui temos o resultado do Jest no terminal, 1 test suite, que contém 4 testes unitários, todos passara. agora eu poderia refatorar essa classe para que ela pudesse receber mais valores para as contas, ou que ela suportasse mais operações, mas isso fica fora do escopo desse post.


Teste mockado

Utilizamos esse nome para descrever testes que testam coisas como uma chamada de API, e não queremos testar a API, queremos apenas testar se a chamada para ela funciona corretamente, nesse caso, vou utilizar um código meu, de algum tempo atrás, que consome a API do BACEN para pegar o valor da PTAX

Controller

/***
 * Classe responsável por realizar uma chamada para a API do Banco Central do Brasil
 * e retornar a cotação de compra do dólar americano em relação ao real para a data atual ou a última sexta-feira.
 */
public with sharing class ExchangeRateController {
  /**
   * Método que pega o dia atual e volta para a última sexta-feira, caso seja um final de semana.
   * @return Uma string com uma data válida para consumo da API no formato "MM-dd-yyyy"
   * @author Jônatas Lima de Medeiros <jonatas.lima.medeiros@live.com>
   **/
  public static String updateDateTime() {
    BusinessHours bh = [SELECT Id FROM BusinessHours WHERE name = 'Bacen'];
    System.debug('bh: ' + bh);

    DateTime now = DateTime.now();

    Boolean isBusinessHours = BusinessHours.isWithin(bh.id, now);
    System.debug('isBusinessHours: ' + isBusinessHours);

    String formatednow = now.format('E'); // Formata o dia no padrão de três letras. Ex "mon".
    // Caso o dia seja Sábado ou Domingo, remove dias para que volte para Sexta-feira

    if (!isBusinessHours) {
      now = now.addDays(-1);
    }

    if (formatednow == 'Sat') {
      now = now.addDays(-1);
    } else if (formatednow == 'Sun') {
      now = now.addDays(-2);
    }
    String validDate = now.format('MM-dd-yyyy');
    System.debug('##### now: ' + now);
    return validDate;
  }

  /**
   * Método que retorna a PTAX definida pelo Banco Central do Brasil (BACEN)
   * @return O valor da PTAX
   */
  @AuraEnabled(cacheable=true)
  public static Decimal getLastExchangeRate() {
    String validDate = updateDateTime();

    // Monta a URL de chamada para a API usando a data retornada pela função `updateDateTime`
    String endpoint =
      'https://olinda.bcb.gov.br/olinda/servico/PTAX/versao/v1/odata/CotacaoDolarDia(dataCotacao=@dataCotacao)?@dataCotacao=\'' +
      validDate +
      '\'&$top=1&$format=json';
    System.debug('##### endpoint: ' + endpoint);

    // Realiza a chamada para a API do Banco Central do Brasil
    Http http = new Http();
    HttpRequest request = new HttpRequest();
    request.setEndpoint(endpoint);
    request.setMethod('GET');
    HttpResponse response = http.send(request);

    // Verifica se a chamada para a API do BACEN foi bem sucedida
    if (response.getStatusCode() == 200) {
      System.debug('#### response: ' + response);
      Map<String, Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(
        response.getBody()
      );
      System.debug('#### results : ' + responseMap);
      List<Object> values = (List<Object>) responseMap.get('value');
      System.debug('#### value : ' + values);
      Map<String, Object> firstValue = (Map<String, Object>) ((List<Object>) values)
        .get(0);
      // Map<String, Object> firstValue = (Map<String, Object>) values.get(0);
      Decimal ptax = Decimal.valueOf((Double) firstValue.get('cotacaoCompra'));
      System.debug('#### ptax : ' + ptax);
      return ptax;
    } else {
      return null;
    }
  }
}

Mock de sucesso

@isTest
global class ExchangeRateControllerMock implements HttpCalloutMock {
  global HTTPResponse respond(HTTPRequest request) {
    HttpResponse response = new HttpResponse();
    response.setHeader('Content-Type', 'application/json');
    response.setBody(
      '{"value": [{"cotacaoCompra": 5.2493, "cotacaoVenda": 5.2499}]}'
    );
    response.setStatusCode(200);
    return response;
  }
}

Mock de falha

@isTest
global class ExchangeRateControllerMockFailure implements HttpCalloutMock {
  global HTTPResponse respond(HTTPRequest request) {
    HttpResponse response = new HttpResponse();
    response.setHeader('Content-Type', 'application/json');
    response.setBody('{"error": {"code": "404", "message": "Not found"}}');
    response.setStatusCode(404);
    return response;
  }
}

Classe de teste

@isTest
private class ExchangeRateControllerTest {
  @isTest
  static void testGetLastExchangeRateSuccess() {
    // Configura a chamada http
    Test.setMock(HttpCalloutMock.class, new ExchangeRateControllerMock());

    // Chamada do método sendo testado
    Decimal result = ExchangeRateController.getLastExchangeRate();

    // Verifica o resultado
    Decimal expected = 5.2493;
    System.assertEquals(expected, result);
  }

  @isTest
  static void testGetLastExchangeRateFailure() {
    // Configura a chamada http
    Test.setMock(
      HttpCalloutMock.class,
      new ExchangeRateControllerMockFailure()
    );

    // Chamada do método sendo testado
    Decimal result = ExchangeRateController.getLastExchangeRate();

    // Verifica o resultado
    System.assertEquals(null, result);
  }
}

Esse teste envolveu a criação de três classes.

  • Classe de testes

  • Classe de sucesso na requisição

  • Classe de falha na requisição

Porque? Justamente pela razão que citei mais cedo, não devemos testar apenas o caminho onde tudo dá certo. Nessa implementação, não existe um tratamento para o caso de um erro na chamada da API, a classe apenas retorna null, isso foi uma definição de negócios quando fiz a classe, que hoje admito que tem muito espaço para melhora.


Teste com Factory

Existe um Design Pattern chamado Factory, ele serve apenas para criar um objeto. "Como assim?" você pode me perguntar, mas é exatamente isso. É uma classe que cria objetos e os retorna, vou falar mais sobre ela e outros padrões em outro post, mas o que você precisa saber aqui, é que ao invés de fazer um setup de teste com vários dados criados na mão, isso pode ser reduzido utilizando esse padrão.

Usando um exemplo de Salesforce. Você precisa testar uma classe que precisa de 3 contatos, você pode criar os três na mão, sem problema nenhum.

@isTest
public class TestLorem {
	@testSetup static void setup() {
		 List<Account> testAcc = new List<Account>();
		 for(Integer i = 0; i < 3; i++) {
			 testAcc.add(new Account(Name = 'Test Account' + i));
		 }
		 insert testAcc;
	}
}

Ou poderia criar uma classe de factory

public class AccountFactory {
	 public static Account createAccount(String name){
		 Account acc = new Account(name = name);
		 return acc;
	 }
} 

E a classe de teste ficaria

@isTest
public class TestLorem {
	@testSetup static void setup() {
		 List<Account> testAcc = new List<Account>();
		 testAdd.add(AccountFactory.createAccount(1));
		 testAdd.add(AccountFactory.createAccount(2));
		 testAdd.add(AccountFactory.createAccount(3));
		 insert testAcc;
	}
}

Nesse exemplo a diferença é muito pequena, pode até se dizer que mais código foi escrito sem razão, o que seria verdade, mas pense em casos mais complexos, onde se usa record types, existem mais campos obrigatórios, validação de dados, entre outras definições que não poderiam ser criadas tão facilmente dentro de um loop. Aí que entra o a facilidade que esse padrão te permite nessa situação. Para dar um exemplo, em um projeto que trabalhei, nós precisávamos testar algumas ações que precisavam necessariamente de um objeto de "contrato" que por sua vez dependia, entre outras coisas de:

  • Account

  • Contact

  • Ptax

  • Valor do contrato

  • Produtos

  • Data de inicio do contrato

  • Data de fim do contrato

  • Endereço de armazenamento

  • Endereço de produção


Extras

Algumas coisas a mais, relacionadas ao tópico, mas que podem render posts próprios. Caso tenha interesse, mande um ou no LinkedIn e eu planejo um post sobre.


Test Driven Development

Test driven development, muitas vezes abreviado como "TDD" é uma metodologia de desenvolvimento que inverte o nosso padrão mais comum. Primeiro se escreve uma classe de testes, que simplesmente atende os requisitos pedidos, após isso é escrito o código que satisfaz essas condições, fazendo o teste passar, e então, refatoramos o teste e o código, para que ambos sejam mais refinados. Esse padrão é conhecido como "red, green, refactor", pois quando rodamos um teste e ele retorna erro, o console faz o output em vermelho. Quando ele retorna que passou corretamente, sua cor é verde, então:

  1. Red -> Teste criado, mas sem código que o atenda

  2. Green -> Código criado corretamente e atendendo os requisitos

  3. Refactor -> Agora que temos um MVP, reescrevemos o teste, para que este seja mais refinado, e o mesmo vale para o código. Isso pode incluir tratamento de erros, estruturas de dados mais eficientes ou qualquer outra melhoria.


Jira Xray

Pretendo escrever um post mais a fundo sobre Jira em breve, então não vou me aprofundar muito aqui, mas acredito que seja interessante mencionar.

Utilizamos muito o Jira para planejar nossas sprints, tarefas, épicos, relacionar branches e commits com a razão de sua existência e de certa forma, criar uma documentação viva. Um exemplo da versatilidade dessa ferramenta é que eu a utilizo, em conjunto o Obsidian, para escrever cada post aqui do blog, ou testes técnicos, planejar aulas entre outras funções, e uma delas que muitas vezes vejo sendo ignorada é a função de testes, chamada Xray.

O Xray quebra os "silos" de desenvolvedores, QA, PO/PM/Negócios. Ao criar um plano de testes utilizando o Xray, todos terão visibilidade do que passa e do que não passa e permitem que a squad tenha maior conhecimento sobre seu progresso e bloqueios, para que possa melhor seguir com a sprint.


Testes em CI/CD

Hoje, graças ao DevOps, ou DevSecOps, temos o conceito de esteira. Desenvolvemos nossas features em diferentes branches, e precisamos integra-las, a fim de disponibilizar cada feature em ambiente produtivo para consumo. Mas como garantir que a funcionalidade desenvolvida por mim não quebre algo feito por você? Como garantir que ao integrarmos uma feature branch com a branch de produção não iremos derrubar o serviço? Aí que entram os testes automatizados na esteira, ao impedir um merge por um teste quebrado, impedimos uma falha no futuro.


Referências

© 2023, BrazucaForce - Todos os direitos reservados

Políticas de Privacidade

bottom of page