React/Redux Uygulamalarını Jest ve Enzyme ile Test Etmek – Bölüm 4: Redux Reducer’larını Test Etmek
Redux, modern React uygulamalarında durum yönetimini (state management) basitleştiren güçlü bir kütüphanedir. Uygulama durumunun merkezi bir yerde tutulmasını ve öngörülebilir bir şekilde değiştirilmesini sağlar. Redux ekosisteminin kalbinde ise “reducer” adı verilen fonksiyonlar bulunur. Bu makale serimizin dördüncü bölümünde, Redux reducer’larını Jest ve Enzyme ile nasıl etkin bir şekilde test edeceğimizi derinlemesine inceleyeceğiz. Reducer’lar, Redux uygulamanızın mantığının büyük bir kısmını barındırdığı için, doğru bir şekilde test edilmeleri uygulamanızın güvenilirliği ve sürdürülebilirliği açısından kritik öneme sahiptir.
Giriş: Neden Reducer’ları Test Etmeliyiz?
Redux mimarisinde, uygulamanın tüm durumu tek bir merkezi “store” içinde saklanır. Bu durum, doğrudan değiştirilemez; bunun yerine, durumu değiştirmek için “eylemler” (actions) gönderilir. Eylemler, Redux store’a ulaştığında, “reducer” adı verilen fonksiyonlar tarafından işlenir. Reducer’lar, mevcut durumu ve bir eylemi girdi olarak alarak, uygulamanın yeni durumunu döndüren saf fonksiyonlardır.
Reducer’ların test edilmesi, Redux tabanlı uygulamaların geliştirme sürecinde vazgeçilmez bir adımdır. Bunun birkaç önemli nedeni vardır:
* Mantığın Doğruluğunu Garanti Etmek: Reducer’lar, uygulamanızın durumunun nasıl değişeceğini tanımlayan temel iş mantığını içerir. Bu mantığın doğru çalıştığından emin olmak, uygulamanın beklenen şekilde davranmasını sağlar. Testler, her bir eylemin durumu doğru bir şekilde güncellediğini doğrular.
* Refaktoring Güvenliği: Kod tabanınızı refaktör ederken veya yeni özellikler eklerken, mevcut işlevselliği bozma riski her zaman vardır. Kapsamlı reducer testleri, kodunuzda yaptığınız değişikliklerin mevcut mantığı etkilemediğini garanti ederek size güven verir. Bir testi bozmadan refaktör edemiyorsanız, bu, yaptığınız değişikliğin yanlış olduğu veya testinizin güncellenmesi gerektiği anlamına gelir.
* Hata Yakalamayı Kolaylaştırmak: Reducer testleri, potansiyel hataları geliştirme sürecinin erken aşamalarında tespit etmeye yardımcı olur. Hatalar, üretime ulaşmadan önce yakalandığında, düzeltilmesi çok daha kolay ve maliyetsizdir.
* Dokümantasyon Niteliği: İyi yazılmış testler, kodun kendisi gibi bir dokümantasyon görevi görür. Bir geliştirici, bir reducer’ın ne yaptığını anlamak istediğinde, ilgili test dosyasına bakarak farklı eylemlerin durumu nasıl etkilediğini hızlıca görebilir.
* Saf Fonksiyon Olmaları Nedeniyle Kolay Test Edilebilirlik: Reducer’lar, yan etkileri olmayan (side-effect-free) saf fonksiyonlardır. Bu özellik, onların izole bir şekilde ve öngörülebilir bir biçimde test edilmesini son derece kolaylaştırır. Dış bağımlılıkları olmadığı için, karmaşık sahteleme (mocking) veya ortam kurulumlarına genellikle ihtiyaç duyulmaz.
Bu nedenlerden dolayı, Redux uygulamalarında reducer testlerine yeterince zaman ve çaba ayırmak, uzun vadede projenin sağlığı ve geliştirme hızına önemli katkı sağlar.
Redux Reducer’larının Temelleri ve Test Edilebilirliği
Redux reducer’ları, belirli bir imza ve davranış kurallarına uyan fonksiyonlardır. Bu kurallar, reducer’ların test edilebilirliğini doğrudan etkiler ve kolaylaştırır.
Reducer Tanımı ve Saf Fonksiyon Özellikleri
Bir Redux reducer’ı, iki ana argüman alan bir fonksiyondur: mevcut durum (state) ve bir eylem (action). Fonksiyonun döndürdüğü değer ise uygulamanın yeni durumudur. İmza şu şekildedir: (state, action) => newState.
Reducer’ların en önemli özelliği, “saf fonksiyon” (pure function) olmalarıdır. Saf fonksiyonlar, aşağıdaki özellikleri taşır:
1. Aynı Girdi İçin Aynı Çıktıyı Verir: Bir reducer’ı aynı başlangıç durumu ve aynı eylemle birden fazla kez çağırdığınızda, her zaman aynı yeni durumu döndürmelidir.
2. Yan Etki (Side Effect) Üretmez: Saf fonksiyonlar, kendi kapsamları dışındaki herhangi bir şeyi değiştirmezler. Bu, veritabanı çağrıları, API istekleri, dosya okuma/yazma, DOM manipülasyonu veya konsola log yazma gibi işlemleri yapmadıkları anlamına gelir. Reducer’lar sadece yeni bir durum nesnesi oluşturur ve döndürür.
3. Girdi Argümanlarını Değiştirmez (Immutable): Reducer’lar, kendilerine gelen state veya action objelerini doğrudan değiştirmemelidir. Bunun yerine, mevcut durumdan türetilmiş yeni bir durum nesnesi oluşturup geri döndürmelidirler. Bu ilke, “immutability” (değişmezlik) olarak bilinir ve Redux’un temel taşlarından biridir.
Bu saf fonksiyon özellikleri, reducer’ların test edilebilirliğini olağanüstü derecede artırır. Çünkü:
* İzolasyon: Reducer’lar tamamen izoledir. Dış dünyayla etkileşimleri olmadığı için, onları test etmek için karmaşık ortam kurulumlarına veya dış bağımlılıkları sahteleme (mocking) işlemlerine gerek kalmaz. Sadece girdi (state, action) ve beklenen çıktı (newState) ile ilgileniriz.
* Öngörülebilirlik: Aynı girdiler her zaman aynı çıktıyı üreteceği için, testler yazmak ve beklenen sonuçları doğrulamak basittir.
* Immutability’nin Önemi: Durum nesnelerinin değiştirilmeden yeni bir kopyasının oluşturulması, zaman yolculuğu hata ayıklaması (time-travel debugging) gibi Redux’un gelişmiş özelliklerini mümkün kılar. Testlerde, durumun gerçekten değişmez olup olmadığını kontrol etmek için expect(newState).not.toBe(currentState); gibi ifadeler kullanmak önemlidir. Bu, JavaScript’teki nesne referanslarının farklı olduğunu doğrular, yani yeni bir nesne oluşturulmuştur.
Jest ve Enzyme ile Reducer Test Ortamını Kurma
Bu makale serisinin önceki bölümlerinde Jest ve Enzyme’nin temel kurulumunu ele almıştık. Reducer testleri için genellikle Enzyme’ye ihtiyaç duyulmaz çünkü reducer’lar UI bileşenleri değildir; tamamen JavaScript fonksiyonlarıdır. Bu nedenle, testlerimizi yazarken sadece Jest’in güçlü test çatısından faydalanacağız.
Jest Kurulumuna Kısa Bir Hatırlatma
Eğer projenizde Jest kurulu değilse, aşağıdaki komutla kurabilirsiniz:
npm install --save-dev jest
veya
yarn add --dev jest
package.json dosyanıza bir test betiği eklemek iyi bir uygulamadır:
{
"scripts": {
"test": "jest"
}
}
Örnek Bir Reducer Yapısı
Şimdi basit bir todoReducer.js dosyası oluşturalım:
// src/reducers/todoReducer.js
const initialState = {
todos: [],
filter: 'ALL',
};
const todoReducer = (state = initialState, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [
...state.todos,
{
id: action.payload.id,
text: action.payload.text,
completed: false,
},
],
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
),
};
case 'SET_FILTER':
return {
...state,
filter: action.payload.filter,
};
case 'REMOVE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload.id)
};
default:
return state;
}
};
export default todoReducer;
Bu reducer’ı test etmek için, genellikle aynı dizinde veya __tests__ gibi özel bir dizinde todoReducer.test.js adında bir dosya oluştururuz.
// src/reducers/todoReducer.test.js
import todoReducer from './todoReducer';
describe('todoReducer', () => {
// Test senaryoları buraya gelecek
});
describe bloğu, ilgili testleri gruplandırmak için kullanılır. it veya test fonksiyonları ise her bir bireysel test senaryosunu tanımlar. Jest’in expect fonksiyonu ile de beklenen sonuçları doğrularız.
Temel Reducer Test Senaryoları
Reducer’ları test ederken ele almamız gereken birkaç temel senaryo vardır. Bu senaryolar, reducer’ınızın her durumda doğru davrandığından emin olmanızı sağlar.
Başlangıç Durumunu Test Etmek (Initial State)
Bir reducer’ın en temel sorumluluklarından biri, state argümanı undefined olduğunda başlangıç durumunu (initial state) döndürmektir. Bu genellikle Redux store ilk oluşturulduğunda veya bir reducer’ın durumu henüz belirlenmediğinde gerçekleşir.
// src/reducers/todoReducer.test.js
import todoReducer from './todoReducer';
describe('todoReducer', () => {
it('başlangıç durumu tanımlı değilse, başlangıç durumunu döndürmelidir', () => {
const expectedInitialState = {
todos: [],
filter: 'ALL',
};
// Redux genellikle '@@INIT' gibi bir eylem türü gönderir
// veya sadece tanımsız bir eylem türü de aynı sonucu vermelidir.
expect(todoReducer(undefined, { type: '@@INIT' })).toEqual(expectedInitialState);
expect(todoReducer(undefined, {})).toEqual(expectedInitialState); // Boş bir eylem objesiyle de test edilebilir
});
});
Burada, todoReducer‘ı undefined bir state ve rastgele bir eylemle çağırıyoruz. toEqual matcher’ı, nesnelerin derinlemesine eşitliğini kontrol eder, bu da karmaşık durum objeleri için idealdir.
Tanımsız Eylemleri Test Etmek (Unknown Actions)
Bir reducer, kendisine gelen eylem türünü tanımıyorsa, mevcut durumu değiştirmeden geri döndürmelidir. Bu, uygulamanın durumunun beklenmedik eylemlerle bozulmamasını sağlar.
// src/reducers/todoReducer.test.js
import todoReducer from './todoReducer';
describe('todoReducer', () => {
// ... (önceki testler)
it('tanımsız bir eylem türü ile mevcut durumu değiştirmeden döndürmelidir', () => {
const currentState = {
todos: [{ id: 1, text: 'Test Todo', completed: false }],
filter: 'ALL',
};
const unknownAction = { type: 'UNKNOWN_ACTION' };
const newState = todoReducer(currentState, unknownAction);
expect(newState).toEqual(currentState);
// Immutability kontrolü: Nesne referansı aynı olmalı, çünkü durum değişmedi.
expect(newState).toBe(currentState);
});
});
Bu testte, reducer’ın tanımadığı bir eylem türü ile çağrıldığında mevcut durumu olduğu gibi geri döndürdüğünü ve hatta nesne referansının bile değişmediğini (çünkü bir değişiklik yapılmadı) doğrularız.
Belirli Eylemleri Test Etmek (Specific Actions)
Bu, reducer testlerinin ana gövdesini oluşturur. Her bir tanımlı eylem türü için, reducer’ın durumu nasıl değiştirdiğini doğrulamamız gerekir.
ADD_TODO Eylemini Test Etmek
// src/reducers/todoReducer.test.js
import todoReducer from './todoReducer';
describe('todoReducer', () => {
// ... (önceki testler)
it('ADD_TODO eylemi ile yeni bir todo eklemelidir', () => {
const currentState = {
todos: [],
filter: 'ALL',
};
const action = {
type: 'ADD_TODO',
payload: { id: 1, text: 'Yeni görev' },
};
const newState = todoReducer(currentState, action);
expect(newState.todos.length).toBe(1);
expect(newState.todos[0]).toEqual({
id: 1,
text: 'Yeni görev',
completed: false,
});
// Immutability kontrolü: Yeni bir durum nesnesi döndürülmeli
expect(newState).not.toBe(currentState);
expect(newState.todos).not.toBe(currentState.todos);
});
it('mevcut todolara yeni bir todo eklemelidir', () => {
const currentState = {
todos: [{ id: 1, text: 'Mevcut görev', completed: false }],
filter: 'ALL',
};
const action = {
type: 'ADD_TODO',
payload: { id: 2, text: 'İkinci görev' },
};
const newState = todoReducer(currentState, action);
expect(newState.todos.length).toBe(2);
expect(newState.todos[1]).toEqual({
id: 2,
text: 'İkinci görev',
completed: false,
});
expect(newState.todos[0]).toEqual(currentState.todos[0]); // Mevcut todonun değişmediğini kontrol et
expect(newState).not.toBe(currentState);
expect(newState.todos).not.toBe(currentState.todos);
});
});
Bu örnekte, ADD_TODO eyleminin hem boş bir todos dizisine hem de mevcut todos dizisine nasıl yeni bir öğe eklediğini test ediyoruz. Ayrıca, newState ve newState.todos‘un currentState ve currentState.todos‘tan farklı referanslara sahip olduğunu doğrulayarak immutability’yi kontrol ediyoruz.
TOGGLE_TODO Eylemini Test Etmek
// src/reducers/todoReducer.test.js
import todoReducer from './todoReducer';
describe('todoReducer', () => {
// ... (önceki testler)
it('TOGGLE_TODO eylemi ile belirli bir todonun tamamlanma durumunu değiştirmelidir', () => {
const currentState = {
todos: [
{ id: 1, text: 'Görev 1', completed: false },
{ id: 2, text: 'Görev 2', completed: true },
],
filter: 'ALL',
};
const action = {
type: 'TOGGLE_TODO',
payload: { id: 1 },
};
const newState = todoReducer(currentState, action);
expect(newState.todos[0].completed).toBe(true);
expect(newState.todos[1].completed).toBe(true); // Diğer todonun değişmediğini kontrol et
expect(newState).not.toBe(currentState);
expect(newState.todos).not.toBe(currentState.todos);
});
it('var olmayan bir ID için TOGGLE_TODO eylemi ile durumu değiştirmemelidir', () => {
const currentState = {
todos: [{ id: 1, text: 'Görev 1', completed: false }],
filter: 'ALL',
};
const action = {
type: 'TOGGLE_TODO',
payload: { id: 999 }, // Var olmayan ID
};
const newState = todoReducer(currentState, action);
expect(newState).toEqual(currentState); // Durum değişmemeli
expect(newState).not.toBe(currentState); // Ancak immutability ilkesi gereği, todos dizisi map edildiği için yeni bir dizi referansı dönebilir.
// Bu durumda toEqual yeterlidir, toBe kontrolü yanıltıcı olabilir.
// Reducer mantığınıza göre değişir. Eğer hiçbir değişiklik yoksa, direkt return state; yapılması daha iyi olurdu.
// Mevcut durumda map her zaman yeni bir dizi döndürür.
expect(newState.todos).not.toBe(currentState.todos); // Dizinin referansı değişmiş olmalı
});
});
TOGGLE_TODO testinde, belirli bir todo‘nun completed özelliğinin doğru şekilde değiştiğini ve diğer todo‘ların etkilenmediğini kontrol ediyoruz. Ayrıca, var olmayan bir ID ile çağrıldığında durumun değişmediğini de test etmek önemlidir. İmmutability kontrolü burada biraz daha nüanslıdır: map fonksiyonu her zaman yeni bir dizi döndürdüğü için, durum değişmese bile newState.todos‘un referansı değişecektir. Bu durumda toEqual ile içerik eşitliğini kontrol etmek daha doğru bir yaklaşımdır.
SET_FILTER ve REMOVE_TODO Eylemlerini Test Etmek
Benzer şekilde, diğer eylemler için de testler yazabiliriz:
// src/reducers/todoReducer.test.js
import todoReducer from './todoReducer';
describe('todoReducer', () => {
// ... (önceki testler)
it('SET_FILTER eylemi ile filtreyi değiştirmelidir', () => {
const currentState = {
todos: [],
filter: 'ALL',
};
const action = {
type: 'SET_FILTER',
payload: { filter: 'COMPLETED' },
};
const newState = todoReducer(currentState, action);
expect(newState.filter).toBe('COMPLETED');
expect(newState).not.toBe(currentState);
});
it('REMOVE_TODO eylemi ile belirli bir todoyu kaldırmalıdır', () => {
const currentState = {
todos: [
{ id: 1, text: 'Görev 1', completed: false },
{ id: 2, text: 'Görev 2', completed: true },
],
filter: 'ALL',
};
const action = {
type: 'REMOVE_TODO',
payload: { id: 1 },
};
const newState = todoReducer(currentState, action);
expect(newState.todos.length).toBe(1);
expect(newState.todos[0]).toEqual({ id: 2, text: 'Görev 2', completed: true });
expect(newState).not.toBe(currentState);
expect(newState.todos).not.toBe(currentState.todos);
});
it('var olmayan bir ID için REMOVE_TODO eylemi ile durumu değiştirmemelidir', () => {
const currentState = {
todos: [{ id: 1, text: 'Görev 1', completed: false }],
filter: 'ALL',
};
const action = {
type: 'REMOVE_TODO',
payload: { id: 999 }, // Var olmayan ID
};
const newState = todoReducer(currentState, action);
expect(newState).toEqual(currentState);
expect(newState).toBe(currentState); // Hiçbir değişiklik olmadığı için referans da aynı kalmalı
});
});
Bu testlerde de immutability kontrollerini ve beklenen durum değişikliklerini doğru bir şekilde yaptığımızdan emin oluyoruz. Özellikle REMOVE_TODO‘nun var olmayan bir ID ile çağrıldığında durumu hiç değiştirmemesi (ve referansın aynı kalması) beklenen bir davranıştır.
Asenkron Eylemleri ve Reducer Etkileşimlerini Test Etmek
Redux reducer’ları, tanım gereği saf ve senkron fonksiyonlardır. Bu, reducer’ların doğrudan asenkron işlemleri (API çağrıları, zamanlayıcılar vb.) ele almadığı anlamına gelir. Asenkron mantık, genellikle Redux Thunk, Redux Saga veya Redux Observable gibi ara yazılımlar (middleware) aracılığıyla yönetilir.
Bu ara yazılımlar, asenkron bir işlem başladığında (örn. FETCH_USERS_REQUEST), başarılı olduğunda (FETCH_USERS_SUCCESS) veya başarısız olduğunda (FETCH_USERS_FAILURE) farklı senkron eylemler gönderir. Reducer’ların görevi, bu senkron eylemleri dinlemek ve duruma uygun değişiklikleri uygulamaktır.
Dolayısıyla, reducer testleri, asenkron işlemin kendisini değil, asenkron işlem sonucunda gönderilen senkron eylemlerin durumu nasıl etkilediğini doğrulamalıdır.
Örneğin, kullanıcıları getiren bir API çağrınız olduğunu varsayalım. Bu işlem üç aşamalı bir dizi eylem tetikleyebilir:
* FETCH_USERS_REQUEST: Kullanıcılar yüklenmeye başlandı. Durumda bir isLoading: true bayrağı ayarlanabilir.
* FETCH_USERS_SUCCESS: Kullanıcılar başarıyla yüklendi. users dizisi güncellenir, isLoading: false olur.
* FETCH_USERS_FAILURE: Kullanıcılar yüklenirken bir hata oluştu. error mesajı ayarlanır, isLoading: false olur.
Reducer’ınız bu eylemleri aşağıdaki gibi işleyebilir:
// src/reducers/userReducer.js
const initialUserState = {
users: [],
isLoading: false,
error: null,
};
const userReducer = (state = initialUserState, action) => {
switch (action.type) {
case 'FETCH_USERS_REQUEST':
return { ...state, isLoading: true, error: null };
case 'FETCH_USERS_SUCCESS':
return { ...state, isLoading: false, users: action.payload.users };
case 'FETCH_USERS_FAILURE':
return { ...state, isLoading: false, error: action.payload.error };
default:
return state;
}
};
export default userReducer;
Bu reducer’ı test ederken, her bir senkron eylemin durum üzerindeki etkisini ayrı ayrı doğrularız:
// src/reducers/userReducer.test.js
import userReducer from './userReducer';
describe('userReducer', () => {
it('FETCH_USERS_REQUEST eylemi ile isLoading durumunu true yapmalı ve hatayı temizlemelidir', () => {
const currentState = { users: [], isLoading: false, error: 'some error' };
const action = { type: 'FETCH_USERS_REQUEST' };
const newState = userReducer(currentState, action);
expect(newState.isLoading).toBe(true);
expect(newState.error).toBeNull();
expect(newState.users).toEqual(currentState.users); // Diğer kısımlar değişmemeli
});
it('FETCH_USERS_SUCCESS eylemi ile kullanıcıları yüklemeli ve isLoading durumunu false yapmalıdır', () => {
const currentState = { users: [], isLoading: true, error: null };
const fetchedUsers = [{ id: 1, name: 'Alice' }];
const action = { type: 'FETCH_USERS_SUCCESS', payload: { users: fetchedUsers } };
const newState = userReducer(currentState, action);
expect(newState.isLoading).toBe(false);
expect(newState.users).toEqual(fetchedUsers);
expect(newState.error).toBeNull();
});
it('FETCH_USERS_FAILURE eylemi ile hatayı ayarlamalı ve isLoading durumunu false yapmalıdır', () => {
const currentState = { users: [], isLoading: true, error: null };
const errorMessage = 'Network error';
const action = { type: 'FETCH_USERS_FAILURE', payload: { error: errorMessage } };
const newState = userReducer(currentState, action);
expect(newState.isLoading).toBe(false);
expect(newState.error).toBe(errorMessage);
expect(newState.users).toEqual(currentState.users);
});
});
Gördüğünüz gibi, reducer testleri yalnızca senkron durum geçişlerine odaklanır. Asenkron mantığın kendisini (örneğin, bir thunk’ın doğru API çağrısını yapıp yapmadığını) test etmek için farklı stratejiler (middleware testleri) gereklidir, ancak bu, reducer testlerinin kapsamı dışındadır.
Gerçek Dünya Senaryoları ve İleri Düzey Test Teknikleri
Gerçek dünya Redux uygulamaları genellikle daha karmaşık durum yapılarına ve daha çeşitli eylem türlerine sahiptir. Bu tür senaryolarda reducer testlerini daha etkili hale getirmek için bazı ileri düzey teknikler ve yaklaşımlar mevcuttur.
Birden Fazla Eylem Türünü Test Etmek
Bazı durumlarda, bir reducer’ın durumu farklı eylem türlerinin ardışık çağrılarıyla nasıl değiştiğini test etmek isteyebilirsiniz. Ancak, her testin izole olması gerektiği için, genellikle her eylem türünü ayrı ayrı test etmek daha iyidir. Eğer bir eylemin sonucu diğer bir eylemin başlangıç durumu olacaksa, bunu test içinde currentState olarak tanımlayarak yapabilirsiniz.
Karmaşık Durum Yapılarını Test Etmek (Nested States)
Uygulamanız büyüdükçe, Redux durumunuz iç içe geçmiş objeler ve diziler içerebilir. Bu tür durumları güncellerken immutability’yi korumak kritik öneme sahiptir. JavaScript’in spread operatörü (...) veya Immer gibi yardımcı kütüphaneler bu konuda size yardımcı olabilir. Testlerde, bu karmaşık yapıların doğru bir şekilde güncellendiğini ve değişmeyen kısımlarının da bozulmadığını doğrulamak için toEqual matcher’ı önemlidir.
Örneğin, bir kullanıcının profil bilgilerini güncelleyen bir reducer:
// src/reducers/profileReducer.js
const initialProfileState = {
user: {
id: null,
name: '',
email: '',
address: {
street: '',
city: '',
},
},
isEditing: false,
};
const profileReducer = (state = initialProfileState, action) => {
switch (action.type) {
case 'UPDATE_USER_NAME':
return {
...state,
user: {
...state.user,
name: action.payload.name,
},
};
case 'UPDATE_USER_ADDRESS_CITY':
return {
...state,
user: {
...state.user,
address: {
...state.user.address,
city: action.payload.city,
},
},
};
// ... diğer eylemler
default:
return state;
}
};
export default profileReducer;
Testi:
// src/reducers/profileReducer.test.js
import profileReducer from './profileReducer';
describe('profileReducer', () => {
const initialProfileState = {
user: {
id: 1,
name: 'John Doe',
email: 'john@example.com',
address: {
street: '123 Main St',
city: 'Anytown',
},
},
isEditing: false,
};
it('UPDATE_USER_NAME eylemi ile kullanıcının adını güncellemelidir', () => {
const action = { type: 'UPDATE_USER_NAME', payload: { name: 'Jane Doe' } };
const newState = profileReducer(initialProfileState, action);
expect(newState.user.name).toBe('Jane Doe');
expect(newState.user.email).toBe(initialProfileState.user.email); // Diğer alanlar değişmemeli
expect(newState.user.address).toEqual(initialProfileState.user.address); // İç içe nesneler de değişmemeli
expect(newState).not.toBe(initialProfileState);
expect(newState.user).not.toBe(initialProfileState.user);
expect(newState.user.address).toBe(initialProfileState.user.address); // Adres nesnesinin referansı değişmemeli
});
it('UPDATE_USER_ADDRESS_CITY eylemi ile kullanıcının adresindeki şehri güncellemelidir', () => {
const action = { type: 'UPDATE_USER_ADDRESS_CITY', payload: { city: 'New City' } };
const newState = profileReducer(initialProfileState, action);
expect(newState.user.address.city).toBe('New City');
expect(newState.user.address.street).toBe(initialProfileState.user.address.street); // Diğer adres alanları değişmemeli
expect(newState.user.name).toBe(initialProfileState.user.name); // Kullanıcı adı değişmemeli
expect(newState).not.toBe(initialProfileState);
expect(newState.user).not.toBe(initialProfileState.user);
expect(newState.user.address).not.toBe(initialProfileState.user.address); // Adres nesnesinin referansı değişmeli
});
});
Bu testlerde, derinlemesine nesnelerin nasıl güncellendiğini ve immutability kurallarının nasıl uygulandığını gösteriyoruz. expect(newState.user.address).toBe(initialProfileState.user.address); gibi kontroller, güncellenmeyen alt nesnelerin referanslarının aynı kaldığını doğrulamak için önemlidir.
Test Ortamını Hazırlama ve Temizleme (beforeEach, afterEach)
Birden fazla test senaryosunda aynı başlangıç durumunu kullanıyorsanız, Jest’in beforeEach veya beforeAll kancalarını kullanarak her testten önce veya tüm testlerden önce ortak bir başlangıç durumu tanımlayabilirsiniz.
// src/reducers/todoReducer.test.js
import todoReducer from './todoReducer';
describe('todoReducer', () => {
let initialTodoState; // Her test için sıfırlanacak
beforeEach(() => {
initialTodoState = {
todos: [],
filter: 'ALL',
};
});
it('başlangıç durumu tanımlı değilse, başlangıç durumunu döndürmelidir', () => {
expect(todoReducer(undefined, { type: '@@INIT' })).toEqual(initialTodoState);
});
it('ADD_TODO eylemi ile yeni bir todo eklemelidir', () => {
const action = {
type: 'ADD_TODO',
payload: { id: 1, text: 'Yeni görev' },
};
const newState = todoReducer(initialTodoState, action); // beforeEach'ten gelen state'i kullan
expect(newState.todos.length).toBe(1);
});
// ... diğer testler
});
beforeEach, her it bloğundan önce çalışır ve her testin temiz bir başlangıç durumuyla başlamasını sağlar. Bu, testlerin birbirinden bağımsız olmasını garanti eder.
Test Kapsamı (Test Coverage) ve En İyi Uygulamalar
Reducer testlerini yazarken, sadece testlerin geçmesini sağlamakla kalmayıp, aynı zamanda kodunuzun yeterince kapsandığından da emin olmak önemlidir.
Test Kapsamı (Test Coverage)
Jest, yerleşik test kapsamı raporlama özelliğine sahiptir. jest --coverage komutunu çalıştırarak, hangi kod satırlarının testler tarafından çalıştırıldığını gösteren ayrıntılı bir rapor alabilirsiniz.
npm test -- --coverage
veya
yarn test --coverage
Bu rapor, test edilmemiş kod yollarını (örneğin, bir switch ifadesindeki bir case bloğu veya bir if ifadesinin else dalı) belirlemenize yardımcı olur. %100 kod kapsamı her zaman %100 hata ayıklama garantisi vermez, ancak mantığınızın her parçasının en az bir kez çalıştırıldığını gösterir ve güvenilir bir başlangıç noktasıdır.
En İyi Uygulamalar
* Her case Statement’ı Test Edin: Reducer’ınızdaki her switch ifadesinin her case bloğunu ayrı ayrı test ettiğinizden emin olun.
* Her if/else Dalını Test Edin: Reducer’ınızda koşullu mantık (if/else) varsa, her iki dalın da test edildiğini doğrulayın.
* Testlerin Hızlı Olması: Reducer testleri, UI bileşeni testlerine veya entegrasyon testlerine göre çok daha hızlı olmalıdır. Bu hız, geliştirme döngüsünü hızlandırır.
* Anlamlı Test İsimleri Kullanın: it('should add a todo', ...) gibi açıklayıcı test isimleri, test raporlarını okurken veya bir test başarısız olduğunda neyin yanlış gittiğini anlamanıza yardımcı olur.
* Tek Bir Şeyi Test Edin (Arrange-Act-Assert): Her testin tek bir belirli davranışı veya durum geçişini doğrulamaya odaklanması iyi bir uygulamadır.
* Arrange (Hazırlık): Test için gerekli başlangıç durumunu ve eylemi ayarlayın.
* Act (Eylem): Reducer’ı bu durum ve eylemle çağırın.
* Assert (Doğrulama): Yeni durumun beklenen sonuçla eşleştiğini doğrulayın.
* Mocking’e Gerek Yok: Reducer’lar saf fonksiyonlar olduğu için, genellikle dış bağımlılıkları sahteleme (mocking) ihtiyacı duymazsınız. Bu, testleri daha basit ve daha güvenilir hale getirir.
* Immutability Kontrolü: toEqual ile içerik eşitliğini kontrol etmenin yanı sıra, not.toBe veya toBe ile nesne referanslarının doğru şekilde değişip değişmediğini kontrol etmek, immutability kurallarını pekiştirir.
Sonuç
Redux reducer’larını Jest ile test etmek, Redux tabanlı uygulamaların geliştirme sürecinde temel ve kritik bir adımdır. Reducer’ların saf fonksiyon doğası, onları son derece test edilebilir kılar ve bu da uygulamanızın durum yönetim mantığının doğruluğunu ve güvenilirliğini garanti etmenizi sağlar.
Bu makalede, reducer testlerinin neden bu kadar önemli olduğunu, başlangıç durumu, tanımsız eylemler ve belirli eylemler gibi temel senaryoları nasıl ele alacağımızı, asenkron eylemlerin reducer testleri üzerindeki dolaylı etkilerini ve karmaşık durum yapılarını test etmek için ileri düzey teknikleri inceledik. Ayrıca, test kapsamının önemi ve reducer testleri için en iyi uygulamalar hakkında da konuştuk.
Kapsamlı reducer testleri yazarak, uygulamanızın temel iş mantığının sağlam olduğundan emin olabilir, gelecekteki değişiklikler için güvenli bir zemin oluşturabilir ve geliştirme sürecinizin genel kalitesini artırabilirsiniz. Bu, Redux uygulamanızın sağlam bir temel üzerine inşa edildiğini bilerek, diğer test türlerine (bileşen testleri, entegrasyon testleri vb.) geçiş yapmanıza olanak tanır.