Como ingenieros, queremos crear cosas que funcionen , pero con cada nueva característica que creamos, inevitablemente aumentamos el tamaño y la complejidad de nuestras aplicaciones.
A medida que el producto crece, cada vez lleva más tiempo probar manualmente (por ejemplo, con las manos) cada funcionalidad afectada por los cambios.
La ausencia de pruebas automatizadas nos lleva a perder demasiado tiempo y reducir la velocidad de envío o a gastar muy poco para ahorrar velocidad, lo que genera nuevos errores en el trabajo pendiente junto con las llamadas nocturnas de PagerDuty.
Por el contrario, las computadoras pueden programarse para hacer lo mismo repetidamente . Entonces, ¡deleguemos las pruebas a las computadoras!
La idea de la pirámide de pruebas sugiere tres tipos principales de pruebas: unitarias, de integración y de un extremo a otro . Profundicemos en cada tipo y comprendamos por qué los necesitamos.
Una unidad es una pequeña pieza de lógica que se prueba de forma aislada (sin depender de otros componentes).
Las pruebas unitarias son rápidas. Terminan en segundos. El aislamiento les permite ejecutarlos en cualquier momento, localmente y en CI, sin activar los servicios dependientes ni realizar llamadas a API y bases de datos.
Ejemplo de prueba unitaria: una función que acepta dos números y los suma. Queremos llamarlo con diferentes argumentos y afirmar que el valor devuelto es correcto.
// Function "sum" is the unit const sum = (x, y) => x + y test('sums numbers', () => { // Call the function, record the result const result = sum(1, 2); // Assert the result expect(result).toBe(3) }) test('sums numbers', () => { // Call the function, record the result const result = sum(5, 10); // Assert the result expect(result).toBe(15) })
Un ejemplo más interesante es el componente React que muestra algo de texto una vez finalizada la solicitud de API. Necesitamos simular el módulo API para devolver los valores necesarios para nuestras pruebas, representar el componente y afirmar que el HTML renderizado tiene el contenido que necesitamos.
// "MyComponent" is the unit const MyComponent = () => { const { isLoading } = apiModule.useSomeApiCall(); return isLoading ? <div>Loading...</div> : <div>Hello world</div> } test('renders loading spinner when loading', () => { // Mocking the API module, so that it returns the value we need jest.mock(apiModule).mockReturnValue(() => ({ useSomeApiCall: jest.fn(() => ({ // Return "isLoading: false" for this test case isLoading: false })) })) // Execute the unit (render the component) const result = render(<MyComponent />) // Assert the result result.findByText('Loading...').toBeInTheDocument() }) test('renders text content when not loading', () => { // Mocking the API module jest.mock(apiModule).mockReturnValue(() => ({ useSomeApiCall: jest.fn(() => ({ // Return "isLoading: false" for this test case isLoading: false })) })) // Execute the unit (render the component) const result = render(<MyComponent />) // Assert the result result.findByText('Hello world').toBeInTheDocument() })
Cuando su unidad interactúa con otras unidades (dependencias) , lo llamamos integración . Estas pruebas son más lentas que las pruebas unitarias, pero prueban cómo se conectan las partes de su aplicación.
Ejemplo de prueba de integración: un servicio que crea usuarios en una base de datos. Esto requiere que una instancia de base de datos ( dependencia ) esté disponible cuando se ejecutan las pruebas. Probaremos que el servicio pueda crear y recuperar un usuario de la base de datos.
import db from 'db' // We will be testing "createUser" and "getUser" const createUser = name => db.createUser(name) // creates a user const getUser = name => db.getUserOrNull(name) // retrieves a user or null test("creates and retrieves users", () => { // Try to get a user that doesn't exist, assert Null is returned const nonExistingUser = getUser("i don't exist") expect(nonExistingUser).toBe(null); // Create a user const userName = "test-user" createUser(userName); // Get the user that was just created, assert it's not Null const user = getUser(userName); expect(user).to.not.be(null) })
Es una prueba de un extremo a otro cuando probamos la aplicación completamente implementada , donde todas sus dependencias están disponibles. Esas pruebas simulan mejor el comportamiento real del usuario y le permiten detectar todos los posibles problemas en su aplicación, pero son el tipo de prueba más lento .
Siempre que desee ejecutar pruebas de un extremo a otro, debe aprovisionar toda la infraestructura y asegurarse de que haya proveedores externos disponibles en su entorno.
Sólo querrás tenerlos para las funciones críticas de tu aplicación.
Echemos un vistazo a un ejemplo de prueba de un extremo a otro: flujo de inicio de sesión. Queremos ir a la aplicación, completar los datos de inicio de sesión, enviarla y ver el mensaje de bienvenida.
test('user can log in', () => { // Visit the login page page.goto('https://example.com/login'); // Fill in the login form page.fill('#username', 'john'); page.fill('#password', 'some-password'); // Click the login button page.click('#login-button'); // Assert the welcome message is visible page.assertTextVisible('Welcome, John!') })
Recuerde que las pruebas de un extremo a otro son más lentas que las de integración y las pruebas de integración son más lentas que las pruebas unitarias .
Si la característica en la que está trabajando es de misión crítica, considere escribir al menos una prueba de un extremo a otro (como verificar cómo funciona la funcionalidad de inicio de sesión al desarrollar el flujo de autenticación).
Además de los flujos de misión crítica, queremos probar tantos casos extremos y varios estados de la función como sea posible. Las pruebas de integración nos permiten probar cómo funcionan juntas las partes de la aplicación.
Es una buena idea realizar pruebas de integración para puntos finales y componentes del cliente. Los puntos finales deben realizar las operaciones, producir el resultado esperado y no generar errores inesperados.
Los componentes del cliente deben mostrar el contenido correcto y responder a las interacciones del usuario como usted espera que respondan.
Y finalmente, ¿cuándo deberíamos elegir las pruebas unitarias ? Todas las funciones pequeñas que se pueden probar de forma aislada, como sum
que suma los números, Button
que representa la etiqueta <button>
, son excelentes candidatas para las pruebas unitarias. Las unidades son perfectas si sigues el enfoque de desarrollo basado en pruebas .
¡Escribe algunas pruebas! (pero empieza poco a poco)
Haga lo anterior una vez para comprender cómo funciona. Luego, hágalo nuevamente durante el trabajo de alguna función/error. Luego compártelo con tus colegas para que todos puedan escribir exámenes, ahorrar tiempo y dormir mejor por la noche.