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

Test unitarios usando JEST y vue-test-utils

En la segunda parte de los test unitarios en Vue aprendimos a realizar los test de las propiedades y las propiedades computadas.

En esta ocasión comenzaremos con unos test que van a simular la entrada de datos de un usuario y la emisión de eventos.

A continuación el código que vamos a usar para realizar nuestros test.

Home.vue

<template>
  <div class="home">
    <hr />
    <the-header/>
    <hr />
    <child-component />
    <hr />
    <parent-component :message="valueParent" />
    <div class="drag"></div>
  </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: '',
      showModal: false,
      showError: false,
    };
  },
  mounted() {
    document.querySelector('.drag').addEventListener('mousemove', (evt) => {
      if (evt.buttons === 1) {
        console.log(evt, evt.button, evt.buttons);
      }
    });
  },
  methods: {
    sendData() {
      if (this.valueParent.length > 0) {
        this.showError = false;
        this.showModal = true;
        setTimeout(() => {
          this.showModal = false;
          this.valueParent = '';
        }, 2000);
      } else {
        this.showError = true;
      }
    },
  },
  components: {
    theHeader,
    ChildComponent,
    ParentComponent,
  },
};
</script>

<style lang="css" scoped>
  .home {
    margin: 10px;
  }
  .drag {
    border: 1px solid #cccccc;
    height: 400px;
  }

</style>

TheHeader.vue

<template>
  <div>
    <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>
        <hr />
    <input
      type="text"
      v-model="valueParent"
      placeholder="Texto padre"
      :class="{'error': showError}"
    >
    <button @click="sendData">Enviar</button>
    <div :class="['msnSendData', {'hide': !showModal}]">Datos enviados</div>
  </div>
</template>

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

<style lang="scss" scoped>
  .msnSendData {
    margin: 10px;
    box-shadow: 0 0 10px 1px rgba(0,0,0,.2);
    padding: 10px;
    transition: 180ms ease-in-out;
    opacity: 1;
    width: 200px;
  }
  .hide {
    opacity: 0;
    transition: 180ms ease-in-out;
  }
  .error {
    box-shadow: 0 0 10px 4px rgba(255,0,0,.2);
    border: 0;
    padding: 5px;
    margin-right: 10px;
  }
  input {
    margin-right: 10px;
  }
.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>

El test que vamos a realizar es muy simple, escribimos un texto en el elemento input y si este tiene algún valor mostrara un pequeño popup.

En caso de no tener ningún contenido cambiara el estilo del elemento input.

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');
    expect(wrapperMount.vm.propComputed).toBe('Propiedad computada: vue-test-utils');
  });
});

describe('Probando la interación del usuario', () => {
  it('Comprobamos si añadimos algún valor y se muestra un mensaje', async () => {
    const wrapper = mount(TheHeader);
    wrapper.find('input[type=text]').setValue('11111111111111');
    wrapper.find('button').trigger('click');
    await wrapper.vm.$nextTick();
    expect(wrapper.find('.msnSendData').text()).toBe('Datos enviados');
  });
  it('Comprobamos si no añadimos ningún valor, tiene que cambiar su aspecto', async () => {
    const wrapper = mount(TheHeader);
    wrapper.find('input[type=text]').setValue('');
    wrapper.find('button').trigger('click');
    await wrapper.vm.$nextTick();
    expect(wrapper.find('input[type=text]').classes()).toContain('error');
  });
});

Con el test anterior hemos aprendido a usar algunas de las herramientas que ofrece vue-test-utils.

Por un lado hemos usado la palabra reservada trigger, que nos permite manipular los eventos.

También hemos usado setValue, que nos permite añadir un valor a un elemento input.

Y por último hemos usado también la función nexttick, que nos permite esperar a que se actualice el DOM y asegurarnos que el cambio que vaya a ocurrir se refleje correctamente.

Para terminar esta entrada veremos como realizar los test unitarios para emitir eventos.

Simulando la emisión de eventos

La emisión de eventos también es una parte importante ya que nos permite simular la comunicación desde el interior del componente (hijo) hacia fuera (padre).

Para ello vue-test-utils nos ofrece una API que nos permite realizar de una forma muy simple esta tarea.

A continuación un ejemplo.

TheHeader.vue

<template>
  <div>
    <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>
    <hr />
    <input
      type="text"
      v-model="valueParent"
      placeholder="Texto padre"
      :class="{'error': showError}"
    >
    <button @click="sendData">Enviar</button>
    <div :class="['msnSendData', {'hide': !showModal}]">Datos enviados</div>
  </div>
</template>

<script>
export default {
  name: 'the-header',
  props: {
    title: {
      type: String,
      default: '',
    },
  },
  data() {
    return {
      valueParent: '',
      showModal: false,
      showError: false,
    };
  },
  methods: {
    sendData() {
      if (this.valueParent.length > 0) {
        this.$emit('send-data', { status: true, value: this.valueParent });
        this.showError = false;
        this.showModal = true;
        setTimeout(() => {
          this.showModal = false;
          this.valueParent = '';
          this.$emit('send-data', { status: false, value: this.valueParent });
        }, 2000);
      } else {
        this.showError = true;
      }
    },
  },

};
</script>

<style lang="scss" scoped>
.msnSendData {
  margin: 10px;
  box-shadow: 0 0 10px 1px rgba(0,0,0,.2);
  padding: 10px;
  transition: 180ms ease-in-out;
  opacity: 1;
  width: 200px;
}
.hide {
  opacity: 0;
  transition: 180ms ease-in-out;
}
.error {
  box-shadow: 0 0 10px 4px rgba(255,0,0,.2);
  border: 0;
  padding: 5px;
  margin-right: 10px;
}
input {
  margin-right: 10px;
}
.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>

Home.vue

<template>
  <div class="home">
    <hr />
    <the-header @send-data="data" />
    Valor emitido the-header: <b v-text="dataHeader"></b>
    <hr />
    <child-component />
    <hr />
    <parent-component :message="valueParent" />
    <div class="drag"></div>
  </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: '',
      dataHeader: '',
    };
  },
  mounted() {
    document.querySelector('.drag').addEventListener('mousemove', (evt) => {
      if (evt.buttons === 1) {
        console.log(evt, evt.button, evt.buttons);
      }
    });
  },
  methods: {
    data(status) {
      this.dataHeader = status;
    },
  },
  components: {
    theHeader,
    ChildComponent,
    ParentComponent,
  },
};
</script>

<style lang="css" scoped>
  .home {
    margin: 10px;
  }
  .drag {
    border: 1px solid #cccccc;
    height: 400px;
  }

</style>

theheader.spec.js

...
...
describe('Probando la emisión de eventos', () => {
  it('Comprobamos que se emite el evento con un objeto', () => {
    const wrapper = mount(TheHeader);
    wrapper.setData({ valueParent: 'Texto de prueba' });
    wrapper.vm.sendData();
    expect(wrapper.emitted()['send-data'][0]).toEqual([{ status: true, value: 'Texto de prueba' }]);
  });
});
...
...

Debemos saber que emitted().nombreEvento o emitted()[‘nombre-evento’] nos devuelve un array con todos los eventos que se han emitido.

Como vemos el test es muy simple, no obstante veremos los pasos que se han realizado para ejecutar este test:

  1. Hemos añadido un valor a una variable del data, ya que es necesario para ejecutar el siguiente contenido.
  2. A continuación llamamos al método que se encarga de lanzar el evento y que comprueba que este valor del data no este vacío.
  3. Finalmente realizamos la aserción para asegurar que el test ha sido correcto.

En este ejemplo ademas de aprender como se realiza un test sobre la emisión de eventos, también hemos aprendido como añadir un valor a una variable del data y como ejecutar un método.

Con esto terminamos la tercera entrega de test unitarios en Vue usando vue-test-utils, y como siempre podéis acceder al repositorio en Github.