Vue.js Testing Jest Quality

Testing en Vue.js con Jest y Testing Library

Pablo Alcalde García

Testing en Vue.js con Jest y Testing Library

Cómo escribo tests que realmente aportan valor.

Setup inicial

npm install -D @vue/test-utils jest @testing-library/vue
npm install -D @testing-library/jest-dom
// jest.config.js
export default {
  testEnvironment: 'jsdom',
  transform: {
    '^.+\\.vue$': '@vue/vue3-jest',
    '^.+\\.js$': 'babel-jest',
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
}

Anatomía de un test

import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from './Button.vue'

describe('Button.vue', () => {
  it('renderiza el texto correctamente', () => {
    const wrapper = mount(Button, {
      props: { label: 'Click me' }
    })
    
    expect(wrapper.text()).toContain('Click me')
  })
})

Testing de componentes

Props

it('acepta variant prop', () => {
  const wrapper = mount(Button, {
    props: { variant: 'primary' }
  })
  
  expect(wrapper.classes()).toContain('btn-primary')
})

Events

it('emite click event', async () => {
  const wrapper = mount(Button)
  
  await wrapper.trigger('click')
  
  expect(wrapper.emitted('click')).toBeTruthy()
  expect(wrapper.emitted('click')).toHaveLength(1)
})

Slots

it('renderiza slot content', () => {
  const wrapper = mount(Button, {
    slots: {
      default: 'Custom content'
    }
  })
  
  expect(wrapper.text()).toBe('Custom content')
})

Computed properties

it('calcula fullName correctamente', () => {
  const wrapper = mount(UserCard, {
    props: {
      firstName: 'Pablo',
      lastName: 'Alcalde'
    }
  })
  
  expect(wrapper.vm.fullName).toBe('Pablo Alcalde')
})

Testing con Composition API

import { ref } from 'vue'

export function useCounter(initial = 0) {
  const count = ref(initial)
  const increment = () => count.value++
  const decrement = () => count.value--
  
  return { count, increment, decrement }
}
// useCounter.spec.js
import { useCounter } from './useCounter'

describe('useCounter', () => {
  it('incrementa el contador', () => {
    const { count, increment } = useCounter(0)
    
    expect(count.value).toBe(0)
    increment()
    expect(count.value).toBe(1)
  })
})

Testing async

it('carga datos de la API', async () => {
  const wrapper = mount(UserList)
  
  // Esperar actualizaciones
  await wrapper.vm.$nextTick()
  
  // O esperar condición
  await wrapper.find('[data-testid="user-list"]').exists()
  
  expect(wrapper.findAll('.user')).toHaveLength(3)
})

Mocking

API calls

import { vi } from 'vitest'

vi.mock('@/api/users', () => ({
  fetchUsers: vi.fn(() => Promise.resolve([
    { id: 1, name: 'User 1' }
  ]))
}))

Router

const wrapper = mount(Component, {
  global: {
    mocks: {
      $route: { params: { id: '1' } },
      $router: { push: vi.fn() }
    }
  }
})

Store (Pinia)

import { setActivePinia, createPinia } from 'pinia'

beforeEach(() => {
  setActivePinia(createPinia())
})

it('usa el store', () => {
  const store = useUserStore()
  store.setUser({ name: 'Pablo' })
  
  const wrapper = mount(UserProfile)
  expect(wrapper.text()).toContain('Pablo')
})

Testing Library approach

import { render, screen, fireEvent } from '@testing-library/vue'

it('toggles visibility', async () => {
  render(ToggleButton)
  
  const button = screen.getByRole('button')
  const content = screen.queryByText('Hidden content')
  
  expect(content).not.toBeInTheDocument()
  
  await fireEvent.click(button)
  
  expect(screen.getByText('Hidden content')).toBeInTheDocument()
})

Best practices

1. Testing user behavior

// ✅ Bueno: Testea comportamiento
it('muestra error cuando email es inválido', async () => {
  const { getByLabelText, getByText } = render(LoginForm)
  
  await fireEvent.update(getByLabelText('Email'), 'invalid')
  await fireEvent.click(getByText('Submit'))
  
  expect(getByText('Email inválido')).toBeInTheDocument()
})

// ❌ Malo: Testea implementación
it('establece emailError a true', () => {
  const wrapper = mount(LoginForm)
  wrapper.vm.validateEmail('invalid')
  expect(wrapper.vm.emailError).toBe(true)
})

2. Data attributes para testing

<button data-testid="submit-button">Submit</button>
const button = screen.getByTestId('submit-button')

3. Coverage significativo

npm run test:coverage

Apunta a:

  • Statements: 80%+
  • Branches: 75%+
  • Functions: 80%+
  • Lines: 80%+

Mi test suite típico

describe('LoginForm', () => {
  describe('Rendering', () => {
    it('muestra todos los campos')
  })
  
  describe('Validation', () => {
    it('valida email format')
    it('requiere password')
  })
  
  describe('Submission', () => {
    it('llama a la API con datos correctos')
    it('maneja errores de red')
    it('redirige después del login')
  })
})

Conclusión

Los tests bien escritos son documentación viva y red de seguridad. ¡Invierte en ellos!

¿Te ha gustado este artículo?

Si tienes preguntas o quieres discutir sobre estos temas, no dudes en contactarme.

Contáctame
Escríbeme por WhatsApp