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.
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');
}
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>;
}
}
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} />;
}
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:
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="" />;
Top comments (0)
Some comments have been hidden by the post's author - find out more