Obtén 100% en el cuestionario para continuar
Variables y declaraciones
var, let y const, ámbito y hoisting, y por qué const no significa inmutable en un fixture compartido.
Tres palabras clave declaran una variable en JavaScript, y una de ellas no debería aparecer nunca en código escrito hoy. Esta lección trata del porqué, y de la pregunta bastante más interesante que se esconde detrás: const no significa lo que la mayoría de la gente cree que significa, y la distancia entre lo que significa y lo que la gente cree que significa es donde se echan a perder los fixtures de prueba compartidos.
Las tres palabras clave
var industry = 'Retail'
let page = 1
const baseUrl = 'http://localhost:3000'Las tres crean una variable. Lo que las separa es el ámbito, la reasignación y qué pasa antes de que la línea se ejecute.
| Ámbito | Se puede reasignar | Antes de la línea de declaración | |
|---|---|---|---|
var | La función entera | Sí | undefined |
let | El bloque { } que la contiene | Sí | Lanza ReferenceError |
const | El bloque { } que la contiene | No | Lanza ReferenceError |
Ámbito de función frente a ámbito de bloque
Un bloque es cualquier cosa entre llaves: el cuerpo de un if, de un for, de una function, o unas { } sueltas.
let y const viven dentro del bloque en el que fueron declaradas y desaparecen en la llave de cierre. var ignora los bloques por completo y pertenece a toda la función que la rodea.
function report() {
if (true) {
var a = 'var'
let b = 'let'
}
console.log(a) // 'var' - sigue aquí
console.log(b) // ReferenceError - se fue, como debe ser
}Eso no es una sutileza. Es la razón por la que var produjo una generación de fallos en los que una variable declarada dentro de un bucle seguía visible, y seguía con su último valor, mucho después de que el bucle terminara.
Hoisting y la temporal dead zone
JavaScript procesa las declaraciones antes de ejecutar el código. Eso es hoisting, y las tres palabras clave lo sufren. La diferencia es qué contiene la variable mientras tanto.
console.log(a) // undefined - var sufre hoisting y se inicializa
var a = 1
console.log(b) // ReferenceError - let sufre hoisting, pero no se inicializa
let b = 1La ventana entre el inicio del bloque y la línea del let o const, donde la variable existe pero no se puede tocar, se llama temporal dead zone. Existe para que leer una variable demasiado pronto sea un error ruidoso en vez de un undefined silencioso.
👨🏫 Ese es el punto entero, y vale decirlo con todas las letras:undefinedse propaga. Unavarleída demasiado pronto se cuela en una comparación, que pasa, y tu prueba se pone en verde por el motivo equivocado. UnReferenceErrordetiene la prueba justo en la línea que está mal. Lo ruidoso le gana a lo silencioso, siempre.
const no significa inmutable
Aquí está la que le cuesta dinero de verdad a los equipos.
const significa que el enlace no se puede reasignar. No dice absolutamente nada sobre el valor.
const customer = { name: 'Acme', size: 'Large' }
customer.size = 'Small' // está permitido, y ese es justamente el problema
customer = {} // TypeError: Assignment to constant variableLo mismo vale para los arrays:
const created = []
created.push('Acme') // permitido
created.length = 0 // también permitidoAsí que un array const se puede llenar, vaciar y reordenar. Un objeto const puede tener todas sus propiedades reescritas. Lo único que const protege es a qué valor apunta el nombre.
Ahora pon eso dentro de una suite de pruebas.
// fixtures/customer.js
export const newCustomer = { name: 'Acme', industry: 'Retail', size: 'Large' }// dos pruebas, en dos archivos, ambas importando ese objeto
newCustomer.size = 'Small' // la prueba A lo ajusta para su propio casoLa prueba A pasa. La prueba B, que importó el mismo objeto, ahora recibe Small y falla, o peor, pasa por el motivo equivocado. Ejecútalas en el orden inverso y el fallo se mueve. Ese es el clásico ticket de "falla en CI pero no en mi máquina", y const no lo impidió ni por un segundo.
👨🏫 Cuando alguien del equipo dice "es un const, no puede cambiar", está hablando de la variable. El fallo está en el objeto. La lección 5 de este curso cierra este círculo y te muestra la copia que lo arregla.
La regla que sigue este curso
const por defecto. let cuando el valor tiene que cambiar. var nunca.
Y el motivo, en vez de la mera afirmación:
constpor defecto porque quien lee y veconstsabe que ese nombre va a significar lo mismo treinta líneas más abajo. Es una cosa menos que sostener en la cabeza mientras se lee una prueba.letcuando tiene que cambiar, porque fingir lo contrario lleva a contorsiones peores que simplemente admitir que el valor cambia.varnunca, porque su ámbito de función y su comportamiento deundefined-antes-de-la-declaración ya no tienen ninguna ventaja. Todo lo que hacevar,letlo hace con más seguridad.
En código de pruebas
Dos patrones aparecen constantemente.
La variable declarada en describe y asignada en beforeEach. El valor cambia en cada prueba, así que no puede ser const; la aserción necesita verlo, así que no puede vivir dentro del beforeEach.
Un valor capturado de la página. Este muerde a todo el mundo una vez, y vale verlo antes de que te pase:
let firstName // declarada aquí fuera, o la aserción no la ve
cy.get('[data-test="customer-name"]').first().invoke('text').then((text) => {
firstName = text.trim()
})
// ...más tarde, dentro de otro .then(), nunca en el nivel superiorDeclárala en el bloque describe y toda la prueba puede leerla. Declárala dentro del .then() y desaparece en la llave de cierre, que es el comportamiento correcto de let y una sorpresa confusa para quien aprendió var primero.
👨🏫 Hay una segunda trampa en ese fragmento, sobre cuándo ocurre la asignación, y no sobre dónde vive la variable. Ese es el tema de la lección 6, sobre el event loop. Por ahora, fíjate solo en el ámbito.
Vista general de la sintaxis 📖
const
Declara un enlace con ámbito de bloque que no se puede reasignar. El valor en sí todavía se puede mutar.
Sintaxis:
const name = valueEjemplo:
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 un enlace con ámbito de bloque que se puede reasignar.
Sintaxis:
let name = valueEjemplo:
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)
})
})Contenido sugerido 📚
- var - MDN
- let - MDN
- const - MDN
- Hoisting - MDN
- Temporal dead zone - MDN
- Variables and Aliases - documentación oficial de Cypress
Ejercicio 🎯
Abre helpers/data.js y encuentra el esqueleto de baseCustomer.
El contrato en su comentario de documentación es: devolver un objeto de cliente con los campos name, industry y size, listo para que una prueba rellene un formulario.
- Impleméntalo como un objeto
consta nivel de módulo que la función devuelve directamente. Ejecutanpm run cy:run(onpm run pw:test). Las dos pruebas que lo usan pasan. - Ahora añade a la primera de esas pruebas una línea que cambie
sizea'Small'en el objeto que recibió, y ejecuta de nuevo. Mira cómo falla la segunda prueba. - Arréglalo, de modo que las dos pruebas pasen sin importar en qué orden se ejecuten.
- Escribe, en una frase, por qué el paso 2 fue posible aunque el objeto se declarara con
const.
🙊 El paso 1 se ve así, y es la versión que lleva el fallo:
const BASE = { name: 'Acme', industry: 'Retail', size: 'Large' }
export function baseCustomer() {
return BASE
}Cada quien que llama recibe el mismo objeto, así que un cambio hecho por una prueba es visible para todas.constnunca entró en esta historia: el enlaceBASEnunca se reasignó, solo se mutó el objeto al que apunta.
La corrección es entregarle a cada llamada su propia copia:
export function baseCustomer() {
return { name: 'Acme', industry: 'Retail', size: 'Large' }
}El objeto literal ahora está dentro de la función, así que cada llamada construye uno nuevo. No queda nada compartido, lo que significa que no queda nada que se pueda filtrar. Fíjate en que la corrección no fue unconstmejor: ninguna palabra clave habría salvado la primera versión, porque el problema era un objeto llegando a dos pruebas.
El costo es que los valores por defecto ahora están escritos dentro de la función, y no en un lugar con nombre al inicio del archivo. La lección 5 muestra cómo tener las dos cosas, manteniendo un único objeto de origen y entregando una copia de él.
Y si la parte poco familiar aquí es la función en sí, no pasa nada: por ahora basta con saber que su cuerpo se ejecuta de nuevo en cada llamada, y eso es lo que hace que el objeto sea nuevo cada vez. Las funciones tienen una lección propia, la lección 3, donde se cubren como es debido las declaraciones, las arrow functions, los parámetros, los valores por defecto y los closures.
Muestra al mundo lo que aprendiste 🌎
Para mostrarle a tu red profesional lo que aprendiste en esta lección, publica lo siguiente en LinkedIn.
Estoy haciendo el curso "Algoritmos y Programación para QAs" de @Walmyr Lima e Silva Filho en la @Talking About Testing School, donde aprendí por qué const no significa inmutable, cómo un objeto de fixture compartido filtra estado entre pruebas aunque todas las declaraciones sean const, y por qué el ámbito de bloque hace que una prueba falle de forma ruidosa en vez de silenciosa. #TalkingAboutTesting #TATSchool #AlgorithmsAndProgrammingForQAs #JavaScript #TestAutomation
👨🏫 Acuérdate de mencionarme en tu publicación. Aquí está mi perfil de LinkedIn.
Cuestionario
Un fixture compartido se exporta como `export const newCustomer = { name: 'Acme', size: 'Large' }`. La prueba A hace `newCustomer.size = 'Small'` y la prueba B, en otro archivo, empieza a fallar. ¿Por qué no lo impidió const?
Obtén 100% en el cuestionario para continuar