ivanbtrujillo / ivanbtrujillo.com

Blog using Next
https://ivanbtrujillo.now.sh
0 stars 0 forks source link

Aprendiendo Vue - Componente con Test unitarios #16

Open ivanbtrujillo opened 4 years ago

ivanbtrujillo commented 4 years ago

date: "2020-06-27" img: "https://res.cloudinary.com/ivanbtrujillo/image/upload/v1593276419/C2_BFQue_CC_81-debe-tener-un-profesional-especialista-en-Vue_qmljgs.png" summary: Crearé un componente desde cero con Vue.JS y lo testearé con jest y vue-testing-library

Este será el primer artículo de muchos donde ire documentando mi proceso de aprendizaje de Vue. Es cierto que ya he hecho alguna pequeña aplicación con Vue hace ya bastante tiempo. En esta serie de post que voy a empezar voy a retomarlo todo desde cero.

Creando un componente

Para crear una aplicación vue tenemos vue cli, un paquete global de npm que podemos instalar con:

npm install -g @vue/cli

Utilizando vue cli podemos crear proyectos partiendo de distintas boilerplates. La oficial es "webpack" ( vue init webpack my-project) pero podemos utilizar cualquier otra de github. En este repositorio hay muchas de ellas, yo voy a utilizar la oficial.

Lo que haré será crear un pequeño componente para dar una valoración. Lo llamaré rating-component.

vue create ratings-component

Una de las cosas que nos preguntará es el preset que queremos. Elegiremos default.

Abrimos el proyecto con nuestro ide (yo utilizo VSCode) y ejecutamos el servidor en modo desarrollo. Para esto yo prefiero utilizar la consola del VSCode.

cd rating-component && code .

Luego en la consola de VSCode (ábrela con cmd + shift + t):

npm run dev

Veremos nuestra aplicación funcionando en localhost:8080

Eslint

Instalamos los siguientes paquetes:

npm install --save-dev eslint eslint-plugin-vue@next

Creamos un fichero .eslintrc.js y añadimos:

module.exports = {
  root: true,
  env: {
    browser: true,
    jest: true,
  },
  extends: ["eslint:recommended", "plugin:vue/vue3-recommended"],
  rules: {
    "no-console": ["error", { allow: ["warn", "error", "info"] }],
  },
};

Por último, podemos configurar VSCode para que fixee los errores al guardar. Para ello creamos una carpeta .vscode en la raiz del proyecto, dentro creamos un fichero settings.json y añadimos:

{
  "eslint.validate": ["javascript", "javascriptreact", "vue"],
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true,
    "source.organizeImports": false
  }
}

De esta forma ahora al guardar se fixeará nuestro codigo.

Vue dev tools

Para debugear correctamente, es recomendable instalar las vue.js dev tools en nuestro navegador. Puedes encontrarlo en las extensiones de Firefox, Chrome o Safari. Aquí te dejo el enlace para las de Chrome

Creando nuestro componente

Una de las características de Vue es que el HTML, el JS y el CSS se suelen definir en el mismo fichero (con extensión .vue). El contenido de nuestro fichero se divide en tres secciones:

<template>
  /* HTML  */
</template>

<script>
  /* JS  */
</script>

<style>
  /* CSS */
</style>

Vamos a crear un fichero Ratings.vue dentro de src/components con este contenido. Simplemente mostrarermos un h1 que diga Ratings, y le damos un nombre al componente (Ratings):

<template> 
  <h1>Ratings</h1> 
</template>

<script>

export default {
  name: "Ratings"
};

</script>

<style></style>

Lo siguiente que haremos es ir a App.vue e importarlo dentro de :

import Ratings from "./components/Ratings";

Lo declaramos como componente:

export default {
  name: "App",
  components: {
    HelloWorld,
    Ratings
  }
};

Y lo usamos:

<template>
  <div id="app">
    <img src="./assets/logo.png" />
    <HelloWorld />
    <Ratings />
  </div>
</template>

Verás que ahora aparece el texto Ratings en la web.

Ahora que ya tenemos nuestro componente funcionando, vamos a limpiar App.vue para quedarnos solo con nuestro componente Ratings:

<template>
  <div id="app">
    <Ratings />
  </div>
</template>

<script>

import Ratings from "./components/Ratings";

export default {
  name: "App",
  components: {
    Ratings
  }
};
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
</style>

Lo siguiente que haremos es instalar una libreria de iconos para mostrar estrellas rellenas o vacias en funcion de la valoración que vayamos a darle a nuestro componente ratings.

npm i vue-unicons

En main.js, importaremos la libreria y los iconos que queremos. En este caso queremos star y star monochrome.

import Vue from "vue";
import Unicon from "vue-unicons";
import { uniStar, uniStarMonochrome } from "vue-unicons/src/icons";
import App from "./App";

Unicon.add([uniStar, uniStarMonochrome]);
Vue.use(Unicon);
Vue.config.productionTip = false;

/* eslint-disable no-new */
new Vue({
  el: "#app",
  render: h => h(App)
});

Ahora en Ratings, utilizamos estos dos iconos:

<template>
  <div>
    <ul>
      <li>
        <unicon name="star" fill="orange" />
      </li>
      <li>
        <unicon name="star" fill="orange" icon-style="monochrome" />
      </li>
    </ul>
  </div>
</template>

Para quitarle el estilo de lista (los puntos), en el css indicaoms list-style: none:

<style>
ul {
  list-style: none;
}
</style>

Ahora mismo deberiamos ver dos estrellas naranjas (una rellenada y otra sin rellenar). En nuestro componente, vamos a permitir valorar del 1 al 10, por lo que vamos a añadir 10 estrellas vacias de momento. Utilizaremos un v-for para mostrar 10 estrellas vacias:

<template>
  <div>
    <ul>
      <li v-for="n in 10" :key="n">
        <unicon name="star" fill="orange" />
      </li>
    </ul>
  </div>
</template>

Aún podemos hacerlo mejor. Definiremos dos variables para controlar nuestro componente. stars: numero de estrellas seleccionadas maxStars: numero máximo de estrellas que podemos utilizar.

Para ello utilizaremos la propiedad data:

export default {
  name: "Ratings",
  data() {
    return {
       stars: 0,
       maxStars: 10
    }
  }
};

Y reemplazamos el numero 10 del v-for por maxStars.

Queremos que nuestras estrellas se muestren de forma horizontal. Vamos a hacerlo modificando la lista en los estilos:

<style>
ul {
  list-style: none;
}
li {
  float: left;
}
</style>

Primera funcionalidad. Estrellas seleccionadas

Lo primero que vamos a hacer es que se muestren tantas estrellas rellenas como indiquemos en la variable stars (que ahora mismo es igual a 0.

Vamos a ponerle un valor de 6.

A continuación, en nuestro v-for sabemos que la n es el numero en el bucle. Queremos que si el numero de stars es 6, se muestren 6 rellenas.

En unicons, la forma de cambiar de rellena a no rellena es utilizando la propiedad icon-style. Si es monochrome, aparece rellena y si es line (por defecto es line) aparece solo el borde. Lo que vamos a hacer es bindear la propiedad icon-style para que interprete el resultado en base a una logica.

 <unicon name="star" fill="orange" v-bind:icon-style="n <= stars ? 'monochrome' : 'line'" />

Si n (la estrella en el array) es menor o igual que el numero de estrellas, indicamos que sea monochrome y si no, sera line.

Es decir, si stars vale 6, las seis primeras estrellas seran monochrome y las cuatro útlimas seran line.

El v-bind se puede abreviar poniendo únicamente los dos puntos:

 <unicon name="star" fill="orange" :icon-style="n <= stars ? 'monochrome' : 'line'" />

Puedes comprobar que el código funciona correctamente.

Añadiendo un texto descriptivo

Lo siguiente que queremos hacer es mostrar un texto que indique la valoración, en nuestro caso: n de maxStars. Colocamos lo siguiente a continuación del cierre de

<span>{{ stars }} de {{ maxStars }}</span>

Podemos hacerlo mejor usarlo una computed propierty. Este tipo de elementos son valores que se actualizan cuando otros cambian. Vamos a crear una computed property llamada counter:

 computed: {
   counter() {
     return `${this.stars} de ${this.maxStars}`
   }
 }

Y reemplazamos el texto del por:

 <span>{{ counter }}</span>

Vemos que se sigue viendo igual. En cuanto al estilo, vamos a hacer unos cuantos retoques. Modificamos nuestro div para que muestre el texto debajo de la lista. Por supuesto podríamos / deberiamos crear clases en vez de dar estilos a los elementos directamente, pero no es el objetivo de este post:

<style>
div {
  display: flex;
  flex-direction: column;
}
ul {
  list-style: none;
}
li {
  float: left;
}
</style>

Vemos que las estrellas salen muy separadas respecto al texto. Esto es porque las listas tienen un margen y un padding por defecto. Vamos a actualizar li y ul con margin: 0 y padding: 0:

ul {
  list-style: none;
  margin: 0;
  padding: 0;
}
li {
  float: left;
  margin: 0;
  padding: 0;
}

Modificando la valoracion

Queremos que cuando el usuario clique en una estrella, la valoración se modifique a ese valor. Por ejemplo si clica en la estrella número 2, la valoración debe ser 2. Para ello, vamos a utilizar el evento @click y llamaremos una funcion que se llame setRate a la que le pasaremos la estrella en el array que se ha clicado:

 <li v-for="n in maxStars" :key="n" **@click="setRate(n)"**>

Para modificar las stars de data, necesitamos crear el método setRate y actualizar el valor utilizando lo que recibimos por parámetro:

 methods: {
   setRate(star) {
     this.stars = star;
   }
 }

Ahora puedes comprobar que cada vez que clicamos en una estrella diferente, el rate cambia.

Haciendo el componente reutilizable

hasta ahora hemos definido internamente el valor inicial y el maximo valor que pueden dar los usuarios. Para que el componente sea reutilizable, estos valores deben de poder recibirse desde un componente padre. Es decir, en unas aplicaciones querremos un rating del 1 al 5, en otras del 1 al 10, etc.

Para ello vamos a definir dos props: selectedStars y maxStars

props: ["selectedStars", "maxStars"],

maxStars no va a cambiar, por lo que podemos usarlo directamente pero en el caso de selectedStars, lo que haremos será inicializar nuestra variable stars dentro de data, al valor que recibimos por parámetro:

 data() {
   return {
      stars: this.selectedStars
   }
 },

Por último, para pasarle las props a nuestro componente utilizamos el shorthand de v-bind (dos puntos) en App.vue:

<Ratings :maxStars="10" :selectedStars="3" />

Testing. Configurando el entorno

Para testear nuestro componente vamos a moverlo a una carpeta.

Creamos una carpeta Ratings dentro de components y movemos nuestro componente ahi. Actualizamos el import en App.vue y procedemos a crear nuestro fichero Ratings.test.js.

Si empezaramos a escribir nuestro terst, veríamos una serie de warnings indicandote que las palabras describe and it no están definidas. Es un error de eslint. Para solucionarlo añadiremos jest a los entornos de eslint (.eslintrc):

 ... 
 env: {
   browser: true,
   jest: true
 },
...

Ahora instalaremos Jest y vue-testing-library para empezar a testear.

npm install --save-dev jest @testing-library/vue

Vue testing library funciona igual que la version de react, por lo que si estas acostumbrad@ a react testing library, te será muy familiar.

Por último, nos queda configurar Jest. Creamos un fichero jest.config.js en el root de nuestro proyecto con la siguiente configuración:

const path = require("path");

module.exports = {
  rootDir: path.resolve("./"),
  moduleFileExtensions: ["js", "json", "vue"],
  moduleNameMapper: {
    "^@/(.*)$": "<rootDir>/src/$1",
  },
  testURL: "http://localhost",
  transform: {
    "^.+\\.js$": "<rootDir>/node_modules/babel-jest",
    ".*\\.(vue)$": "<rootDir>/node_modules/vue-jest",
  },
  transformIgnorePatterns: ["/node_modules/(?!vue-unicons).+(js|vue)$"],
  snapshotSerializers: ["<rootDir>/node_modules/jest-serializer-vue"],
  coverageDirectory: "<rootDir>/coverage",
  collectCoverageFrom: [
    "src/**/*.{js,vue}",
    "!src/main.js",
    "!**/node_modules/**",
  ],
};

Para ejecutar los test, recuerda que has de ejecutar el comando jest. Puedes crearte una serie de scripts en el package.json para ejecutar jest en modo watch y jest con coverage:

    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"

Cuando lanzes el comando coverage, aparte de ver la cobertura por consola se te crearán los ficheros correspondientes en la carpeta coverage del proyecto.

Creando nuestro test

Vamos a importar render, screen y fireEvent en nuestro fichero de test, las depedencias de Unicon para poder testear el componetne con iconos y el componente que vamos a testear (Ratings). Inicializamos Unicon como hicimos en el main.js:

import { render, screen, fireEvent } from "@testing-library/vue";
import Vue from "vue";
import Unicon from "vue-unicons";
import { uniStar, uniStarMonochrome } from "vue-unicons/src/icons";
import Ratings from "./Ratings";

Unicon.add([uniStar, uniStarMonochrome]);
Vue.use(Unicon);

Antes de continuar, vamos a añadir un par de propiedades a nuestro Ratings.vue para poder acceder a los iconos:

<ul>
  <li
    v-for="n in maxStars"
    :key="n"
    role="rating-icon"
    :data-testid="`icon-${n}`"
    @click="setRate(n)"
  >
    <unicon
      name="star"
      fill="orange"
      :data-testid="n <= stars ? 'filled-star' : 'empty-star'"
      :icon-style="n <= stars ? 'monochrome' : 'line'"
    />
  </li>
</ul>

El primer test que vamos a hacer es comprobar que se renderizan tantas estrellas como indicamos con la propiedad maxStars:

describe("Ratings.vue", () => {
  it("should render as many stars as we indicate using the maxStars property", () => {
    render(Ratings, {
      props: {
        maxStars: 10
      }
    });
    expect(screen.queryAllByRole("rating-icon").length).toBe(10);
  });
});

El segundo caso será comprobar que hay tantas estrellas filled como indicamos en selectedStars:

  it("should render as many filled stars as we indicate using the selectedStars property", () => {
    render(Ratings, {
      props: {
        maxStars: 10,
        selectedStars: 6
      }
    });
    expect(screen.queryAllByTestId("filled-star").length).toBe(6);
    expect(screen.queryAllByTestId("empty-star").length).toBe(4);
  });

En el tercer caso, comprobaremos que el texto aparece y es correcto:

  it("should show a text with the amount of selected stars", () => {
    render(Ratings, {
      props: {
        maxStars: 10,
        selectedStars: 6
      }
    });
    expect(screen.getByText("6 de 10")).toBeTruthy();
  });

Y en el último comprobaremos que al seleccionar otro rating, el numero de estrellas rellenadas y el numero de estrellas vacias cambian, así como el texto:

  it("should change the rating when select another one", async () => {
    render(Ratings, {
      props: {
        maxStars: 10,
        selectedStars: 6
      }
    });

    const icons = screen.queryAllByRole("rating-icon");
    expect(icons.length).toBe(10);
    expect(screen.queryAllByTestId("filled-star").length).toBe(6);
    expect(screen.queryAllByTestId("empty-star").length).toBe(4);
    expect(screen.getByText("6 de 10")).toBeTruthy();
    await fireEvent.click(screen.getByTestId("icon-2"));
    expect(screen.queryAllByTestId("filled-star").length).toBe(2);
    expect(screen.queryAllByTestId("empty-star").length).toBe(8);
    expect(screen.getByText("2 de 10")).toBeTruthy();
  });

Y con esto ya tendríamos un 100% de cobertura en nuestro componente.

Espero que te sea de utilidad y si tienes sugerencias o mejores enfoques no dudes en comentar, este articulo es basado en mi aprendizaje de Vue.

¡Un saludo!