DEV Community

IagoLast
IagoLast

Posted on

Simplificando el Testing de componentes mediante ViewComponents

Uno de los grandes avances en el mundo del frontend ha sido la aparición de Storybook, una herramienta que nos permite previsualizar componentes de forma aislada y controlada. Por ejemplo, podemos ver cómo se renderiza nuestro componente <Pill/> ante diferentes combinaciones de atributos.

Screenshot 2021-09-26 at 13.34.11

Screenshot 2021-09-26 at 13.34.40

A partir de Storybook nace Chromatic una herramienta que nos permite realizar tests de regresión visual para comprobar en cada Pull requests que tanto el comportamiento como la visualización de nuestros componentes es correcta.

Aunque estos tests resultan tremendamente útiles mucha gente encuentra complicado probar de forma sencilla los diferentes estados en los que se puede encontrar su componente. Normalmente esto sucede porque los componentes están muy acoplados, hacen peticiones a terceros, necesitas muchos clicks para obtener el estado deseado...

Una de mis soluciones favoritas a este problema es crear componentes de vista sin estado, es decir, crear componentes puramente funcionales donde todo lo que se renderiza depende exclusivamente de los parámetros que se le pasen, pongamos un ejemplo:

Este es el componente Usuario cuya funciónalidad consiste en hacer una petición a una API REST y mostrar el nombre de usuario que contiene la respuesta a la petición. Según el estado de la red mostrará un contenido diferente:

  • Loading escrito en color gris cuando el estado es "idle o loading"
  • Error escrito en color rojo cuando el estado es "error"
  • El nombre del usuario obtenido desde la red cuando el estado es "success".
import { useEffect, useState } from 'react';

export default function UserComponent() {
    const [state, setState] = useState({
        networkStatus: 'idle',
        username: '',
    });

    useEffect(function init() {
        setState({ networkStatus: 'loading', username: '' });
        fetch('https://jsonplaceholder.typicode.com/users/1')
            .then((res) => res.json())
            .then((res) => {
                setState({ networkStatus: 'success', username: res.name });
            })
            .catch((err) => {
                setState({ networkStatus: 'error', username: '' });
            });
    }, []);

    if (state.networkStatus === 'idle') {
        return <span style={{ color: 'gray' }}> idle </span>;
    }

    if (state.networkStatus === 'loading') {
        return <span style={{ color: 'gray' }}> Loading</span>;
    }

    if (state.networkStatus === 'error') {
        return <span style={{ color: 'red' }}> error</span>;
    }

    if (state.networkStatus === 'success') {
        return <span style={{ color: 'green' }}> {state.username} </span>;
    }

    throw Error('Unexpected network status');
}
Enter fullscreen mode Exit fullscreen mode

Como se puede ver tenemos efectos secundarios en nuestro test (una petición de red) que causan múltiples inconvenientes.

Si por ejemplo queremos probar el estado de error tendríamos que forzar un fallo en la red y el test se volvería más difícil de escribir. O si la red falla el test resultará en un falso positivo lo que a la larga hará que no confiemos en sus resultados y lo ignoremos.

Una forma sencilla de librarse de esto es aplicando un poco de arquitectura de software y separar el componente original en dos componentes: Uno encargado de la lógica y otro encargado de la presentación.

El encargado de la presentación queda de esta forma:

export interface IUserViewComponentProps {
    username: string;
    status: 'idle' | 'loading' | 'error' | 'success';
}

export default function UserViewComponent(props: IUserViewComponentProps) {
    if (props.status === 'idle') {
        return <span style={{ color: 'gray' }}> idle </span>;
    }

    if (props.status === 'loading') {
        return <span style={{ color: 'gray' }}> Loading</span>;
    }

    if (props.status === 'error') {
        return <span style={{ color: 'red' }}> error</span>;
    }

    if (props.status === 'success') {
        return <span style={{ color: 'green' }}> {props.username} </span>;
    }
}
Enter fullscreen mode Exit fullscreen mode

Es exactamente el mismo código de antes pero sin ningún tipo de efecto secundario o estado interno. Es un componente funcional donde lo que se muestra depende exclusivamente de los valores de los atributos haciendo que sea tremendamente fácil de probar.

El componente original queda reducido a un envoltorio que gestiona el estado e inyecta al componente de vista los atributos correctos:

import { useEffect, useState } from 'react';
import UserViewComponent from './User.view';

export default function UserContainerComponent() {
    const [state, setState] = useState({ networkStatus: 'idle', username: '' });

    useEffect(function init() {
        setState({ networkStatus: 'loading', username: '' });
        fetch('https://jsonplaceholder.typicode.com/users/1')
            .then((res) => res.json())
            .then((res) => {
                setState({ networkStatus: 'success', username: res.name });
            })
            .catch((err) => {
                setState({ networkStatus: 'error', username: '' });
            });
    }, []);

    return <UserViewComponent status={state.networkStatus} username={state.username} />;
}
Enter fullscreen mode Exit fullscreen mode

De esta forma tan sencilla hemos extraído todos los side-effects de nuestro componente y podemos dar cobertura mediante tests visuales a todas las posibilidades utilizando el componente de vista:

Screenshot 2021-09-26 at 14.42.18

El código de los tests con StoryBook:

import UserViewComponent from './User.view';


export const UserComponentStoryIdle = () => <UserViewComponent status="idle" username="" />;

export const UserComponentStoryLoading = () => <UserViewComponent status="loading" username="" />;

export const UserComponentStorySuccess = () => <UserViewComponent status="success" username="John Doe" />;

export const UserComponentStoryError = () => <UserViewComponent status="error" username="" />;
Enter fullscreen mode Exit fullscreen mode

Discussion (1)

Some comments have been hidden by the post's author - find out more