Calidad de código en React Native con testing y buenas prácticas

Capítulo 11

Tiempo estimado de lectura: 11 minutos

+ Ejercicio

Qué significa “calidad de código” en una app React Native

En un proyecto profesional, la calidad de código se refleja en tres resultados: (1) cambios seguros (puedes refactorizar sin miedo), (2) comportamiento predecible (errores controlados y consistentes), y (3) escalabilidad del equipo (convenciones claras y estructura fácil de navegar). Para lograrlo, combinaremos estándares de arquitectura (estructura por features, separación de responsabilidades, tipado y convenciones) con una estrategia de testing (unitario, componentes y flujos críticos) que actúe como red de seguridad.

Estándares del proyecto: estructura por features

Objetivo

Evitar carpetas genéricas gigantes (components, screens, utils) que crecen sin control. En su lugar, agrupar por “feature” (dominio funcional) para que cada parte sea localizable, testeable y mantenible.

Estructura recomendada

src/  app/  features/    auth/      api/      components/      hooks/      screens/      state/      types.ts      __tests__/    profile/      ...  shared/    components/    hooks/    lib/    types/  services/    http/    storage/  config/  
  • features/<feature>: todo lo específico del dominio (UI, lógica, tipos, tests).
  • shared/: piezas reutilizables y agnósticas del dominio (botones, inputs, helpers puros).
  • services/: infraestructura transversal (cliente HTTP, logging, almacenamiento, analytics), sin depender de UI.
  • config/: configuración (endpoints, flags, entornos).

Reglas de dependencia (para evitar acoplamiento)

  • shared no importa desde features.
  • Una feature no debería importar directamente otra feature; si es necesario, extrae a shared o crea una capa de “orquestación” en app/.
  • La UI no debería conocer detalles de HTTP; usa servicios o repositorios.

Separación de responsabilidades: UI, lógica y datos

Patrón práctico: “Container + Presentational” (sin dogmas)

Separa componentes que obtienen datos/ejecutan acciones (container) de componentes que solo renderizan (presentational). Esto reduce complejidad y facilita pruebas.

// features/profile/components/ProfileView.tsx (presentational) import React from 'react'; import { View, Text, Button } from 'react-native'; type Props = { name: string; email: string; onRefresh: () => void; loading?: boolean; error?: string | null; }; export function ProfileView({ name, email, onRefresh, loading, error }: Props) { return ( <View> <Text testID="name">{name}</Text> <Text testID="email">{email}</Text> {error ? <Text testID="error">{error}</Text> : null} <Button title={loading ? 'Cargando...' : 'Actualizar'} onPress={onRefresh} disabled={loading} /> </View> ); }
// features/profile/screens/ProfileScreen.tsx (container) import React from 'react'; import { ProfileView } from '../components/ProfileView'; import { useProfile } from '../hooks/useProfile'; export function ProfileScreen() { const { profile, refresh, loading, error } = useProfile(); return ( <ProfileView name={profile?.name ?? ''} email={profile?.email ?? ''} onRefresh={refresh} loading={loading} error={error} /> ); }

Servicios y “repositorios” para datos

Centraliza el acceso a datos en módulos que puedan mockearse fácilmente en tests. Evita llamar fetch/axios directamente desde componentes.

// features/profile/api/profileRepository.ts export type Profile = { id: string; name: string; email: string; }; export interface ProfileRepository { getProfile(): Promise<Profile>; } 
// features/profile/api/profileRepository.http.ts import { httpClient } from '../../../services/http/httpClient'; import type { Profile, ProfileRepository } from './profileRepository'; export const profileRepositoryHttp: ProfileRepository = { async getProfile() { return httpClient.get<Profile>('/me'); }, };

Tipado (TypeScript) para reducir bugs y mejorar refactors

Qué tipar primero (impacto alto)

  • Modelos de dominio: Profile, Order, etc.
  • Contratos de servicios/repositorios: interfaces con métodos.
  • Props de componentes presentacionales.
  • Respuestas de API (idealmente mapeadas a dominio).

Evita “any” con estrategias simples

  • Usa genéricos en el cliente HTTP: httpClient.get<T>().
  • Define tipos para estados de carga y error.
  • Si un dato viene “sucio” de API, crea un mapper que valide/normalice.
// shared/types/result.ts export type AppError = { code: string; message: string; cause?: unknown; }; export type Result<T> = { ok: true; data: T } | { ok: false; error: AppError };

Convenciones de nombres y organización de archivos

ElementoConvenciónEjemplo
ComponentesPascalCaseProfileView.tsx
HookscamelCase con prefijo useuseProfile.ts
Servicios/ReposcamelCase + sufijoprofileRepositoryHttp.ts
Tests.test.ts/.test.tsxProfileView.test.tsx
Mocksprefijo mock o carpeta __mocks__mockProfileRepository.ts

Regla práctica: el nombre del archivo debe anticipar su responsabilidad. Si un archivo empieza a hacer “de todo”, es señal de refactor.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

Manejo consistente de errores

Problema típico

Errores tratados de forma distinta en cada pantalla: a veces alert, a veces texto, a veces se ignoran. Esto genera UX inconsistente y tests frágiles.

Estándar: un tipo de error de app + normalización

Define un formato único (AppError) y una función que convierta errores desconocidos (HTTP, parsing, timeouts) a ese formato.

// shared/lib/errors/normalizeError.ts import type { AppError } from '../../types/result'; export function normalizeError(e: unknown): AppError { if (e && typeof e === 'object' && 'message' in e) { return { code: 'UNKNOWN', message: String((e as any).message), cause: e }; } return { code: 'UNKNOWN', message: 'Ocurrió un error inesperado', cause: e }; }

Patrón para hooks: estado de carga + error tipado

// features/profile/hooks/useProfile.ts import { useCallback, useEffect, useState } from 'react'; import type { ProfileRepository, Profile } from '../api/profileRepository'; import { profileRepositoryHttp } from '../api/profileRepository.http'; import { normalizeError } from '../../../shared/lib/errors/normalizeError'; export function useProfile(repo: ProfileRepository = profileRepositoryHttp) { const [profile, setProfile] = useState<Profile | null>(null); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); const refresh = useCallback(async () => { setLoading(true); setError(null); try { const data = await repo.getProfile(); setProfile(data); } catch (e) { const appError = normalizeError(e); setError(appError.message); } finally { setLoading(false); } }, [repo]); useEffect(() => { refresh(); }, [refresh]); return { profile, refresh, loading, error }; }

Ventaja: la UI recibe siempre loading y error con el mismo significado, lo que simplifica componentes y pruebas.

Estrategia de testing: pirámide práctica

Qué probar y por qué

  • Unitarias (muchas): funciones puras, normalización de errores, mappers, reglas de negocio.
  • Componentes (algunas): render, estados (loading/error), callbacks, accesibilidad básica.
  • Flujos críticos (pocas, pero clave): login/checkout/guardado, navegación esencial, escenarios de error.

Herramientas habituales

  • jest para unit tests y mocks.
  • @testing-library/react-native para pruebas de componentes orientadas a comportamiento.
  • Para flujos end-to-end, una opción común es Detox (si tu proyecto lo adopta).

Configuración mínima de Jest (referencia)

// jest.config.js module.exports = { preset: 'react-native', testMatch: ['**/?(*.)+(test).[tj]s?(x)'], setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'], clearMocks: true, };

Recomendación: mantén la configuración simple y agrega ajustes solo cuando un caso real lo requiera.

Pruebas unitarias: ejemplo con normalización de errores

// shared/lib/errors/normalizeError.test.ts import { normalizeError } from './normalizeError'; test('devuelve mensaje genérico si no hay message', () => { const err = normalizeError(123); expect(err.code).toBe('UNKNOWN'); expect(err.message).toBe('Ocurrió un error inesperado'); }); test('usa message si existe', () => { const err = normalizeError(new Error('Fallo de red')); expect(err.message).toBe('Fallo de red'); });

Pruebas de componentes: estados y callbacks

Prueba el comportamiento observable: qué se muestra y qué pasa cuando el usuario interactúa. Evita testear implementación interna.

// features/profile/components/ProfileView.test.tsx import React from 'react'; import { render, fireEvent } from '@testing-library/react-native'; import { ProfileView } from './ProfileView'; test('muestra nombre y email', () => { const { getByTestId } = render( <ProfileView name="Ada" email="ada@mail.com" onRefresh={() => {}} /> ); expect(getByTestId('name').props.children).toBe('Ada'); expect(getByTestId('email').props.children).toBe('ada@mail.com'); }); test('dispara onRefresh al presionar', () => { const onRefresh = jest.fn(); const { getByText } = render( <ProfileView name="Ada" email="ada@mail.com" onRefresh={onRefresh} /> ); fireEvent.press(getByText('Actualizar')); expect(onRefresh).toHaveBeenCalledTimes(1); }); test('muestra error si existe', () => { const { getByTestId } = render( <ProfileView name="Ada" email="ada@mail.com" error="No se pudo cargar" onRefresh={() => {}} /> ); expect(getByTestId('error').props.children).toBe('No se pudo cargar'); });

Mocks de servicios: repositorios falsos para tests deterministas

Mock manual (recomendado para claridad)

// features/profile/__tests__/mockProfileRepository.ts import type { ProfileRepository } from '../api/profileRepository'; export function mockProfileRepository(overrides?: Partial<ProfileRepository>): ProfileRepository { return { getProfile: async () => ({ id: '1', name: 'Ada', email: 'ada@mail.com' }), ...overrides, }; }

Test del hook con repositorio mockeado

En lugar de depender de red, inyecta el repositorio. Esto hace el test rápido y confiable.

// features/profile/hooks/useProfile.test.ts import { renderHook, act } from '@testing-library/react-native'; import { useProfile } from './useProfile'; import { mockProfileRepository } from '../__tests__/mockProfileRepository'; test('carga perfil al iniciar', async () => { const repo = mockProfileRepository(); const { result } = renderHook(() => useProfile(repo)); // espera a que el efecto termine await act(async () => {}); expect(result.current.profile?.name).toBe('Ada'); expect(result.current.error).toBe(null); }); test('maneja error del repositorio', async () => { const repo = mockProfileRepository({ getProfile: async () => { throw new Error('Boom'); } }); const { result } = renderHook(() => useProfile(repo)); await act(async () => {}); expect(result.current.profile).toBe(null); expect(result.current.error).toBe('Boom'); });

Pruebas de flujos críticos: enfoque por “casos de negocio”

Selecciona 3–5 flujos que no pueden fallar

  • Inicio de sesión (éxito y credenciales inválidas).
  • Compra/checkout (éxito, pago rechazado, reintento).
  • Guardar cambios de perfil (validación, error de red, reintento).
  • Recuperación ante error (pantalla muestra estado y permite reintentar).

Ejemplo: flujo crítico a nivel de integración (sin E2E completo)

Idea: renderizar una pantalla contenedora con dependencias mockeadas y simular interacción. Esto cubre wiring entre UI + hook + repositorio sin depender de backend real.

// features/profile/screens/ProfileScreen.integration.test.tsx import React from 'react'; import { render, fireEvent, waitFor } from '@testing-library/react-native'; import { ProfileView } from '../components/ProfileView'; import { useProfile } from '../hooks/useProfile'; jest.mock('../hooks/useProfile'); const useProfileMock = useProfile as jest.Mock; test('al presionar Actualizar llama refresh', async () => { const refresh = jest.fn(); useProfileMock.mockReturnValue({ profile: { id: '1', name: 'Ada', email: 'ada@mail.com' }, refresh, loading: false, error: null, }); const { getByText } = render( <ProfileView name="Ada" email="ada@mail.com" onRefresh={refresh} /> ); fireEvent.press(getByText('Actualizar')); await waitFor(() => expect(refresh).toHaveBeenCalled()); });

Nota: aquí se muestra el patrón de integración con mocks. En tu proyecto real, lo ideal es renderizar ProfileScreen y mockear el repositorio inyectado (o el módulo del repositorio) para cubrir el flujo completo sin red.

Refactor guiado: mejorar mantenibilidad sin cambiar comportamiento

Escenario inicial (código difícil de testear)

Un componente que hace todo: llama API, maneja loading/error, transforma datos y renderiza. Esto complica el testing porque necesitas montar UI para validar lógica y además mockear red dentro del componente.

// ANTES: ProfileScreen.tsx (anti-patrón) import React, { useEffect, useState } from 'react'; import { View, Text, Button } from 'react-native'; import { httpClient } from '../../services/http/httpClient'; export function ProfileScreen() { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); async function load() { setLoading(true); setError(null); try { const data = await httpClient.get<any>('/me'); setName(data.name); setEmail(data.email); } catch (e: any) { setError(e?.message ?? 'Error'); } finally { setLoading(false); } } useEffect(() => { load(); }, []); return ( <View> <Text>{name}</Text> <Text>{email}</Text> {error ? <Text>{error}</Text> : null} <Button title={loading ? 'Cargando...' : 'Actualizar'} onPress={load} /> </View> ); }

Paso 1: extraer contrato de datos (repositorio)

Sin cambiar UI, crea ProfileRepository y una implementación HTTP. Beneficio: puedes mockearlo en tests.

// DESPUÉS (paso 1): features/profile/api/profileRepository.ts export type Profile = { id: string; name: string; email: string; }; export interface ProfileRepository { getProfile(): Promise<Profile>; }

Paso 2: mover lógica a un hook

El componente deja de saber de HTTP y se vuelve un “orquestador” simple. Beneficio: testear lógica sin renderizar UI compleja.

// DESPUÉS (paso 2): features/profile/hooks/useProfile.ts // (igual al mostrado antes, con inyección de repo)

Paso 3: separar UI en componente presentacional

Beneficio: pruebas de UI rápidas y enfocadas (render/estados/callbacks) sin dependencias externas.

// DESPUÉS (paso 3): ProfileView + ProfileScreen (igual al mostrado antes)

Paso 4: mejorar tipado y eliminar “any”

  • Reemplaza any por Profile.
  • Normaliza errores con normalizeError.
  • Agrega testID en elementos clave para tests estables.

Paso 5: añadir tests como red de seguridad

  • Unit test para normalizeError.
  • Component test para ProfileView (render, error, botón).
  • Hook test para useProfile con repositorio mock.

Checklist de calidad para PRs (pull requests)

  • ¿La feature está encapsulada en features/<feature> y no dispersa en carpetas genéricas?
  • ¿La UI no conoce detalles de infraestructura (HTTP/storage)?
  • ¿Los errores se normalizan y se muestran de forma consistente?
  • ¿No hay any innecesarios? ¿Los modelos y contratos están tipados?
  • ¿Hay tests para lógica crítica y estados (éxito/error/loading)?
  • ¿Los nombres de archivos y funciones reflejan responsabilidad única?
  • ¿El refactor mantiene comportamiento (tests pasan) y reduce complejidad?

Ahora responde el ejercicio sobre el contenido:

¿Cuál es el principal beneficio de inyectar un repositorio (ProfileRepository) en un hook como useProfile en lugar de llamar HTTP directamente desde el componente?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Al definir un contrato (repositorio) e inyectarlo en el hook, la lógica deja de depender de la red y se puede reemplazar por un mock en tests. Esto mejora la confiabilidad y velocidad de las pruebas y reduce el acoplamiento con la infraestructura.

Siguiente capítulo

Despliegue y mantenimiento de una app React Native profesional

Arrow Right Icon
Portada de libro electrónico gratuitaReact Native desde Cero a App Profesional
92%

React Native desde Cero a App Profesional

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.