Isolated Unit Tests e Shallow Unit Tests

Esse artigo está sendo escrito durante a preparação para uma talk aqui na Concrete. Aqui nos preocupamos muito com qualidade de software e testes unitários. Técnicas melhores para escrever testes sempre são pautas de talks e conversas entre nós Devs.

Além disso tivemos alguns problemas recentes em um projeto ao testar componentes do Angular e que aparentemente é um problema que transborda para outros frameworks, já que os conceitos não são claros.

Para quem não conhece sobre testes unitários, uma unidade é a porção de código que você escolhe testar e durante o teste você não pode ter dependencias de outros trechos de código, componentes,serviços, rede e etc. Para simular algumas dependências, você deve usar spy, stubs e mocks.

Ter um componente como unidade, envolve escrever testes unitários das entradas, saídas e comportamentos deste componente. Boa parte dos frameworks e bibliotecas atuais, fornecem formas de criar testes unitários para seus componentes.

Para esses testes, temos dois tipos de abordagem que iremos tratar nesse artigo. Elas são conhecidas como shallow rendering unit tests e isolated unit tests(testes unitários de renderização superficial e testes unitários isolado).

Isolated Unit Tests

O que são os isolated unit tests? De forma simples, em testes unitários isolados você importa sua classe e chama os métodos dela, sem olhar para o seu template e o comportamento do mesmo.

Você escreve seus it e expects para métodos da classe, informa os parâmetros e observa a saída ou modificação de comportamento das propriedades ao chamar esses métodos.

Então se tivermos o seguinte componente (o exemplo foi tirado o site oficial do Angular.io)

export class HeroesComponent implements OnInit {
  heroes = HEROES;  selectedHero: Hero;
// métodos de constructor e onInit omitidos p/ facilitar a leitura do exemplo 
  onSelect(hero: Hero): void {
    this.selectedHero = hero;
  }
}

o nosso teste seria escrito da seguinte forma:

// não adicionei os imports para facilitar a leitura
describe('HeroComponent test', () => {
  let heroComponent;
  beforeEach(() => {
    heroComponent = new HeroComponent();
  });
  
  it('expect selectedHero has a hero name after call', () => {
    cons hero = { id: 0, name: 'Black Panther' };
    
    heroComponent.onSelect(hero);
    
    expect(component.selectedHero).toEqual(hero); // Simplificado p/ o exemplo.
  });
});

Agora vamos ver um exemplo de teste de renderização superficial ou shallow test.

Shallow rendering test

O que são testes unitários de renderização superficial? São testes unitários que renderizam parte do seu template, sem precisar ou depender de outros componentes e com isso fazer as suposições. Esse tipo de teste requer algumas coisas além do teste isolado, mas, vamos aos exemplos antes de falar mais sobre isso.

Imagina que temos o seguinte template como parte do nosso HeroComponent já mencionado na parte dos testes isolados.

<h2>{{hero.name | uppercase}} Details</h2>
<div><span>id: </span>{{hero.id}}</div>
<label>name:
  <input [(ngModel)]="hero.name" placeholder="name">
</label>

Como testar se houve uma modificação do nome do herói em nosso componente, já que isso acontece no template e não em nossa classe? Para isso, faremos o seguinte teste:

describe('HeroComponent test', () => {
  let fixture, heroComponent;
  beforeEach(() => {
    fixture = TestBed.createComponent(HeroComponent);
    heroComponent = fixture.componentInstance;
    fixture.detectChanges();
  });
  it('expect hero name be upperCase after render', () => {
    heroComponent.selectedHero = { id: 0, name: 'Black Panther' };
    fixture.detectChanges();
    const heroName = heroComponent.debugElement.nativeElement.querySelector('');
    expect(heroName.textContent).toBeEqual('Black Panther');
  });
});

Esse é um exemplo simples de como acontece o shallow unit test, ele "renderiza" parte do seu template criando elementos do DOM e observando eles ao longo do teste.

Problemas com shallow unit test

Ao comparar os testes, notamos como a estratégia de shallow é maior e mais verboso do que a forma isolada, além disso ele também traz alguns outros problemas e dificuldades para testar.

Alguns problemas que temos é que demora um pouco mais para executar os testes, porque algumas interações e comportamentos dos componentes ao serem testados exigem uma declaração ainda maior. Quando se trata de eventos que acontecem no DOM depedendo da experiência da pessoa ou do time com testes, pode ser algo que tira a velocidade de desenvolvimento e em alguns momentos parece que escrevemos testes integrados, já que existe a dependência de outras interações do browser.

Resolvendo o problema de eventos ao usar shallow unit test

Para um ponto especificamente quero deixar uma solução, já que foi algo pelo qual tive certos problemas com eventos disparados com clicks e eventos subsquentes que trazem verdadeiros problemas nos testes shallow.

Por exemplo, quando clicamos em alguns elementos do browser outros eventos são disparados, mas, quando isso acontece através de um script, não temos o disparo desses eventos, então o que fazer para testar?

Temos que criar um dispatchEvent especifico para vermos o resultado e chamadas da função que emitem. Como podemos ver no exemplo abaixo:

const input = document.createElement('input');
input.type = 'text';
input.placeholder = 'focus here!';
input.addEventListener('focus', function(event) {
  console.log('element:', this);
  console.log('event:', event);
})
input.dispatchEvent(new Event('focus', { bubbles: true, cancelable: false }))

Com isso conseguimos chamar algumas funções dentro da classe do nosso componente. Para essa solução fica o agradecimento ao Marcus Ortense.

Qual das duas abordagens escolher?

Demonstrar essas duas abordagens de testes é importante, especificamente na documentação do angular, só temos a abordagem através de shallow rendering e podemos usar outras formas e padrões para ter uma boa cobertura de teste e não depender das ações do template.

Agradecimentos e referências

Agradecimento pela ajuda e revisão do Emerson de Faria Batista que ajudou na revisão e também do Marcus Ortense já mencionado.

Referencias:
angular.io
Unit and Integration tests for Angular components. Part 2 out of 3.

Ficou com alguma dúvida? Me manda uma DM no Twitter.