Talking About TestingATT</>
Algoritmos e Programação para QAs

Acerte 100% do quiz para continuar

Aula
Prévia gratuita

Variáveis e declarações

var, let e const, escopo e hoisting, e por que const não significa imutável em uma fixture compartilhada.

Três palavras-chave declaram uma variável em JavaScript, e uma delas nunca deveria aparecer em código escrito hoje. Esta lição é sobre o porquê, e sobre a pergunta bem mais interessante que se esconde atrás disso: const não significa o que a maioria das pessoas pensa que significa, e a distância entre o que ele significa e o que as pessoas acham que ele significa é onde fixtures de teste compartilhadas se estragam.

As três palavras-chave

var industry = 'Retail'
let page = 1
const baseUrl = 'http://localhost:3000'

As três criam uma variável. O que as separa é escopo, reatribuição e o que acontece antes da linha rodar.

EscopoPode ser reatribuídaAntes da linha de declaração
varA função inteiraSimundefined
letO bloco { } que a envolveSimLança ReferenceError
constO bloco { } que a envolveNãoLança ReferenceError

Escopo de função versus escopo de bloco

Um bloco é qualquer coisa entre chaves: o corpo de um if, de um for, de uma function, ou um { } sozinho.

let e const vivem dentro do bloco em que foram declarados e somem na chave de fechamento. var ignora blocos por completo e pertence à função inteira ao redor.

function report() {
  if (true) {
    var a = 'var'
    let b = 'let'
  }
  console.log(a) // 'var'      - ainda está aqui
  console.log(b) // ReferenceError - sumiu, como deveria
}

Isso não é uma sutileza. É a razão de o var ter produzido uma geração de bugs em que uma variável declarada dentro de um laço continuava visível, e continuava com o último valor, muito depois de o laço acabar.

Hoisting e a temporal dead zone

O JavaScript processa as declarações antes de rodar o código. Isso é hoisting, e as três palavras-chave sofrem hoisting. A diferença é o que a variável guarda nesse meio-tempo.

console.log(a) // undefined         - var sofre hoisting e é inicializada
var a = 1

console.log(b) // ReferenceError    - let sofre hoisting, mas não é inicializada
let b = 1

A janela entre o topo do bloco e a linha do let ou const, na qual a variável existe mas não pode ser tocada, se chama temporal dead zone. Ela existe para que ler uma variável cedo demais seja um erro barulhento em vez de um undefined silencioso.

👨‍🏫 Esse é o ponto todo, e vale dizer com todas as letras: undefined se propaga. Uma var lida cedo demais escoa para uma comparação, que passa, e o seu teste fica verde pelo motivo errado. Um ReferenceError para o teste exatamente na linha que está errada. Barulhento ganha de silencioso, sempre.

const não significa imutável

Aqui está a que custa dinheiro de verdade aos times.

const significa que o vínculo não pode ser reatribuído. Não diz absolutamente nada sobre o valor.

const customer = { name: 'Acme', size: 'Large' }
customer.size = 'Small'   // tudo bem, e é justamente esse o problema
customer = {}             // TypeError: Assignment to constant variable

O mesmo vale para arrays:

const created = []
created.push('Acme')      // tudo bem
created.length = 0        // também tudo bem

Ou seja, um array const pode ser preenchido, esvaziado e reordenado. Um objeto const pode ter todas as propriedades reescritas. A única coisa que o const protege é para qual valor o nome aponta.

Agora coloque isso dentro de uma suíte de testes.

// fixtures/customer.js
export const newCustomer = { name: 'Acme', industry: 'Retail', size: 'Large' }
// dois testes, em dois arquivos, os dois importando esse objeto
newCustomer.size = 'Small'   // o teste A ajusta o objeto para o caso dele

O teste A passa. O teste B, que importou o mesmo objeto, agora recebe Small e falha, ou pior, passa pelo motivo errado. Rode-os na ordem inversa e a falha se move. Esse é o clássico chamado de "falha na CI, mas não na minha máquina", e o const não impediu isso por um segundo sequer.

👨‍🏫 Quando uma pessoa colega diz "é um const, não pode mudar", ela está falando da variável. O bug está no objeto. A lição 5 deste curso fecha esse ciclo e te mostra a cópia que resolve isso.

A regra que este curso segue

const por padrão. let quando o valor precisa mudar. var nunca.

E o motivo, em vez da mera afirmação:

  • const por padrão porque quem lê e vê const sabe que aquele nome vai significar a mesma coisa trinta linhas abaixo. É uma coisa a menos para segurar na cabeça enquanto lê um teste.
  • let quando precisa mudar, porque fingir o contrário leva a contorcionismos piores do que simplesmente admitir que o valor muda.
  • var nunca, porque o escopo de função e o comportamento de undefined-antes-da-declaração não têm mais nenhuma vantagem. Tudo o que o var faz, o let faz com mais segurança.

Em código de teste

Dois padrões aparecem o tempo todo.

A variável declarada no describe e atribuída no beforeEach. O valor muda a cada teste, então não pode ser const; a asserção precisa enxergá-lo, então não pode viver dentro do beforeEach.

Um valor capturado da página. Esse morde todo mundo uma vez, e vale ver antes de acontecer com você:

let firstName    // declarada aqui fora, ou a asserção não a enxerga

cy.get('[data-test="customer-name"]').first().invoke('text').then((text) => {
  firstName = text.trim()
})

// ...mais tarde, dentro de outro .then(), nunca no nível de cima

Declare no bloco describe e o teste inteiro consegue lê-la. Declare dentro do .then() e ela some na chave de fechamento, que é o comportamento correto do let e uma surpresa confusa para quem aprendeu var primeiro.

👨‍🏫 Há uma segunda armadilha nesse trecho, sobre quando a atribuição acontece, e não sobre onde a variável vive. Esse é o assunto da lição 6, sobre o event loop. Por ora, repare apenas no escopo.

Visão geral da sintaxe 📖

const

Declara um vínculo com escopo de bloco que não pode ser reatribuído. O valor em si ainda pode ser modificado.

Sintaxe:

const name = value

Exemplo:

describe('Customers table', () => {
  const expectedColumns = ['Name', 'Industry', 'Size', 'Registered at', 'Contact email']

  it('renders every column', () => {
    cy.visit('/')
    cy.get('th').should('have.length', expectedColumns.length)
  })
})

let

Declara um vínculo com escopo de bloco que pode ser reatribuído.

Sintaxe:

let name = value

Exemplo:

describe('Sorting', () => {
  let firstRowName

  beforeEach(() => {
    cy.visit('/')
    cy.get('[data-test="row-name"]').first().invoke('text').then((text) => {
      firstRowName = text.trim()
    })
  })

  it('changes the first row when sorted the other way', () => {
    cy.get('[data-test="sort-name"]').click()
    cy.get('[data-test="row-name"]').first().should('not.have.text', firstRowName)
  })
})

Conteúdo sugerido 📚

Exercício 🎯

Abra o helpers/data.js e encontre o stub de baseCustomer.

O contrato no comentário de documentação é: devolver um objeto de cliente com os campos name, industry e size, pronto para um teste preencher um formulário.

  1. Implemente-o como um objeto const no nível do módulo que a função devolve diretamente. Rode npm run cy:run (ou npm run pw:test). Os dois testes que o usam passam.
  2. Agora acrescente ao primeiro desses testes uma linha que muda size para 'Small' no objeto que ele recebeu, e rode de novo. Veja o segundo teste falhar.
  3. Conserte, de modo que os dois testes passem em qualquer ordem.
  4. Escreva, em uma frase, por que o passo 2 foi possível mesmo com o objeto declarado com const.
🙊 O passo 1 fica assim, e é a versão que carrega o bug:
const BASE = { name: 'Acme', industry: 'Retail', size: 'Large' }

export function baseCustomer() {
  return BASE
}
Toda pessoa que chama recebe o mesmo objeto, então uma alteração feita por um teste é visível para todos eles. O const nunca entrou nessa história: o vínculo BASE nunca foi reatribuído, apenas o objeto para o qual ele aponta foi modificado.

A correção é entregar a cada chamada a sua própria cópia:
export function baseCustomer() {
  return { name: 'Acme', industry: 'Retail', size: 'Large' }
}
O objeto literal agora está dentro da função, então toda chamada constrói um novo em folha. Não sobrou nada compartilhado, o que significa que não sobrou nada para vazar. Repare que a correção não foi um const melhor: nenhuma palavra-chave teria salvado a primeira versão, porque o problema era um objeto chegando a dois testes.

O custo é que os valores padrão agora estão escritos dentro da função, e não em um lugar nomeado no topo do arquivo. A lição 5 mostra como ter as duas coisas, mantendo um único objeto de origem e entregando uma cópia dele.

E se a parte não familiar aqui for a própria função, tudo bem: por ora, basta saber que o corpo dela roda de novo a cada chamada, e é isso que faz o objeto ser novo toda vez. Funções ganham uma lição só delas, a lição 3, onde declarações, arrow functions, parâmetros, valores padrão e closures são cobertos direito.

Mostre ao mundo o que você aprendeu 🌎

Para mostrar à sua rede profissional o que você aprendeu nesta lição, publique o seguinte no LinkedIn.

Estou fazendo o curso "Algoritmos e Programação para QAs" do @Walmyr Lima e Silva Filho na @Talking About Testing School, onde aprendi por que const não significa imutável, como um objeto de fixture compartilhado vaza estado entre testes mesmo com todas as declarações sendo const, e por que o escopo de bloco faz um teste falhar alto em vez de falhar em silêncio. #TalkingAboutTesting #TATSchool #AlgorithmsAndProgrammingForQAs #JavaScript #TestAutomation

👨‍🏫 Lembre-se de me marcar no seu post. Aqui está meu perfil no LinkedIn.

Quiz

Pergunta 1 de 2
Pontuação: 0

Uma fixture compartilhada é exportada como `export const newCustomer = { name: 'Acme', size: 'Large' }`. O teste A faz `newCustomer.size = 'Small'` e o teste B, em outro arquivo, passa a falhar. Por que o const não impediu isso?

Acerte 100% do quiz para continuar