Test unitarios en VUE (JEST & Vue-Test-Utils) – Parte 2

Test unitarios usando JEST y vue-test-utils

En la primera parte de los test unitarios en Vue aprendimos a instalar las herramientas necesarias y creamos un test muy sencillo.

Bien, pues en esta y las sucesivas entradas del blog aprenderemos a usar la librería oficial de test unitarios para Vue.

Comencemos, Vue-test-utils nos ofrece la posibilidad de montar nuestros componentes de dos formas diferentes:

  • Usando shallowMount: Monta el componente sin sus hijos.
  • Usando mount: Monta el componente con sus hijos.

En ambos casos cuando el componente es montado este nos devuelve un wrapper (objeto) que contiene el componente de Vue, ademas de algunos métodos para realizar test.

Para entender y ver la diferencia vamos a realizar unas pruebas y mostraremos el resultado en ambos casos que nos devuelve Jest.

// Child.vue
<template>
  <div>
    HIJO
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
};
</script>
// Parent.vue
<template>
  <div>
    PADRE
    <child-component />
  </div>
</template>

<script>
import ChildComponent from './Child.vue';

export default {
  name: 'ParentComponent',
  components: {
    ChildComponent,
  },
};
</script>
// Home.vue
<template>
  <div class="home">
    <hr />
    <the-header/>
    <hr />
    <child-component />
    <hr />
    <parent-component />
  </div>
</template>

<script>
import theHeader from '@/components/TheHeader.vue';
import ChildComponent from '@/components/Child.vue';
import ParentComponent from '@/components/Parent.vue';

export default {
  name: 'home',
  components: {
    theHeader,
    ChildComponent,
    ParentComponent,
  },
};
</script>

El código anterior muestra la creación y montaje de los componentes en nuestra aplicación de pruebas.

A continuación el código para realizar nuestro test y ver el contenido.

import { mount, shallowMount } from '@vue/test-utils';
import TheHeader from '@/components/TheHeader.vue';
import ChildComponent from '@/components/Child.vue';
import ParentComponent from '@/components/Parent.vue';

describe('Componente TheHeader.vue', () => {
  const wrapper = mount(TheHeader);
  it('Comprobamos si el test funciona', () => {
    expect(true).toBeTruthy();
  });
  it('El componente se ha pintado', () => {
    expect(wrapper.vm.$refs.menu).not.toBeUndefined();
  });
  it('Comprobamos que el título se esta pintando', () => {
    wrapper.setProps({ title: 'Menú APP' });
    expect(wrapper.html().includes('Menú APP')).toBeTruthy();
  });
});

describe('Probamos las dos formas de montar componentes', () => {
  describe('Comprobamos el componente hijo', () => {
    const wrapperMount = mount(ChildComponent);
    const wrapperShallowMount = shallowMount(ChildComponent);
    console.log('---- Mount ----');
    console.log(wrapperMount.html());
    console.log('---------------');
    console.log('---- shallowMount ----');
    console.log(wrapperShallowMount.html());
    console.log('----------------------');
  });
  describe('Comprobamos el componente padre', () => {
    const wrapperMount = mount(ParentComponent);
    const wrapperShallowMount = shallowMount(ParentComponent);
    console.log('---- Mount ----');
    console.log(wrapperMount.html());
    console.log('---------------');
    console.log('---- shallowMount ----');
    console.log(wrapperShallowMount.html());
    console.log('----------------------');
  });
});

Ahora comprobaremos el resultado y podremos ver la diferencia entre mount shallowMount.

 PASS  tests/unit/theheader.spec.js
  Componente TheHeader.vue
    √ Comprobamos si el test funciona (2ms)
    √ El componente se ha pintado
    √ Comprobamos que el título se esta pintando (3ms)
  Probamos las dos formas de montar componentes
    Comprobamos el componente hijo
      √ Mount child (14ms)
      √ shallowMount child (3ms)
    Comprobamos el componente padre
      √ Mount Parent (3ms)
      √ shallowMount Parent (2ms)

  console.log tests/unit/theheader.spec.js:25
    Mount child

  console.log tests/unit/theheader.spec.js:26
    <div>
      HIJO
    </div>

  console.log tests/unit/theheader.spec.js:29
    shallowMount child

  console.log tests/unit/theheader.spec.js:30
    <div>
      HIJO
    </div>

  console.log tests/unit/theheader.spec.js:37
    Mount Parent

  console.log tests/unit/theheader.spec.js:38
    <div>
      PADRE
      <div>
      HIJO
    </div></div>

  console.log tests/unit/theheader.spec.js:41
    shallowMount Parent

  console.log tests/unit/theheader.spec.js:42
    <div>
      PADRE
      <child-component-stub></child-component-stub></div>

Test Suites: 1 passed, 1 total
Tests:       7 passed, 7 total
Snapshots:   0 total
Time:        1.967s, estimated 2s
Ran all test suites.

Si nos fijamos, podemos ver que el Padre que contiene un hijo cuando usamos shallowMount nos ha montado una especie de componente «falso» (<child-component-stub></child-component-stub>) que sustituye al real.

Esto nos puede resultar muy útil cuando necesitamos aislar los test que tenemos que realizar sobre el padre y no necesitamos a sus hijos.

En este punto ya sabemos como montar los componentes que vamos a testear.

A continuación veremos como se realizan los test sobre las propiedades los componentes.

Realizando test sobre las propiedades

Para realizar los test sobre las propiedades de nuestro componentes, vue-test-utils ofrece tanto para mount como shallowMount un segundo parámetro llamado propsData.

Este segundo parámetro nos permite añadir las propiedades que necesitamos para montar nuestro componente con una configuración inicial.

describe('Montamos los componentes modificando las propiedades', () => {
  it('Mount Parent con las nuevas propiedades usando propsData', () => {
    const wrapperMount = mount(ParentComponent, {
      propsData: {
        message: 'Mensaje prueba jest / vue-test-utils',
      },
    });
    expect(wrapperMount.find('ul li:nth-of-type(2)').text()).toBe('message: Mensaje prueba jest / vue-test-utils');
  });
});

Ademas de propsData, también podemos usar setProps para añadir propiedades.

describe('Montamos los componentes modificando las propiedades', () => {
  it('Mount Parent usando setProps', () => {
    const wrapperMount = mount(ParentComponent);
    wrapperMount.setProps({ message: 'Mensaje prueba jest / vue-test-utils' });
    expect(wrapperMount.find('ul li:nth-of-type(2)').text()).toBe('message: Mensaje prueba jest / vue-test-utils');
  });
});

Otra parte importante dentro de Vue son las propiedades computadas, así que ahora veremos como se realizan los test.

Propiedades computadas

Los test sobre las propiedades computadas son muy sencillos, ya que únicamente deberemos llamarlas como lo haríamos en nuestro código.

describe('Probando las propiedades computadas', () => {
  it('Probamos las propiedades computadas', () => {
    const wrapperMount = mount(ParentComponent);
    expect(wrapperMount.find('ul li:nth-of-type(3)').text()).toBe('Propiedad computada:');
    wrapperMount.setProps({ message: 'vue-test-utils' });
    expect(wrapperMount.find('ul li:nth-of-type(2)').text()).toBe('message: vue-test-utils');
    expect(wrapperMount.find('ul li:nth-of-type(3)').text()).toBe('Propiedad computada: vue-test-utils');
    // Propiedad computada
    expect(wrapperMount.vm.propComputed).toBe('Propiedad computada: vue-test-utils');
  });
});

Para terminar esta segunda parte del tutorial añadiré tanto el código de los componentes como el del test unitario.

Home.vue

<template>
  <div class="home">
    <hr />
    <the-header/>
    <hr />
    <input type="text" v-model="valueParent" placeholder="Texto padre">
    <hr />
    <child-component />
    <hr />
    <parent-component :message="valueParent" />
  </div>
</template>

<script>
import theHeader from '@/components/TheHeader.vue';
import ChildComponent from '@/components/Child.vue';
import ParentComponent from '@/components/Parent.vue';

export default {
  name: 'home',
  data() {
    return {
      valueParent: '',
    };
  },
  components: {
    theHeader,
    ChildComponent,
    ParentComponent,
  },
};
</script>

<style lang="css" scoped>
  .home {
    margin: 10px;
  }
</style>

TheHeader.vue

<template>
  <nav class="menu" ref="menu">
    <ul>
      <li class="selected"><a href="#">Home</a></li>
      <li class="selected"><a href="#">About</a></li>
      <li class="selected"><a href="#">Contact</a></li>
      <li v-text="title"></li>
    </ul>
  </nav>
</template>

<script>
export default {
  name: 'the-header',
  props: {
    title: {
      type: String,
      default: '',
    },
  },
};
</script>

<style lang="scss" scoped>
.menu {
  box-shadow: 2px 2px 5px rgba(0,0,0,0.2);
  ul {
    list-style: none;
    background: #333;
    display: flex;
    align-items: center;
    color: white;
    li {
      padding: 10px;
      cursor: pointer;
      &.selected {
        transition: ease-in-out 220ms;
        a {
          color: white;
          display: block;
          text-decoration: none;
        }
        &:hover {
          background: cadetblue;
          color: white;
          transition: ease-in-out 220ms;
        }
      }
    }
  }
}


</style>

Parent.vue

<template>
  <div>
    <hr>
    PADRE
    <ul>
      <li>PROPS</li>
      <li>message: {{ message }}</li>
      <li v-text="propComputed"></li>
    </ul>
    <hr>
    <child-component />
  </div>
</template>

<script>
import ChildComponent from './Child.vue';

export default {
  name: 'ParentComponent',
  components: {
    ChildComponent,
  },
  props: {
    message: {
      type: String,
      default: '',
    },
  },
  computed: {
    propComputed() {
      return `Propiedad computada: ${this.message}`;
    },
  },
};
</script>

Child.vue

<template>
  <div>
    HIJO
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
};
</script>

theheader.spec.js

import { mount, shallowMount } from '@vue/test-utils';
import TheHeader from '@/components/TheHeader.vue';
import ChildComponent from '@/components/Child.vue';
import ParentComponent from '@/components/Parent.vue';

describe('Componente TheHeader.vue', () => {
  const wrapper = mount(TheHeader);
  it('Comprobamos si el test funciona', () => {
    expect(true).toBeTruthy();
  });
  it('El componente se ha pintado', () => {
    expect(wrapper.vm.$refs.menu).not.toBeUndefined();
  });
  it('Comprobamos que el título se esta pintando', () => {
    wrapper.setProps({ title: 'Menú APP' });
    expect(wrapper.html().includes('Menú APP')).toBeTruthy();
  });
});

describe('Probamos las dos formas de montar componentes', () => {
  describe('Comprobamos el componente hijo', () => {
    // const wrapperMount = mount(ChildComponent);
    // const wrapperShallowMount = shallowMount(ChildComponent);
    it('Mount child', () => {
      // console.log('Mount child');
      // console.log(wrapperMount.html());
    });
    it('shallowMount child', () => {
      // console.log('shallowMount child');
      // console.log(wrapperShallowMount.html());
    });
  });
  describe('Comprobamos el componente padre', () => {
    // const wrapperMount = mount(ParentComponent);
    // const wrapperShallowMount = shallowMount(ParentComponent);
    it('Mount Parent', () => {
      // console.log('Mount Parent');
      // console.log(wrapperMount.html());
    });
    it('shallowMount Parent', () => {
      // console.log('shallowMount Parent');
      // console.log(wrapperShallowMount.html());
    });
  });
});

describe('Montamos los componentes modificando las propiedades', () => {
  it('Mount Parent con las nuevas propiedades usando propsData', () => {
    const wrapperMount = mount(ParentComponent, {
      propsData: {
        message: 'Mensaje prueba jest / vue-test-utils',
      },
    });
    expect(wrapperMount.find('ul li:nth-of-type(2)').text()).toBe('message: Mensaje prueba jest / vue-test-utils');
  });
  it('Mount Parent usando setProps', () => {
    const wrapperMount = mount(ParentComponent);
    wrapperMount.setProps({ message: 'Mensaje prueba jest / vue-test-utils' });
    expect(wrapperMount.find('ul li:nth-of-type(2)').text()).toBe('message: Mensaje prueba jest / vue-test-utils');
  });
});

describe('Probando las propiedades computadas', () => {
  it('Probamos las propiedades computadas', () => {
    const wrapperMount = mount(ParentComponent);
    expect(wrapperMount.find('ul li:nth-of-type(3)').text()).toBe('Propiedad computada:');
    wrapperMount.setProps({ message: 'vue-test-utils' });
    expect(wrapperMount.find('ul li:nth-of-type(2)').text()).toBe('message: vue-test-utils');
    expect(wrapperMount.find('ul li:nth-of-type(3)').text()).toBe('Propiedad computada: vue-test-utils');
    // Propiedad computada
    expect(wrapperMount.vm.propComputed).toBe('Propiedad computada: vue-test-utils');
  });
});