React / Redux Uygulamalarını Jest ve Enzyme ile Test Etmek – Bölüm 3: Redux Aksiyonlarını Test Etmek
Redux, React uygulamalarında durum yönetimini merkezi ve öngörülebilir bir şekilde ele almak için kullanılan güçlü bir kütüphanedir. Uygulamanız büyüdükçe, Redux mimarisinin her bir parçasının (aksiyonlar, reducer’lar, bileşenler) beklediği gibi çalıştığından emin olmak kritik hale gelir. Bu serinin ilk bölümünde React bileşenlerini Jest ve Enzyme ile test etmeyi, ikinci bölümünde ise Redux reducer’larını test etmeyi ele aldık. Şimdi sıra, Redux ekosisteminin önemli bir parçası olan aksiyonları test etmeye geldi.
Aksiyonlar, Redux uygulamanızın durumunda meydana gelen değişiklikleri açıklayan sade JavaScript nesneleridir. Bir aksiyon, uygulamanızda ne olduğunu belirtir, ancak durumun nasıl değişeceğini açıklamaz; bu reducer’ların görevidir. Aksiyonlar, genellikle aksiyon yaratıcıları (action creators) adı verilen fonksiyonlar aracılığıyla oluşturulur. Özellikle asenkron aksiyonlar, API çağrıları gibi yan etkileri yönettiği ve birden fazla aksiyonu sırayla gönderdiği için test edilmesi gereken karmaşık mantık içerebilir.
Aksiyonları test etmek, uygulamanızın temel iş mantığının ve dış servislerle (API’ler gibi) etkileşimlerinin doğru çalıştığından emin olmanızı sağlar. Bu bölümde, Jest’i ana test çerçevesi olarak kullanarak Redux aksiyonlarını nasıl test edeceğimizi derinlemesine inceleyeceğiz. Enzyme, bileşen testleri için harika olsa da, aksiyon testleri için daha çok redux-mock-store ve HTTP isteklerini taklit etmek için axios-mock-adapter veya nock gibi kütüphanelere ihtiyacımız olacak.
Redux Aksiyonlarını Anlamak
Testlere geçmeden önce Redux aksiyonlarının yapısını kısaca hatırlayalım:
* Senkron Aksiyonlar: Bunlar, hemen bir aksiyon nesnesi döndüren basit fonksiyonlardır. Örneğin, bir todo listesine öğe ekleme veya bir filtrenin değerini değiştirme gibi durum değişikliklerini tetiklerler. Bu aksiyonlar genellikle herhangi bir yan etki (side effect) içermez.
* Asenkron Aksiyonlar (Thunk’lar): Redux Thunk gibi ara yazılımlar (middleware) kullanıldığında, aksiyon yaratıcıları sadece bir nesne döndürmek yerine bir fonksiyon döndürebilirler. Bu fonksiyonlar genellikle dispatch ve getState argümanlarını alır ve API çağrıları, zamanlayıcılar veya diğer yan etkileri yönetmek için kullanılır. Yan etki tamamlandığında veya başarısız olduğunda, bir veya daha fazla senkron aksiyonu dispatch ederler.
Bu bölümde, her iki tür aksiyon yaratıcısını da nasıl test edeceğimizi göstereceğiz.
Redux Aksiyon Testleri İçin Kurulum
Aksiyonları test etmek için aşağıdaki araçlara ihtiyacımız olacak:
* Jest: JavaScript test çerçevesi. Projenizde zaten kurulu olmalı.
* redux-mock-store: Redux store’u taklit etmek için kullanılır. Asenkron aksiyonların dispatch ettiği tüm aksiyonları kaydeder, böylece bunları doğrulayabiliriz.
* redux-thunk: Asenkron aksiyonlar için Redux Thunk kullanıyorsanız, test ortamında da bu middleware’e ihtiyacımız olacak.
* axios (veya benzeri bir HTTP istemcisi): API çağrıları için kullanılıyorsa.
* axios-mock-adapter (veya nock): HTTP isteklerini taklit etmek için. Bu, testlerimizin gerçek API’lere bağımlı olmamasını ve daha hızlı çalışmasını sağlar.
Kurulum komutları:
npm install --save-dev redux-mock-store redux-thunk axios-mock-adapter axios
veya yarn
yarn add --dev redux-mock-store redux-thunk axios-mock-adapter axios
Proje yapısı genellikle src/actions klasöründe aksiyon dosyalarını ve src/__tests__/actions içinde ilgili test dosyalarını içerir.
Senkron Aksiyon Yaratıcılarını Test Etmek
Senkron aksiyon yaratıcıları en basitidir. Sadece belirli bir türde ve yükte (payload) bir aksiyon nesnesi döndürdüklerinden emin olmamız gerekir.
Örnek Senkron Aksiyon
Bir todos uygulaması için yeni bir todo ekleyen bir aksiyon yaratıcısı düşünelim:
// src/actions/todoActions.js
let nextTodoId = 0; // Basit bir ID yönetimi
export const addTodo = (text) => ({
type: 'ADD_TODO',
payload: {
id: nextTodoId++,
text,
},
});
export const setVisibilityFilter = (filter) => ({
type: 'SET_VISIBILITY_FILTER',
payload: {
filter,
},
});
Senkron Aksiyon Testi
Bu aksiyon yaratıcılarını test etmek oldukça basittir. Fonksiyonu çağırırız ve döndürdüğü nesnenin beklenen type ve payload değerlerine sahip olup olmadığını kontrol ederiz.
// src/__tests__/actions/todoActions.test.js
import { addTodo, setVisibilityFilter } from '../../actions/todoActions';
describe('Senkron Aksiyon Yaratıcıları', () => {
it('addTodo, beklenen ADD_TODO aksiyonunu oluşturmalı', () => {
const text = 'Redux öğren';
const expectedAction = {
type: 'ADD_TODO',
payload: {
id: expect.any(Number), // ID'nin bir sayı olmasını bekliyoruz
text,
},
};
// ID'nin artan bir değer olduğunu varsayarsak, her testte farklı olabilir.
// Bu yüzden 'expect.any(Number)' kullanmak daha güvenli.
// Veya ID üretme mantığını mock'layabiliriz.
expect(addTodo(text)).toEqual(expectedAction);
});
it('setVisibilityFilter, beklenen SET_VISIBILITY_FILTER aksiyonunu oluşturmalı', () => {
const filter = 'SHOW_COMPLETED';
const expectedAction = {
type: 'SET_VISIBILITY_FILTER',
payload: {
filter,
},
};
expect(setVisibilityFilter(filter)).toEqual(expectedAction);
});
});
Burada expect.any(Number) kullanmak, id‘nin tam değerini bilmemiz gerekmediğinde veya her çağrıda değişebileceği durumlarda faydalıdır. Eğer id‘nin belirli bir değer olmasını bekliyorsak, nextTodoId‘yi testten önce sıfırlayabilir veya taklit edebiliriz.
Asenkron Aksiyon Yaratıcılarını Test Etmek (Redux Thunk)
Asenkron aksiyonlar, genellikle API çağrıları veya diğer yan etkileri içerdiğinden daha karmaşıktır. Bu tür aksiyonlar birden fazla senkron aksiyonu dispatch edebilir (örneğin, bir isteğin başladığını, başarılı olduğunu veya başarısız olduğunu belirten aksiyonlar). redux-mock-store ve HTTP taklit kütüphaneleri burada devreye girer.
Örnek Asenkron Aksiyon (API Çağrısı)
Kullanıcıları bir API’den çeken bir asenkron aksiyon düşünelim:
// src/actions/userActions.js
import axios from 'axios';
// Senkron aksiyon yaratıcıları
export const fetchUsersRequest = () => ({
type: 'FETCH_USERS_REQUEST',
});
export const fetchUsersSuccess = (users) => ({
type: 'FETCH_USERS_SUCCESS',
payload: users,
});
export const fetchUsersFailure = (error) => ({
type: 'FETCH_USERS_FAILURE',
payload: error,
});
// Asenkron aksiyon yaratıcısı (Thunk)
export const fetchUsers = () => async (dispatch) => {
dispatch(fetchUsersRequest()); // İstek başladı aksiyonunu gönder
try {
const response = await axios.get('/api/users');
dispatch(fetchUsersSuccess(response.data)); // Başarılı aksiyonu gönder
} catch (error) {
dispatch(fetchUsersFailure(error.message)); // Hata aksiyonunu gönder
}
};
Asenkron Aksiyon Testi İçin Kurulum
redux-mock-store‘u redux-thunk middleware’i ile yapılandırmamız gerekiyor:
// src/__tests__/actions/userActions.test.js
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; // Axios isteklerini taklit etmek için
import {
fetchUsers,
fetchUsersRequest,
fetchUsersSuccess,
fetchUsersFailure,
} from '../../actions/userActions';
// Redux Thunk middleware'ini mock store'a ekliyoruz
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
// Axios'u taklit etmek için bir adaptör oluşturuyoruz
const mockAxios = new MockAdapter(axios);
describe('Asenkron Aksiyon Yaratıcıları (Thunks)', () => {
// Her testten sonra mock'u temizle
afterEach(() => {
mockAxios.reset();
});
// ... testlerimiz buraya gelecek
});
Başarılı API Çağrısı Durumunu Test Etmek
API çağrısı başarılı olduğunda FETCH_USERS_REQUEST ve ardından FETCH_USERS_SUCCESS aksiyonlarının gönderildiğini doğrulamalıyız.
// src/__tests__/actions/userActions.test.js (devamı)
it('kullanıcılar başarıyla çekildiğinde FETCH_USERS_SUCCESS aksiyonunu göndermeli', async () => {
const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
// Axios'un /api/users adresine yapılan GET isteğine 200 durum kodu ve kullanıcı verileriyle yanıt vermesini taklit et
mockAxios.onGet('/api/users').reply(200, users);
const expectedActions = [
fetchUsersRequest(), // İlk olarak istek başladı aksiyonu
fetchUsersSuccess(users), // Ardından başarılı aksiyonu
];
// Boş bir başlangıç durumu ile mock store oluştur
const store = mockStore({});
// Asenkron aksiyonu dispatch et ve tamamlanmasını bekle
await store.dispatch(fetchUsers());
// Store tarafından gönderilen aksiyonların beklenen aksiyonlarla eşleştiğini doğrula
expect(store.getActions()).toEqual(expectedActions);
});
Burada mockAxios.onGet('/api/users').reply(200, users); ile /api/users adresine yapılan bir GET isteğinin başarılı bir yanıt döndüreceğini belirtiyoruz. store.dispatch(fetchUsers()) çağrıldıktan sonra, store.getActions() metodu, dispatch edilen tüm aksiyonların bir listesini döndürür. Bu listeyi beklenen aksiyon dizisiyle karşılaştırırız.
Başarısız API Çağrısı Durumunu Test Etmek
API çağrısı başarısız olduğunda FETCH_USERS_REQUEST ve ardından FETCH_USERS_FAILURE aksiyonlarının gönderildiğini doğrulamalıyız.
// src/__tests__/actions/userActions.test.js (devamı)
it('kullanıcı çekme başarısız olduğunda FETCH_USERS_FAILURE aksiyonunu göndermeli', async () => {
const errorMessage = 'Network Error';
// Axios'un /api/users adresine yapılan GET isteğine 500 durum kodu ve hata mesajıyla yanıt vermesini taklit et
mockAxios.onGet('/api/users').reply(500, errorMessage);
const expectedActions = [
fetchUsersRequest(), // İstek başladı aksiyonu
fetchUsersFailure(errorMessage), // Hata aksiyonu
];
const store = mockStore({});
await store.dispatch(fetchUsers());
expect(store.getActions()).toEqual(expectedActions);
});
Bu test, mockAxios.onGet('/api/users').reply(500, errorMessage); ile API’nin bir hata döndüreceğini simüle eder ve fetchUsersFailure aksiyonunun doğru hata mesajıyla dispatch edildiğini doğrular.
getState Kullanılan Aksiyonları Test Etmek
Bazı asenkron aksiyonlar, mevcut Redux durumuna erişmek için getState argümanını kullanır. Örneğin, bir verinin zaten yüklenip yüklenmediğini kontrol edebilir ve yalnızca gerekliyse bir API çağrısı yapabilirler.
Örnek getState Kullanan Aksiyon
// src/actions/dataActions.js
export const fetchDataIfNeeded = () => (dispatch, getState) => {
const state = getState();
if (state.data.isLoading || state.data.items.length > 0) {
return; // Veri zaten yükleniyor veya yüklendi, tekrar çekme
}
// Normalde burada bir API çağrısı olurdu
dispatch({ type: 'FETCH_DATA_REQUEST' });
// Simülasyon için hemen başarı aksiyonu gönderelim
setTimeout(() => {
dispatch({ type: 'FETCH_DATA_SUCCESS', payload: ['item1', 'item2'] });
}, 10);
};
getState Kullanan Aksiyon Testi
redux-mock-store‘a başlangıç durumu sağlayarak getState‘i taklit edebiliriz.
// src/__tests__/actions/dataActions.test.js
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { fetchDataIfNeeded } from '../../actions/dataActions';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('getState kullanan aksiyonlar', () => {
it('veri zaten yüklüyse veya yüklendiyse hiçbir aksiyon göndermemeli', async () => {
// Durum: veri yükleniyor
let store = mockStore({ data: { isLoading: true, items: [] } });
await store.dispatch(fetchDataIfNeeded());
expect(store.getActions()).toEqual([]);
// Durum: veri zaten yüklendi
store = mockStore({ data: { isLoading: false, items: ['item1'] } });
await store.dispatch(fetchDataIfNeeded());
expect(store.getActions()).toEqual([]);
});
it('veri yüklenmediyse ve yüklenmiyorsa aksiyonları göndermeli', async () => {
const expectedActions = [
{ type: 'FETCH_DATA_REQUEST' },
{ type: 'FETCH_DATA_SUCCESS', payload: ['item1', 'item2'] },
];
const store = mockStore({ data: { isLoading: false, items: [] } });
await store.dispatch(fetchDataIfNeeded());
// Asenkron işlemi beklemek için küçük bir gecikme ekleyelim (setTimeout yüzünden)
// Gerçek bir API çağrısında await yeterli olurdu.
await new Promise((resolve) => setTimeout(resolve, 20));
expect(store.getActions()).toEqual(expectedActions);
});
});
Bu testlerde, mockStore({ data: { isLoading: true, items: [] } }) gibi bir başlangıç durumu sağlayarak getState() çağrısı yapıldığında hangi durumun döndürüleceğini belirliyoruz. Bu sayede aksiyonun içindeki koşullu mantığı test edebiliriz. setTimeout kullandığımız için, testin asenkron çağrının tamamlanmasını beklemesi için ek bir await new Promise((resolve) => setTimeout(resolve, 20)); ekledik. Gerçek bir API çağrısında axios gibi kütüphaneler Promise döndürdüğü için bu ek gecikmeye genellikle gerek kalmaz.
Aksiyon Testleri İçin En İyi Uygulamalar
1. Tüm Yolları Test Edin: Asenkron aksiyonlar için hem başarı hem de hata durumlarını test ettiğinizden emin olun.
2. Dış Bağımlılıkları Taklit Edin: API çağrıları, Date.now(), localStorage gibi dış bağımlılıkları testlerinizden izole etmek için taklit edin. Bu, testlerin daha hızlı, daha güvenilir ve tekrarlanabilir olmasını sağlar.
3. Aksiyon Yaratıcılarını Odaklı Tutun: Aksiyon yaratıcıları genellikle durumu doğrudan değiştirmek yerine dispatch etme ve yan etkileri yönetme sorumluluğuna sahiptir. Karmaşık durum dönüşümlerini reducer’lara bırakın.
4. Açıklayıcı Test İsimleri Kullanın: Testlerinizin neyi test ettiğini açıkça belirten isimler kullanın. Bu, test başarısız olduğunda sorunu anlamanıza yardımcı olur.
5. Testleri İzole Edin: Her testin diğer testlerden bağımsız olduğundan emin olun. afterEach kancalarını kullanarak mock’ları sıfırlamak buna yardımcı olur.
6. Gerçek Aksiyon Yaratıcılarını Kullanın: Testlerinizde fetchUsersRequest() gibi gerçek aksiyon yaratıcılarını kullanın, aksiyon nesnelerini doğrudan { type: 'FETCH_USERS_REQUEST' } şeklinde yazmak yerine. Bu, aksiyon yaratıcılarınızda bir değişiklik olduğunda testlerinizin sizi uyarmasını sağlar.
Sonuç
Redux aksiyonlarını test etmek, uygulamanızın veri akışının ve yan etkilerinin doğru çalıştığından emin olmak için hayati öneme sahiptir. Bu bölümde, Jest’i redux-mock-store, redux-thunk ve axios-mock-adapter gibi yardımcı kütüphanelerle birlikte kullanarak hem senkron hem de asenkron aksiyon yaratıcılarını nasıl test edeceğimizi öğrendik.
Senkron aksiyonlar için doğrudan döndürülen nesneyi doğrulamak yeterliyken, asenkron aksiyonlar için redux-mock-store‘un dispatch edilen aksiyonları kaydetme yeteneğini ve HTTP isteklerini taklit etme araçlarını kullandık. getState kullanan aksiyonlar için ise mock store’a bir başlangıç durumu sağlayarak testlerimizi gerçekleştirdik.
Artık React bileşenlerini, Redux reducer’larını ve Redux aksiyonlarını nasıl test edeceğinizi biliyorsunuz. Bu üç temel taşın sağlam bir şekilde test edilmesi, uygulamanızın sağlamlığını ve sürdürülebilirliğini önemli ölçüde artıracaktır. Bir sonraki bölümde, Redux selector’larını test etme veya tüm parçaları bir araya getiren entegrasyon testlerine odaklanabiliriz.