Gabriel Jiménez | Hace 8 días
En un artículo anterior desarrollamos un Navbar desde 0 con React, aprendimos qué es un Navbar, qué componentes lo forman y cómo desarrollarlo paso a paso. Si no lo has visto, puedes revisarlo aquí Cómo hacer un Navbar con react desde 0.
Para este artículo, nos enfocaremos en aprender porque es importante probarlo, que herramientas necesito y terminaremos con un ejemplo práctico.
Un navbar es un componente UI utilizado principalmente para permitir al usuario navegar a través de nuestra aplicación.
Un componente Navbar se componente de otros sub componentes:
Componente Brand
Encargado de mostrar el nombre y/o logo de la aplicación.
export const Brand = ({ title, logo }) => {
if (!children && !logo) {
throw new Error("Brand requiere al menos un texto o un logo.")
}
if (children && typeof children !== "string") {
throw new Error("Brand solo acepta texto como children.")
}
return (
<>
{logo && <img src={logo} alt="logo" />}
{children && <span>{title}</span>}
</>
)
}
Componente Nav
Agrupa una lista de elementos de navegación.
export const NavItem = ({ to, children }) => {
if (typeof to !== "string" || to.trim() === "") {
throw new Error("NavItem requiere un prop 'to' no vacío.")
}
if (typeof children !== "string") {
throw new Error("NavItem solo acepta texto como children.")
}
return (
<a href={to} className="nav-item">
{children}
</a>
)
}
export const Nav = ({ children }) => {
const items = Children.toArray(children)
items.forEach((child) => {
if (!React.isValidElement(child) || child.type !== NavItem) {
throw new Error("Nav solo puede renderizar hijos de tipo <NavItem />.")
}
})
return <div className="navbar-nav">{items}</div>
}
Componente Toggler
Permite ocultar o mostrar el menú de navegación.
export const Toggler = ({ initialOpen, onChange, icon = "☰" }) => {
const [open, setOpen] = useState(initialOpen)
const handleClick = () => {
const next = !open
setOpen(next)
if (onChange) onChange(next)
}
return (
<button
type="button"
className="navbar-toggler"
onClick={handleClick}
>
{icon}
</button>
)
}
Componente Actions
Agrupa elementos que no son necesariamente elementos de navegación como por ejemplo: formularios, botones, buscadores, entre otros.
export const Action = ({ children }) => {
return <div className="action">{children}</div>
}
export const Actions = ({ children }) => {
const items = Children.toArray(children)
items.forEach((child) => {
if (!React.isValidElement(child) || child.type !== Action) {
throw new Error("Actions solo puede renderizar hijos de tipo <Action />.")
}
})
return <div className="navbar-actions">{items}</div>
}
Componente NavBar
Contiene la lógica que se encarga de orquestar como los componentes trabajan entre si.
export const NavBar = ({ children }) => {
let brand = null
let actions = null
let nav = null
let toggler = null
const items = Children.toArray(children)
items.forEach((child) => {
if (!React.isValidElement(child)) {
throw new Error("NavBar solo puede renderizar componentes válidos.")
}
switch (child.type) {
case Brand:
brand = child
break
case Actions:
actions = child
break
case Nav:
nav = child
break
case Toggler:
toggler = child
break
default:
throw new Error(
"NavBar solo puede renderizar hijos de tipo <Brand />, <Actions />, <Nav /> o <Toggler />."
)
}
})
const initialOpen = toggler ? !!toggler.props?.initialOpen : true
const [isOpen, setIsOpen] = useState(initialOpen)
const handleToggle = (next) => {
setIsOpen(next)
if (toggler?.props?.onChange) toggler.props.onChange(next)
}
const togglerWithHandlers =
toggler &&
cloneElement(toggler, {
onChange: handleToggle,
initialOpen: initialOpen,
})
return (
<nav className="navbar">
{brand}
{isOpen && nav}
{actions}
{togglerWithHandlers}
</nav>
)
}
export default NavBar
Es importante porque puede contener lógica de negocio como: un formulario para crear un recurso, un buscador para localizar un producto, acciones que desencadenen funcionalidades específicas. Además, es un componente que puede ser reutilizado en diferentes lugares de nuestra aplicación.
Las herramientas más utilizadas para probar nuestros componentes en React son:
Jest. Permitir escribir, ejecutar y evaluar nuestras pruebas.
React Testing Library. Facilita probar nuestros componentes React, al proporcionarnos funciones específicas para montar, buscar, trabajar con peticiones asíncronas, y más.
Si quieres profundizar en cada herramienta, puedes revisar cómo funciona Jest en React
y cómo utilizar React Testing Library para probar componentes de forma más realista.
Como vimos anteriormente, el Navbar se componente sub componentes. Podemos probar cada uno de ellos de forma aislada o probar directamente el componente Navbar. Cualquiera de las dos opciones es totalmente valida, para este ejemplo práctico probaremos cada uno de forma individual.
Para el componente Brand, vamos a probar lo siguiente:
File: src/Navbar/Navbar.test.jsx
. . .
4: describe("Brand", () => {
5: it("lanza error si no se envía ni title ni logo", () => {
6: expect(() => {
7: {/* No se envía ningún prop válido, debe lanzar error */}
8: render(<Brand />)
9: }).toThrow("Brand requiere al menos un texto o un logo.")
10: })
11:
12: it("lanza error si title no es una cadena", () => {
13: expect(() => {
14: {/* title no es texto, esto provoca el error */}
15: render(<Brand title={<button>Clic</button>} />)
16: }).toThrow("Brand solo acepta texto como title.")
17: })
18:
19: it("renderiza correctamente cuando se envía title o logo", () => {
20: render(
21: <Brand title="Mi App" logo="logo" />
22: )
23:
24: expect(document.body.textContent).toContain("Mi App")
25:
26: const img = screen.getByRole("img", { name: /logo/i })
27: expect(img).toBeInTheDocument()
28: })
29: })
El componente Nav es un poco más complejo que el Brand, ya que se componente de otro componente. Podríamos probarlos de forma aislada cada uno, sin embargo, para este caso veamos como probar únicamente uno de ellos.
Para decidir que componente probar, solo debemos analizar que componente es el padre, en este caso, Nav utiliza a NavItem como proveedor.
Probaremos lo siguiente:
File: src/Navbar/Navbar.test.jsx
. . .
29: describe("Nav", () => {
30: it("Nav solo debe permitir componentes de tipo NavItem", () => {
31: expect(() => {
32: render(
33: <Nav>
34: <NavItem to="/home">Home</NavItem>
35: {/* Este hijo no es un NavItem, debe lanzar un error */}
36: <div>Otro</div>
37: </Nav>
38: )
39: }).toThrow("Nav solo puede renderizar hijos de tipo <NavItem />.")
40: })
41:
42: it("NavItem recibe un prop 'to' de tipo cadena", () => {
43: expect(() => {
44: render(
45: <Nav>
46: {/* 'to' no es string, esto debe provocar el error */}
47: <NavItem to={123}>Home</NavItem>
48: </Nav>
49: )
50: }).toThrow("NavItem requiere un prop 'to' no vacío.")
51: })
52:
53: it("NavItem recibe un prop 'children' de tipo cadena", () => {
54: expect(() => {
55: render(
56: <Nav>
57: <NavItem to="/home">
58: {/* children no es texto, debe lanzar error */}
59: <span>Home</span>
60: </NavItem>
61: </Nav>
62: )
63: }).toThrow("NavItem solo acepta texto como children.")
64: })
65:
66: it("NavItem se renderiza correctamente con 'to' y 'children'", () => {
67: render(
68: <Nav>
69: <NavItem to="/home">Home</NavItem>
70: </Nav>
71: )
72:
73: const link = screen.getByRole("link", { name: "Home" })
74: expect(link).toHaveAttribute("href", "/home")
75: })
76: })
Para este componente, simularemos que al hacer clic el componente Nav se muestre y al volver a hacer clic se oculte.
File: src/Navbar/Navbar.test.jsx
. . .
80: describe("Toggler", () => {
81: it("al hacer clic alterna entre mostrar y ocultar el Nav", () => {
82: const onChange = jest.fn()
83:
84: render(<Toggler initialOpen={false} onChange={onChange} />)
85:
86: const button = screen.getByRole("button")
87:
88: // Primer clic: se abre
89: fireEvent.click(button)
90: expect(onChange).toHaveBeenLastCalledWith(true)
91:
92: // Segundo clic: se cierra
93: fireEvent.click(button)
94: expect(onChange).toHaveBeenLastCalledWith(false)
95: })
96: })
Probaremos que:
File: src/Navbar/Navbar.test.jsx
. . .
98: describe("Actions", () => {
99: it("Actions solo permite componentes de tipo Action", () => {
100: expect(() => {
101: render(
102: <Actions>
103: <Action>Guardar</Action>
104: {/* Este hijo no es un Action, debe lanzar error */}
105: <div>Otro</div>
106: </Actions>
107: )
108: }).toThrow("Actions solo puede renderizar hijos de tipo <Action />.")
109: })
110:
111: it("Actions se renderiza correctamente con componentes Action", () => {
112: render(
113: <Actions>
114: <Action>Guardar</Action>
115: <Action>Cancelar</Action>
116: </Actions>
117: )
118:
119: const bodyTextContent = document.body.textContent
120: expect(bodyTextContent).toContain("Guardar")
121: expect(bodyTextContent).toContain("Cancelar")
122: })
123: })
Para el componente padre, validaremos:
File: src/Navbar/Navbar.test.jsx
. . .
125: describe("NavBar", () => {
126: it("solo permite componentes Brand, Nav, Actions y Toggler", () => {
127: expect(() => {
128: render(
129: <NavBar>
130: <Brand title="Mi App" />
131: <Nav>
132: <NavItem to="/home">Home</NavItem>
133: </Nav>
134: <Actions>
135: <Action>Guardar</Action>
136: </Actions>
137: <div>Invalido</div>
138: <Toggler initialOpen={true} />
139: </NavBar>
140: )
141: }).toThrow(
142: "NavBar solo puede renderizar hijos de tipo <Brand />, <Actions />, <Nav /> o <Toggler />."
143: )
144: })
145:
146: it("se renderiza correctamente y el Nav se muestra/oculta con el Toggler", () => {
147: render(
148: <NavBar>
149: <Brand title="Mi App" />
150: <Nav>
151: <NavItem to="/home">Home</NavItem>
152: </Nav>
153: <Actions>
154: <Action>Guardar</Action>
155: </Actions>
156: <Toggler initialOpen={false} icon="☰" />
157: </NavBar>
158: )
159:
160: // Render base
161: expect(screen.getByText("Mi App")).toBeInTheDocument()
162: expect(screen.getByText("Guardar")).toBeInTheDocument()
163:
164: // initialOpen=false => Nav oculto
165: expect(screen.queryByRole("link", { name: "Home" })).toBeNull()
166:
167: // Click => Nav visible
168: fireEvent.click(screen.getByRole("button", { name: "☰" }))
169: expect(screen.getByRole("link", { name: "Home" })).toBeInTheDocument()
170:
171: // Click otra vez => Nav oculto
172: fireEvent.click(screen.getByRole("button", { name: "☰" }))
173: expect(screen.queryByRole("link", { name: "Home" })).toBeNull()
174: })
175: })
Al probar nuestros componentes de forma aislada en muchos de los casos es mejor, ya que cada uno tiene su propia lógica. En algunos casos conviene probarlos en conjunto como en el caso del componente Nav, donde su componente hijo era bastante sencillo. Lo importante es comenzar a escribir nuestras primeras pruebas para facilitar la escalabilidad de nuestros componentes con el tiempo.
¿Quieres aprender más casos reales de testing en React?
Explora el hub de Testing en React con guías, ejemplos y buenas prácticas paso a paso.