Vue Test Utils Kılavuzu

Bölüm 1

Atakan Tekoğlu
Modanisa Engineering
9 min readFeb 16, 2022

--

Bu yazımda Vue componentlerinin Vue Test Utils ile nasıl ve ne şekilde test edilebileceğine dair kısa ve ilgili maddeye odaklı örneklerine ulaşabilirsiniz.

Kodlarımızı test etmek için Vue Test Utils ve Jest kullanıyoruz. VTU aslında alt tarafta Jest’i kullanmaktadır. Yani VTU öğrenirken aynı zamanda da Jest öğreniyorsunuz. Basit örneklerle bu iki aracın ne şekilde kullanılabileceğine dair öğrendiklerimi paylaşacağım. Son olarak, her bir örnek birbirinden bağımsız olarak ele alınacaktır.

Vue Test Utils ve Jest

Bu yazımda hangi konulara değindim?

  • Mounting Component
    -mount
    -shallowMount
  • Testing Props
  • Factory Function
  • Testing Computed Properties
  • Testing Emitted Events

Vue Test Utils render edilen DOM üzerinde kolay bir şekilde dolaşmamız için bize gerekli tüm araçları sunar. Bir component’e ait üyelerin (methods, computeds, vs.) test edilebilmesi için soyutlanmış olarak bizim elimizde bulunması gerekir. Bu soyutlama işlemini ise VTU iki şekilde gerçekleştirir; bunlar mount ve shallowMount olarak adlandırılır. İlk olarak bu iki unsurun ne olduğunu öğrenerek başlayalım.

Mounting Vue Instance (mount vs shallowMount)

Test edilecek olan hedef component’in soyut bir versiyonuna ulaşmak için mount ve shallowMount kullanılır. Component mount edildiğinde bize bir wrapperobject döner. Bu object içinde component’i test etmek için gerekli olan yardımcı fonksiyonlar bulunur.

mount metot deklarasyonu

mount ve shallowMount ikilisinin yaptığı görev temel olarak aynı olmakla birlikte bir fark ile ayrılmaktadırlar. Şimdi hem bir component nasıl mount edilir hem de bu farkın ne şekilde tanımlandığını öğreneceğimiz ilk örneğimizi yapalım.

Parent-child ilişkisine sahip iki componentimiz olsun: Book ve Page componentleri.

<template>
<div>
<Page/>
</div>
</template>
<script>
import Page from "./Page";
export default {
name: 'BookComponent',
components: {
Page,
},
};
</script>
<template>
<h1>Page Component</h1>
</template>

<script>
export default {
name: 'PageComponent',
};
</script>

Şimdi ise test tarafında renderingComponent.spec.js isimli test dosyamızda işlemlerimize devam edelim.

Aşağıda ilk olarak mount ve shallowMount, sonrasında hedef componentler Book ve Page vue dosyaları import edilir. Ayrıca mount fonksiyonu bize bir wrapper object döndürür ve bunun içinde yardımcı fonksiyonlarımız bulunur demiştim. Bu test için html() yardımcı fonksiyonunu kullanıyoruz.

import { mount,shallowMount } from "@vue/test-utils"
import Page from '../../components/Page'
import Book from "../../components/Book";

describe("Rendering Components", () => {
test("render child component with shallow mount",() => {
const shallowWrapper = shallowMount(Page)
const mountWrapper = mount(Page)
console.log(shallowWrapper.html())
console.log(mountWrapper.html())
})
})

İlk testimizde child component olan Page.vue mount ve shallowMount ile soyutlanmış biçimde elimize geçer. Dikkat edilirse component iki şekilde mount edildikten sonra dönen wrapper object değişkende tutuluyor. Sonra her iki yöntem için html() yardımcı fonksiyon çağırılıyor ve log işlemi gerçekleşiyor. Peki log bize ne yazdırdı bir göz atalım.

Loglama işlemini incelediğimizde bir sürprizle karşılaşmıyoruz. Çünkü Page.vue içerisinde sadece ama sadece render edilen tek bir <h1></h1> html etiketi bulunuyor.

Şimdi parent component olan Book.vue dosyasını iki şekilde mount edip loglama işlemini gerçekleştirelim. Yukarda yazdığımız test altına aşağıdaki testi ekleyelim.

test("render parent component with mount",() => {
const shallowWrapper = shallowMount(Book)
const mountWrapper = mount(Book)
console.log(shallowWrapper.html())
console.log(mountWrapper.html())
})

Bu testimizin çıktısını inceleyelim.

Burada shallowMount ile mount arasındaki farkı anlamaya başlıyoruz. İkinci log satırında mount ile soyutlanan parent component kendisi içinde render edilen Page.vue component’i içinde bulunan <h1> etiketi altındaki Page Component yazısını yazdırdır.

Öte yandan ilk satırdaki log işlemine baktığımızda shallowMount ile soyutlanan parent component Page.vue component’i yerine

<page-stub></page-stub>

gibi bir ifade bastırdı. shallowMount soyutladığı component olan Book.vue içerisinde render edilen child component olan Page.vue ile ilgilenmedi. Onu bir stub olarak gördü.

Stub: gerçeği anlamına gelen sahte bir object

Toplayacak olursak mount child componentleri de render ederken shallowMount sadece ilgili component elemanlarını render eder ve child olanları bir stub olarak değerlendirir.

Takip eden bağlantıya tıklayarak ilgili commit’i inceleyebilirsiniz.[mount vs shallowMount]

Testing Props

Parent component ile child component arasındaki veri akışı props denilen özel veri taşıyıcılarla yapılır. Bu araçların testini yaparken mount ve shallowMount fonksiyonlarına verilen ikinci bir parametre bize yardımcı olur.

Aşağıdaki yapıyı incelersek bir component mount edilirken ikinci parametre olarak bir object alır. Bu object içinde propsData denilen özel yapı ile hedef component prop’larına veri aktarımı yapılır ve test işlemleri buna göre gerçeklenir.

const wrapper = mount(Foo, {
propsData: {
foo: 'bar'
}
})

Aşağıda yine parent-child ilişkisine sahip olan iki component bulunmaktadır: LoginContainer.vue ve LoginForm.vue

Aşağıdaki kod örneğinde LoginContainer, LoginForm componentini kapsar ve prop olarak isAdmin isimli Boolean bir değer gönderir. Eğer isAdmin ‘true’ ise admin’e özel bir giriş formu değilse kullanıcıya özel bir giriş formunun gösterildiği bir senaryo hazırlanmıştır.

<template>
<div>
<LoginForm :isAdmin="true"/>
</div>
</template>

<script>
import LoginForm from "./LoginForm";
export default {
name: 'LoginContainer',
components: {
LoginForm,
},
};
</script>

Yukarıda LoginForm componentinin LoginContainer içinde render edildiğini ve isAdmin isimli prop binding işlemini görebiliyoruz.

<template>
<div>
<div v-if="isAdmin" data-test="admin-form">Admin Form Design</div>
<div v-else data-test="user-form">User Form Design</div>
</div>
</template>

<script>
export default {
name: 'LoginForm',
props: {
isAdmin: {
type: Boolean,
default: false
},
},
};
</script>

Yukarıda LoginForm componentinin yapısını görebilirsiniz. Eğer isAdmin:true ise hayali olarak Admin Form değilse User form render edilecektir. Şimdi bunun testini yapalım.

import { mount,shallowMount } from "@vue/test-utils"
import LoginForm from "../../src/components/PropsData/LoginForm";


describe("Props Data Testing", () => {
test("should render user form",() => {
const shallowWrapper = shallowMount(LoginForm, {
propsData: {
isAdmin: false
}
})
expect(shallowWrapper.find("[data-test=\"user-form\"]").text())
.toBe("User Form Design")
})
})

Test işleminin detaylarını anlatmaya başlayalım. mount fonksiyonunun ikinci parametresine bir object ve bunun içine prop binding işlemini yapabilmemiz için bize yardımcı olan propsData’nın nasıl verildiğini görebiliyoruz.

isAdmin, child componentimin beklediği prop’tur. Buna göre ilk olarak false olarak bind edelim ve false olması durumunda User Formun render edileceğini bekleriz.

  • expect → user form render edildi mi?
  • toBe → edildi ise bunu doğrula

expect bir jest fonksiyonudur ve içine test edilecek değeri alır. Buna göre ilk olarak senaryomuza göre user form’un bulunduğu div’i yakalıyoruz. Bunu daha önce bahsettiğim gibi wrapperların bizler için sağladığı özel fonksiyonlar vasıtasıyla gerçekleştiriyoruz. Bu testimizde ilgili yardımcı find() olmaktadır. Find içine bir CSS attribute vererek ilgili user-formunun bulunduğu div’i yakalayabiliyoruz. CSS Attribute selector nasıl çalışıyor bilmiyorsanız buraya tıklayarak öğrenebilirsiniz.

shallowWrapper.find("[data-test=\"user-form\"]")

Div bu adımda bulundu. Sırada beklenen değerin expect içinde iddia edilmesi gerekmektedir. Ve bu iddia sonrasında doğrulanmalıdır. toBe tam olarak bu doğrulamanın yapılması için bize yardım eden araçtır. Eğer ki isAdmin:false ise git ilgili div’i bul ve onun içindeki yazının (toBe) “User Form Design” olmasını bekle.(expect)

expect(shallowWrapper.find("[data-test=\"user-form\"]").text())
.toBe("User Form Design")

Şimdi ise isAdmin:true ile prop binding yapılırsa testimizin ne şekilde yazılması gerektiğini konuşalım. Yukarıdaki testin altına bu durumun testini ekleyelim.

test("should render admin form",() => {
const shallowWrapper = shallowMount(LoginForm, {
propsData: {
isAdmin: true
}
})
expect(shallowWrapper.find("[data-test=\"admin-form\"]").text())
.toBe("Admin Form Design")
})

Burada farklı olan ilk olarak isAdmin: true olarak bind edilir. Eğer bu şekilde bind edildi ise benim kontrol etmem gereken şey admin formunun düzgün bir şekilde render edilip edilmediğidir. Bunun için yapmam gereken sadece expect içinde data-test değerinin ‘admin-form’ olarak değiştirilmesi ve beklenen değerin “Admin Form Design” olarak doğrulanması olayıdır. İncelediğimiz örneğe takip eden linke tıklayarak ilgili commit’i inceleyerek bakabilirsiniz.

Factory Function

Şimdiye kadar yazılan testlerde component mount ve props işlemlerini tekrar tekrar yazmak durumunda kaldık. Bundan sonra kod tekrarı yapmamak adına factory function kullanacağız. Bu tamamen mantıksal bir yaklaşım ve DRY prensibini uygulamak adına kullanacağımız bir yöntem.

Props testing örneğimizi bu mantıksal yaklaşım ile tekrar yazalım. Yukardaki kodlara göz attığımızda tekrar eden kısımların mount component ve props binding işlemleri olduğunu fark etmişsinizdir. Bunların hepsini factory function içinde toplayabiliriz.

describe("Props Data Testing", () => {
const factory = (options) => {
return mount(LoginForm, {
propsData: {
...options
}
})
}
test("should render user form",() => {
const params = {isAdmin: false}
const shallowWrapper = factory(params)
expect(shallowWrapper.find("[data-test=\"user-form\"]").text())
.toBe("User Form Design")
})
test("should render admin form",() => {
const params = {isAdmin: true}
const shallowWrapper = factory(params)
expect(shallowWrapper.find("[data-test=\"admin-form\"]").text())
.toBe("Admin Form Design")
})
})

Artık mount işlemini factory fonksiyonu ile yapıyor ve dahası ilerde test edilecek hedef component’in test edilecek kısımlarının (store,computeds, methods…) tek bir noktadan hızlı bir şekilde yönetilebilmesini sağladık. Takip eden bağlantıya tıklayarak ilgili commit’i inceleyebilirsiniz. (Factory Function)

Bu bölümde ne öğrendik?

  • test için kullanılacak olan props’ların propsData ile nasıl set edileceği
  • DRY prensibine uygun test yazabilmeyi

Bunlara ek olarak propsData yerine setProps’da kullanılabilmektedir. Daha detaylı bilgi için takip eden bağlantıya gidebilirsiniz.(setProps)

Testing Computed Properties

Computed test işlemi oldukça kolaydır. Çünkü herhangi bir JavaScript fonksiyonundan farkı yoktur.

Senaryomuza göre yaşı 18'den küçük olan bir kişinin sistemimize üye olmasını engellemek isteyelim. Kullanıcı yaşını girsin ve kayıt ol butonuna tıkladığında ise bir uyarı açılsın ve içinde ‘Yaşınız 18’den küçük olduğu için sistemimize kayıt işleminiz gerçekleştirilememiştir.’ yazsın. Buna göre test işlemlerimizi gerçekleştirelim.

<template>
<div>
<h1 v-if="checkAge" data-test="warning">Yaşınız 18'den küçük olduğu için sistemimize kayıt işleminiz gerçekleştirilememiştir.</h1>
<h1 v-else>Kayıt başarılı bir şekilde gerçekleşmiştir.</h1>
</div>
</template>

<script>
export default {
name: 'ComputedComponent',
props: {
age: {
type: Number,
},
},
computed: {
checkAge() {
return this.age < 18;
}
},
};
</script>

Şimdi bu componentimizi test edelim.

computed içinde checkAge isimli property eğer ki age < 18 ise true döner ve <h1> etiketi içinde ilgili mesaj yazdırılır. Adımlarımız belli oldu.

  • component’i mount et
  • mount ederken age isimli prop gönder
  • hedef computed’i çalıştır
  • dönen değerin true olduğunu doğrula
  • sonrasında hata mesajının yazdırıldığı h1 etiketini yakala
  • yakaladığın etiket arasında mesajın yazdırıldığını doğrula

Yukarıdaki adımlara göre testimizi yazarsak aşağıdaki sonucu elde ederiz.

import { mount } from "@vue/test-utils"
import Computed from "../../src/components/Computed/Computed";

describe("Computed Properties Testing", () => {
const factory = (options) => {
return mount(Computed, {
propsData: {
...options
}
})
}
test("should see warning form",() => {
const params = {age: 17}
const wrapper = factory(params)
expect(wrapper.vm.checkAge).toBeTruthy()
const warning = wrapper.find("[data-test=\"warning\"]")
expect(warning.text()).toEqual('Yaşınız 18\'den küçük olduğu için sistemimize kayıt işleminiz gerçekleştirilememiştir.')
})
})

Ne yaptığımızı anlatmaya başlayalım.

İlk olarak zaten bildiğimiz prop gönderme işlemini yapıyoruz. Sonrasında checkAge isimli computed’i tetiklemem lazım. Bunun için yine wrapper object’in yardımcı araçlarından vm’i kullanabilirim.

Her Vue component’i bir Vue instance’sidir. Bu instance’ye referans eden ise vm ( view model ) değişkenidir. Ve bu referans aracılığı ile Vue component’i içinde istediğim noktaya ulaşabilirim. Bu şekilde checkAge computed’i tetikliyorum.

Sonrasında ise ilgili h1 etiketini bulup içindeki mesajın doğruluğunu test ederek işlemimi tamamlıyorum.

Bu bölümde ne öğrendik?

  • computed property nasıl test edilir
  • vm nedir ve nasıl kullanılabilir

Bu bağlantıya tıklayarak örnekte yapılan işlemleri inceleyebilirsiniz.

Testing Emitted Events

Projeler büyüdükçe componentler arası veri aktarımı da artıyor. Parent component child component’e veri aktarımını prop aracılığı ile yapıyorken tam tersi işlem ise emitted eventler aracılığı ile gerçekleşmektedir.

Vue Test Utils emitted eventlerin test edilmesi için ‘emitted API’ ile bize destek vermektedir. (emitted)

emitted syntax

Öncelikle emitted ne şekilde çalışır bunu öğrenerek başlayalım. Bunun için aşağıdaki örneğe ve çıktısına bakalım.

<template>
<h1>Emitter Component</h1>
</template>

<script>
export default {
name: "EmitterComponent",
methods: {
emitMyEvent() {
this.$emit("sendNameAge", "Alp", "20")
}
}
}
</script>

Görüldüğü üzere bir isim ve yaş bilgisi emit edilmektedir.

import { mount } from "@vue/test-utils"
import Emitter from "../../src/components/Emitter/Emitter";

describe("Emits an event testing", () => {
const factory = () => {
return mount(Emitter)
}
test("emits and event with parameters",() => {
const wrapper = factory()
wrapper.vm.emitMyEvent()
console.log(wrapper.emitted())
})
})

Şimdi test dosyasını inceleyelim.

console.log(wrapper.emitted())

satırı bize wrapper object aracılığıyla sunulan bir özelliktir. Burda bilinmesi gereken en önemli unsur emitted’in bir object döndürdüğüdür. Bu test dosyasının çıktısına bakacak olursak daha net anlayacağız.

Görüldüğü üzere emitted bir object döndürür ve içinde emit edilen event’in ismi ve buna atanmış bir dizi ve bu dizinin içinde ise gönderilmek istenen parametreler yine bir dizinin elemanları olarak bulunur.

Yukarıdaki bilgiye dayanarak şu işlemlerin ne sonuç verdiğine bakalım.

console.log(wrapper.emitted().sendNameAge);
  • [ [ ‘Alp’, ‘20’ ] ]
console.log(wrapper.emitted().sendNameAge[0]);
  • [ ‘Alp’, ‘20’ ]
console.log(wrapper.emitted().sendNameAge[0][0]);
  • Alp
console.log(wrapper.emitted().sendNameAge[0][1]);
  • 20

emitted nasıl ve ne şekilde kullanılır öğrendiğimize göre şimdi test işlemini yapabiliriz.

import { mount } from "@vue/test-utils"
import Emitter from "../../src/components/Emitter/Emitter";

describe("Emits an event testing", () => {
const factory = () => {
return mount(Emitter)
}
test("emits and event with parameters",() => {
const wrapper = factory()
wrapper.vm.emitMyEvent()
expect(wrapper.emitted().sendNameAge[0]).toEqual(["Alp", "20"])
})
})

Öğrendiğimiz bilgileri kullanarak bir assertion işlemini gerçekleştirmiş olduk. Bu örneği incelemek için ilgili commit’e gidebilirsiniz.

Sonraki yazımda mocking global objects, triggering events —user input, mocking a HTTP Client — Axios gibi konulara değineceğim.

--

--