Desarrollo Orientado a Pruebas (TDD) en React paso a paso
Clase 6 de 20 • Curso de React Testing Library
Contenido del curso
Clase 6 de 20 • Curso de React Testing Library
Contenido del curso
Juan Jose Bernal Villamarin
Wilmer Garzon Cabezas
Mariana Valencia Gallego
Wilmer Garzon Cabezas
Cristian Contreras
Wilmer Garzon Cabezas
Elioenai Garcia
Wilmer Garzon Cabezas
Salome Arboleda
Usando TDD lo cree así:
import { describe, it, expect } from 'vitest'; import { render, screen, fireEvent, act } from '@testing-library/react'; import TodoList from './TodoList'; describe('<TodoList />', () => { it('should display input to enter Todo activity ', () => { render(<TodoList />); const input = screen.getByPlaceholderText('Enter Todo Activity'); expect(input).toBeInTheDocument(); }); it('should display button to add Todo', () => { render(<TodoList />); const button = screen.getByText('Add Todo'); expect(button).toBeInTheDocument(); }); it('should display ul empty at the begin', () => { render(<TodoList />); const ul = screen.getByRole('list'); expect(ul).toBeEmptyDOMElement(); }); it('should add todo to the list', async () => { render(<TodoList />); const input = screen.getByPlaceholderText('Enter Todo Activity'); fireEvent.change(input, { target: { value: 'Learn Vitest' } }); const button = screen.getByText('Add Todo'); await act(async () => { fireEvent.click(button); }); const ul = screen.getByRole('list'); expect(ul).not.toBeEmptyDOMElement(); expect(ul).toContainHTML('<li>Learn Vitest</li>'); expect(screen.getByText('Learn Vitest')).toBeInTheDocument(); }); }); ```import { describe, it, expect } from 'vitest';import { render, screen, fireEvent, act } from '@testing-library/react';import TodoList from './TodoList';describe('\<TodoList />', () => { it('should display input to enter Todo activity ', () => { render(*\<TodoList* */>*); const input = screen.getByPlaceholderText('Enter Todo Activity'); expect(input).toBeInTheDocument();   });   it('should display button to add Todo', () => { render(*\<TodoList* */>*); const button = screen.getByText('Add Todo'); expect(button).toBeInTheDocument();   });   it('should display ul empty at the begin', () => { render(*\<TodoList* */>*); const ul = screen.getByRole('list'); expect(ul).toBeEmptyDOMElement(); });   it('should add todo to the list', async () => { render(*\<TodoList* */>*); const input = screen.getByPlaceholderText('Enter Todo Activity'); fireEvent.change(input, { target: { value: 'Learn Vitest' } });   const button = screen.getByText('Add Todo'); await act(async () => { fireEvent.click(button); }); const ul = screen.getByRole('list'); expect(ul).not.toBeEmptyDOMElement(); expect(ul).toContainHTML('\<li>Learn Vitest\</li>'); expect(screen.getByText('Learn Vitest')).toBeInTheDocument(); }); }); 1. src/TodoList/TodoList.tsx ```js import React, {useState} from 'react'; const TodoList = () => { const [todoValue, setTodoValue] = useState(""); const [list, setList] = useState<string[]>([]); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { setTodoValue(e.target.value); } const addTodo = () => { setList([...list, "Learn Vitest"]); } return ( <div> <input type="text" placeholder="Enter Todo Activity" value={todoValue} onChange={handleChange}/> <button onClick={addTodo}>Add Todo</button> <ul> { list.map((item, index) => ( <li key={index}>{item}</li> )) } </ul> </div> ); } export default TodoList; ```import React, {useState} from 'react'; const TodoList = () => { const \[todoValue, setTodoValue] = useState("");   const \[list, setList] = useState\<string\[]>(\[]);   const handleChange = (e: React.ChangeEvent\<HTMLInputElement>) => { setTodoValue(e.target.value); }   const addTodo = () => { setList(\[...list, "Learn Vitest"]); }   return ( *<*div*>* *<*input type="text" placeholder="Enter Todo Activity" value={todoValue} onChange={handleChange}*/>* *<*button onClick={addTodo}*>*Add Todo*\</*button*>* *<*ul*>* { list.map((item, index) => ( *<*li key={index}*>*{item}*\</*li*>* )) } *\</*ul*>* *\</*div*>* );} export default TodoList;
Gran aporte Juan! 💚
Relacionado al setState, siempre que se tenga en cuenta el estado original para sumarle, restarle, etc, es buena práctica hacerlo de esta manera
setContador((prev) => prev + 1)}
De lo contrario, podría dependiendo de la función y cómo esta sea llamada, generar errores en el resultado final y no dar el resultado esperado
Por ejemplo el siguiente bloque de código, escrito como está en el curso, se esperaría que sumara 3 veces pero solo suma 1 vez ya que en el momento en que la función es llamada el valor del estado es 0 y nunca se suma
const handleClick = () => {
setContador(contador + 1);
setContador(contador + 1);
setContador(contador + 1);
};
Gran aporte, muchas gracias Mariana! 💚
Los comportamientos no deterministas se refieren a situaciones en las que el resultado de una operación puede variar en diferentes ejecuciones, incluso con los mismos datos de entrada. Esto puede ocurrir, por ejemplo, en sistemas que dependen de la fecha y hora actual o en conexiones a servicios externos que pueden fallar o comportarse de manera inconsistente. Estos comportamientos son problemáticos en testing, ya que pueden llevar a resultados inesperados. Por eso, se recomienda usar mocks para simular estos comportamientos y garantizar la fiabilidad de las pruebas.
Gran aporte Cristian! 💚
Otra refactorización que podría ser en el componente <u>Contador </u>es reutilizar el Button que se creó en la clase 4 😎
Great! gran aporte Elioenai! 💚
Así cree mi TDD y también aplique Table Driven Testing:
import { it, expect, describe } from 'vitest'; import { render, screen, fireEvent, act } from '@testing-library/react'; import { Timer } from './Timer'; const actionButtons = [{ name: 'Start' }, { name: 'Stop' }, { name: 'Reset' }]; describe('⏰ <Timer /> ⏰', () => { it('should have a title', () => { render(<Timer />); const title = screen.getByRole('heading', { level: 2, name: 'Timer' }); expect(title).toBeInTheDocument(); }); it('should show the initial time as 00:00:00', () => { render(<Timer />); const time = screen.getByText('00:00:00'); expect(time).toBeInTheDocument(); }); it.each(actionButtons)('should have a $name button', ({ name }) => { render(<Timer />); const button = screen.getByRole('button', { name }); expect(button).toBeInTheDocument(); }); it('start and stop the timer when clicking', async () => { render(<Timer />); const startButton = screen.getByRole('button', { name: 'Start' }); const stopButton = screen.getByRole('button', { name: 'Stop' }); await act(() => { fireEvent.click(startButton); }); const time = await screen.findByText('00:00:01', {}, { timeout: 2000 }); expect(time).toBeInTheDocument(); await act(() => { fireEvent.click(stopButton); }); const stoppedTime = await screen.findByText('00:00:01', {}, { timeout: 2000 }); expect(stoppedTime).toBeInTheDocument(); }); it('reset the timer when clicking reset button', async () => { render(<Timer />); const startButton = screen.getByRole('button', { name: 'Start' }); const resetButton = screen.getByRole('button', { name: 'Reset' }); await act(() => { fireEvent.click(startButton); }); const time = await screen.findByText('00:00:01', {}, { timeout: 2000 }); expect(time).toBeInTheDocument(); await act(() => { fireEvent.click(resetButton); }); const resetTime = await screen.findByText('00:00:00', {}, { timeout: 2000 }); expect(resetTime).toBeInTheDocument(); }); });
Mi componente:
import { useState, useRef, useEffect } from 'react'; const Timer = () => { const [time, setTime] = useState('00:00:00'); const timerRef = useRef<NodeJS.Timeout | null>(null); const start = () => { timerRef.current = setInterval(() => { setTime(prevTime => { const [hours, minutes, seconds] = prevTime.split(':').map(Number); let totalSeconds = hours * 3600 + minutes * 60 + seconds + 1; const newHours = String(Math.floor(totalSeconds / 3600)).padStart(2, '0'); totalSeconds %= 3600; const newMinutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0'); const newSeconds = String(totalSeconds % 60).padStart(2, '0'); return `${newHours}:${newMinutes}:${newSeconds}`; }); }, 1000); }; const stop = () => cleanTimer(); const reset = () => { cleanTimer(); timerRef.current = null; setTime('00:00:00'); }; const cleanTimer = () => { if (timerRef.current) clearInterval(timerRef.current); }; useEffect(() => { return () => { cleanTimer(); }; }, []); return ( <div> <h2>Timer</h2> <strong>{time}</strong> <div> <button onClick={start}>Start</button> <button onClick={stop}>Stop</button> <button onClick={reset}>Reset</button> </div> </div> ); }; export { Timer };