Takip et

React / Redux Uygulamalarını Jest ve Enzyme ile Test Etmek – Bölüm 3: Redux Aksiyonlarını Test Etmek

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önet

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.

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.