Vue.js Uygulamalarında Test Stratejileri ve Best Practices

Yunus Emre Güzel
8 Ocak 202515 dkVue.js
Vue.js Uygulamalarında Test Stratejileri ve Best Practices

Vue.js Uygulamalarında Test Stratejileri ve Best Practices

Modern web uygulamalarında test yazma, kodun güvenilirliğini ve sürdürülebilirliğini sağlamak için kritik bir öneme sahiptir. Vue.js ekosisteminde test yazma, zengin araç seti ve framework desteği sayesinde oldukça etkili bir şekilde yapılabilmektedir.

Test Piramidi ve Vue.js'de Test Türleri

Test piramidi, farklı test türlerinin ideal dağılımını gösteren bir modeldir. Vue.js uygulamalarında üç temel test türü bulunur:

  1. Unit Tests: Bileşenlerin ve fonksiyonların izole edilmiş testleri
  2. Component Tests: Bileşenlerin entegrasyon testleri
  3. End-to-End Tests: Tüm uygulamanın gerçek kullanıcı senaryolarıyla test edilmesi

Unit Testing ile Başlayalım

Unit testler, kodunuzun en küçük parçalarını test etmenizi sağlar. Vue.js'de Vitest kullanarak etkili unit testler yazabilirsiniz.

// composables/useCounter.ts
import { ref } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  const increment = () => {
    count.value++
  }
  
  const decrement = () => {
    count.value--
  }
  
  const reset = () => {
    count.value = initialValue
  }
  
  return {
    count,
    increment,
    decrement,
    reset
  }
}

// tests/composables/useCounter.test.ts
import { describe, it, expect } from 'vitest'
import { useCounter } from '@/composables/useCounter'

describe('useCounter', () => {
  it('should initialize with default value', () => {
    const { count } = useCounter()
    expect(count.value).toBe(0)
  })
  
  it('should initialize with provided value', () => {
    const { count } = useCounter(10)
    expect(count.value).toBe(10)
  })
  
  it('should increment counter', () => {
    const { count, increment } = useCounter(0)
    increment()
    expect(count.value).toBe(1)
  })
  
  it('should decrement counter', () => {
    const { count, decrement } = useCounter(0)
    decrement()
    expect(count.value).toBe(-1)
  })
  
  it('should reset counter', () => {
    const { count, increment, reset } = useCounter(0)
    increment()
    increment()
    reset()
    expect(count.value).toBe(0)
  })
})

Component Testing: Vue Test Utils ile Bileşen Testleri

Vue Test Utils, Vue bileşenlerini test etmek için resmi test yardımcı kütüphanesidir.

// components/TodoItem.vue
<script setup lang="ts">
interface Props {
  todo: {
    id: number
    text: string
    completed: boolean
  }
}

const props = defineProps<Props>()
const emit = defineEmits<{
  (e: 'toggle', id: number): void
  (e: 'delete', id: number): void
}>()
</script>

<template>
  <div class="todo-item" :class="{ completed: todo.completed }">
    <input
      type="checkbox"
      :checked="todo.completed"
      @change="emit('toggle', todo.id)"
    />
    <span>{{ todo.text }}</span>
    <button @click="emit('delete', todo.id)">Sil</button>
  </div>
</template>

// tests/components/TodoItem.test.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import TodoItem from '@/components/TodoItem.vue'

describe('TodoItem.vue', () => {
  const todo = {
    id: 1,
    text: 'Test Todo',
    completed: false
  }
  
  it('renders todo text', () => {
    const wrapper = mount(TodoItem, {
      props: { todo }
    })
    
    expect(wrapper.text()).toContain('Test Todo')
  })
  
  it('emits toggle event when checkbox is clicked', async () => {
    const wrapper = mount(TodoItem, {
      props: { todo }
    })
    
    await wrapper.find('input[type="checkbox"]').trigger('change')
    
    expect(wrapper.emitted('toggle')).toBeTruthy()
    expect(wrapper.emitted('toggle')?.[0]).toEqual([1])
  })
  
  it('emits delete event when delete button is clicked', async () => {
    const wrapper = mount(TodoItem, {
      props: { todo }
    })
    
    await wrapper.find('button').trigger('click')
    
    expect(wrapper.emitted('delete')).toBeTruthy()
    expect(wrapper.emitted('delete')?.[0]).toEqual([1])
  })
  
  it('applies completed class when todo is completed', () => {
    const completedTodo = { ...todo, completed: true }
    const wrapper = mount(TodoItem, {
      props: { todo: completedTodo }
    })
    
    expect(wrapper.classes()).toContain('completed')
  })
})

End-to-End Testing: Cypress ile Gerçek Kullanıcı Senaryoları

E2E testler, uygulamanızı gerçek bir tarayıcıda test etmenizi sağlar.

// cypress/e2e/todo.cy.ts
describe('Todo App', () => {
  beforeEach(() => {
    cy.visit('/')
  })
  
  it('should add new todo', () => {
    const todoText = 'Buy groceries'
    
    cy.get('[data-test="new-todo"]')
      .type(`${todoText}{enter}`)
    
    cy.get('[data-test="todo-list"]')
      .should('contain', todoText)
  })
  
  it('should toggle todo completion', () => {
    // Yeni todo ekle
    cy.get('[data-test="new-todo"]')
      .type('Test todo{enter}')
    
    // Checkbox'ı işaretle
    cy.get('[data-test="todo-item"]')
      .first()
      .find('input[type="checkbox"]')
      .click()
    
    // Completed class'ının eklendiğini kontrol et
    cy.get('[data-test="todo-item"]')
      .first()
      .should('have.class', 'completed')
  })
  
  it('should delete todo', () => {
    // Yeni todo ekle
    const todoText = 'Delete me'
    cy.get('[data-test="new-todo"]')
      .type(`${todoText}{enter}`)
    
    // Silme butonuna tıkla
    cy.get('[data-test="todo-item"]')
      .first()
      .find('button')
      .click()
    
    // Todo'nun silindiğini kontrol et
    cy.get('[data-test="todo-list"]')
      .should('not.contain', todoText)
  })
})

Test Best Practices

1. Test Organizasyonu

  • Dosya Yapısı:

    tests/
    ├── unit/
    │   ├── composables/
    │   └── utils/
    ├── components/
    └── e2e/
    
  • Naming Conventions:

    • Test dosyaları: *.test.ts veya *.spec.ts
    • Test grupları: Anlamlı describe blokları
    • Test case'leri: Açıklayıcı it/test blokları

2. Test Coverage

  • Coverage Hedefleri:

    • Unit Tests: %80+
    • Component Tests: %60+
    • E2E Tests: Kritik kullanıcı yolları
  • Coverage Raporlama:

    {
      "scripts": {
        "test:coverage": "vitest run --coverage"
      },
      "vitest": {
        "coverage": {
          "reporter": ["text", "json", "html"],
          "exclude": ["node_modules/", "dist/"]
        }
      }
    }
    

3. Mocking ve Stubbing

// services/api.ts
import { vi } from 'vitest'

// API servisini mock'la
vi.mock('@/services/api', () => ({
  fetchTodos: vi.fn().mockResolvedValue([
    { id: 1, text: 'Mock Todo', completed: false }
  ])
}))

// Component testi
it('fetches and displays todos', async () => {
  const wrapper = mount(TodoList)
  
  // API çağrısının tamamlanmasını bekle
  await flushPromises()
  
  expect(wrapper.text()).toContain('Mock Todo')
})

4. Test Driven Development (TDD)

TDD yaklaşımı ile test yazma:

  1. Red: Önce başarısız test yaz
  2. Green: Minimum kod ile testi geçir
  3. Refactor: Kodu iyileştir, testlerin geçtiğinden emin ol
// Red: Başarısız test
describe('useAuth', () => {
  it('should authenticate user with valid credentials', async () => {
    const { login, user, error } = useAuth()
    
    await login('test@example.com', 'password123')
    
    expect(user.value).toBeTruthy()
    expect(error.value).toBeNull()
  })
})

// Green: Minimum implementasyon
export function useAuth() {
  const user = ref(null)
  const error = ref(null)
  
  const login = async (email: string, password: string) => {
    user.value = { email }
  }
  
  return { login, user, error }
}

// Refactor: İyileştirme
export function useAuth() {
  const user = ref<User | null>(null)
  const error = ref<Error | null>(null)
  
  const login = async (email: string, password: string) => {
    try {
      const response = await api.login(email, password)
      user.value = response.data
      error.value = null
    } catch (e) {
      user.value = null
      error.value = e instanceof Error ? e : new Error('Login failed')
    }
  }
  
  return { login, user, error }
}

Performans ve Test Optimizasyonu

1. Test Hızını Artırma

  • Paralel Test Çalıştırma:

    {
      "vitest": {
        "threads": true,
        "maxThreads": 4,
        "minThreads": 1
      }
    }
    
  • Test İzolasyonu:

    beforeEach(() => {
      // Her test öncesi state'i sıfırla
      vi.clearAllMocks()
      localStorage.clear()
    })
    

2. CI/CD Entegrasyonu

# .github/workflows/test.yml
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v2
      
      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '18'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run unit tests
        run: npm run test:unit
        
      - name: Run e2e tests
        run: npm run test:e2e
        
      - name: Upload coverage
        uses: codecov/codecov-action@v2

Sonuç

Vue.js uygulamalarında test yazma, uygulamanızın kalitesini ve güvenilirliğini artıran önemli bir pratiktir. Bu rehberde öğrendiğimiz:

  • Test Türleri ve Araçlar:

    • Unit Tests (Vitest)
    • Component Tests (Vue Test Utils)
    • E2E Tests (Cypress)
  • Best Practices:

    • Test organizasyonu
    • Coverage hedefleri
    • Mocking stratejileri
    • TDD yaklaşımı
  • Optimizasyon:

    • Performans iyileştirmeleri
    • CI/CD entegrasyonu

sayesinde daha güvenilir ve sürdürülebilir Vue.js uygulamaları geliştirebilirsiniz.

Kaynaklar