Kazdy krytyczny system stacji NOVA LAB musi przejsc rygorystyczne testy przed wdrozeniem. Tak samo composables - musimy miec pewnosc, ze dzialaja poprawnie w kazdym scenariuszu.
Do testowania composables uzywamy
@vue/test-utils i Vitest (lub Jest). Kluczowa jest funkcja pomocnicza, ktora uruchamia composable wewnatrz komponentu testowego.1// test-utils.ts
2import { createApp, defineComponent } from 'vue'
3
4// Helper: uruchom composable w izolowanym komponencie
5function withSetup(composable) {
6 let result
7
8 const TestComponent = defineComponent({
9 setup() {
10 result = composable()
11 return () => {} // pusty render
12 }
13 })
14
15 const app = createApp(TestComponent)
16 app.mount(document.createElement('div'))
17
18 return { result, app }
19}1import { describe, it, expect } from 'vitest'
2import { ref } from 'vue'
3
4// Composable do testowania
5function useCounter(initial = 0) {
6 const count = ref(initial)
7 const increment = () => count.value++
8 const decrement = () => count.value--
9 const reset = () => { count.value = initial }
10 return { count, increment, decrement, reset }
11}
12
13describe('useCounter', () => {
14 it('startuje z wartoscia poczatkowa', () => {
15 const { result } = withSetup(() => useCounter(10))
16 expect(result.count.value).toBe(10)
17 })
18
19 it('domyslnie startuje od 0', () => {
20 const { result } = withSetup(() => useCounter())
21 expect(result.count.value).toBe(0)
22 })
23
24 it('inkrementuje wartosc', () => {
25 const { result } = withSetup(() => useCounter())
26 result.increment()
27 expect(result.count.value).toBe(1)
28 result.increment()
29 expect(result.count.value).toBe(2)
30 })
31
32 it('dekrementuje wartosc', () => {
33 const { result } = withSetup(() => useCounter(5))
34 result.decrement()
35 expect(result.count.value).toBe(4)
36 })
37
38 it('resetuje do wartosci poczatkowej', () => {
39 const { result } = withSetup(() => useCounter(10))
40 result.increment()
41 result.increment()
42 result.reset()
43 expect(result.count.value).toBe(10)
44 })
45})Gdy composable zalezy od zewnetrznych zrodel danych, mockujemy je.
1import { describe, it, expect, vi } from 'vitest'
2import { ref, nextTick } from 'vue'
3
4// Composable z zaleznoscia od fetch
5function useSensorData(fetchFn) {
6 const data = ref(null)
7 const error = ref(null)
8 const loading = ref(false)
9
10 async function load(sensorId) {
11 loading.value = true
12 error.value = null
13 try {
14 data.value = await fetchFn(sensorId)
15 } catch (e) {
16 error.value = e.message
17 } finally {
18 loading.value = false
19 }
20 }
21
22 return { data, error, loading, load }
23}
24
25describe('useSensorData', () => {
26 it('laduje dane z serwera', async () => {
27 // Mock funkcji fetch
28 const mockFetch = vi.fn().mockResolvedValue({
29 id: 'T-01',
30 temperature: 22,
31 status: 'active'
32 })
33
34 const { result } = withSetup(() => useSensorData(mockFetch))
35
36 expect(result.loading.value).toBe(false)
37
38 await result.load('T-01')
39
40 expect(mockFetch).toHaveBeenCalledWith('T-01')
41 expect(result.data.value).toEqual({
42 id: 'T-01',
43 temperature: 22,
44 status: 'active'
45 })
46 expect(result.loading.value).toBe(false)
47 })
48
49 it('obsluguje bledy', async () => {
50 const mockFetch = vi.fn().mockRejectedValue(
51 new Error('Serwer niedostepny')
52 )
53
54 const { result } = withSetup(() => useSensorData(mockFetch))
55
56 await result.load('T-01')
57
58 expect(result.error.value).toBe('Serwer niedostepny')
59 expect(result.data.value).toBeNull()
60 })
61})Watchers sa asynchroniczne - potrzebujemy
nextTick lub flushPromises.1import { describe, it, expect } from 'vitest'
2import { ref, watch, nextTick, computed } from 'vue'
3
4function useSearchFilter(items) {
5 const query = ref('')
6 const filtered = computed(() => {
7 if (!query.value) return items.value
8 return items.value.filter(item =>
9 item.name.toLowerCase().includes(query.value.toLowerCase())
10 )
11 })
12
13 return { query, filtered }
14}
15
16describe('useSearchFilter', () => {
17 it('filtruje elementy po zapytaniu', async () => {
18 const items = ref([
19 { name: 'Sensor temperatury' },
20 { name: 'Sensor cisnienia' },
21 { name: 'Sensor radiacji' }
22 ])
23
24 const { result } = withSetup(() => useSearchFilter(items))
25
26 // Poczatkowo wszystkie elementy
27 expect(result.filtered.value).toHaveLength(3)
28
29 // Filtrowanie
30 result.query.value = 'temp'
31 await nextTick()
32
33 expect(result.filtered.value).toHaveLength(1)
34 expect(result.filtered.value[0].name).toBe('Sensor temperatury')
35 })
36
37 it('zwraca wszystko dla pustego zapytania', async () => {
38 const items = ref([{ name: 'A' }, { name: 'B' }])
39 const { result } = withSetup(() => useSearchFilter(items))
40
41 result.query.value = 'A'
42 await nextTick()
43 expect(result.filtered.value).toHaveLength(1)
44
45 result.query.value = ''
46 await nextTick()
47 expect(result.filtered.value).toHaveLength(2)
48 })
49})1import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2import { ref, onMounted, onUnmounted } from 'vue'
3
4function usePolling(fetchFn, interval = 5000) {
5 const data = ref(null)
6 const isPolling = ref(false)
7 let timerId = null
8
9 function startPolling() {
10 isPolling.value = true
11 timerId = setInterval(async () => {
12 data.value = await fetchFn()
13 }, interval)
14 }
15
16 function stopPolling() {
17 isPolling.value = false
18 if (timerId) {
19 clearInterval(timerId)
20 timerId = null
21 }
22 }
23
24 onUnmounted(stopPolling)
25
26 return { data, isPolling, startPolling, stopPolling }
27}
28
29describe('usePolling', () => {
30 beforeEach(() => {
31 vi.useFakeTimers()
32 })
33
34 afterEach(() => {
35 vi.restoreAllTimers()
36 })
37
38 it('odpytuje serwer w zadanych odstepach', async () => {
39 let callCount = 0
40 const mockFetch = vi.fn().mockImplementation(() => {
41 callCount++
42 return Promise.resolve({ reading: callCount * 10 })
43 })
44
45 const { result } = withSetup(() => usePolling(mockFetch, 1000))
46
47 result.startPolling()
48 expect(result.isPolling.value).toBe(true)
49
50 // Symulacja uplywu czasu
51 await vi.advanceTimersByTime(3000)
52
53 expect(mockFetch).toHaveBeenCalledTimes(3)
54
55 result.stopPolling()
56 expect(result.isPolling.value).toBe(false)
57 })
58})nextTick i flushPromises dla reactivity