Takip et

Jest.spyOn: Birim Testleriniz İçin Kapsamlı Kılavuz

Modern JavaScript uygulamalarında kod kalitesi ve güvenilirliği, birim testleriyle başlar. Peki, dış bağımlılıkları olan veya yan etkiler üreten fonksiyonları test ederken ne yapmalısınız? Jest.spyOn, bu zorlukların üstesinden gelerek testlerinizi daha izole ve yönetilebilir hale getirmenizi sağlar. Bu kapsamlı kılavuzda, Jest.spyOn’ın ne olduğunu, nasıl kullanıldığını ve birim test stratejinizi nasıl dönüştüreceğini detaylarıyla öğreneceksiniz.

Yazılım geliştirme süreçlerinde, özellikle de karmaşık ve modüler uygulamalarda, birim testleri (unit tests) kritik bir rol oynar. Bir birimin (fonksiyon, metod, sınıf) beklenen şekilde çalıştığını doğrulamak için yazılan bu testler, kodunuzun güvenilirliğini artırır ve gelecekteki değişikliklerin mevcut işlevselliği bozmasını engeller. Ancak, gerçek dünyadaki birçok fonksiyon, kendi başına izole bir şekilde çalışmaz; veritabanlarına, dış API’lere, dosya sistemlerine veya diğer modüllere bağımlıdırlar. İşte tam da bu noktada Jest.spyOn gibi araçlar devreye girer ve test etme sürecini kökten değiştirir.

Bir fonksiyonun dış bağımlılıkları olduğunda, bu bağımlılıkların gerçek implementasyonlarıyla test yapmak birkaç soruna yol açabilir. Öncelikle, testler yavaşlar. Her test koşulduğunda bir veritabanı bağlantısı kurmak, bir API çağrısı yapmak veya bir dosyayı okumak, test süresini uzatır ve geliştirici deneyimini olumsuz etkiler. İkincisi, testler kararsız hale gelir. Dış servislerdeki gecikmeler, ağ hataları veya veritabanı değişiklikleri, kodunuzda hiçbir hata olmamasına rağmen testlerin başarısız olmasına neden olabilir. Bu duruma “flaky tests” denir ve geliştiricilerin testlere olan güvenini sarsar. Üçüncüsü, birim testi prensiplerine aykırıdır. Birim testlerinin temel amacı, ilgili birimi izole bir şekilde test etmektir. Bağımlılıkların gerçek implementasyonlarıyla test yapmak, aslında entegrasyon testi benzeri bir senaryo yaratır ve birimin kendi mantığından ziyade bağımlılıkların davranışlarını da test etmeye başlarsınız. Bu da hata tespiti ve hata ayıklama süreçlerini zorlaştırır.

Jest.spyOn, tam da bu sorunları çözmek için tasarlanmış güçlü bir Jest aracıdır. Bir objenin mevcut bir metodunu “casuslamanıza” (spy on) olanak tanır. Yani, metodun orijinal implementasyonunu korurken, o metoda yapılan çağrıları izleyebilir, hangi argümanlarla çağrıldığını görebilir, kaç kez çağrıldığını sayabilir ve hatta dönüş değerini veya davranışını geçici olarak değiştirebilirsiniz. Bu yetenek, bir fonksiyonun dış bağımlılıklarını izole etmenin ve test ortamında kontrol edilebilir hale getirmenin anahtarıdır. Böylece, testleriniz daha hızlı, daha güvenilir ve daha izole olur. Metodun yan etkilerini, yani veritabanına yazma veya bir HTTP isteği gönderme gibi işlemleri, test ortamında gerçekte gerçekleşmeden simüle edebilirsiniz. Bu sayede, odak noktanız sadece test ettiğiniz birimin kendi mantığına kayar, dış etkenlerden etkilenmezsiniz. Özellikle karmaşık sistemlerde, her bir modülün ayrı ayrı test edilebilir olması, genel kod kalitesini önemli ölçüde artırır ve hata ayıklama süreçlerini basitleştirir. Kısacası, Jest.spyOn birim testlerinizi daha anlamlı, daha hızlı ve daha güvenilir hale getiren vazgeçilmez bir araçtır. Bu sayede geliştiriciler, kodlarının beklenen şekilde çalıştığından emin olabilir ve projeye güvenle devam edebilirler.

Jest.spyOn Temelleri: Nasıl Çalışır ve Neler Sunar?

Jest.spyOn, bir objenin zaten var olan bir metodunu izlemek ve/veya geçici olarak değiştirmek için kullanılan özel bir fonksiyonudur. Temel olarak, bir casus (spy) oluşturarak, metodun çağrılma durumunu (kaç kez çağrıldı, hangi argümanlarla çağrıldı, ne döndürdü) kaydedersiniz. Ancak bunu yaparken, metodun orijinal implementasyonunu varsayılan olarak korursunuz. Bu, Jest.fn() ile olan temel farklarından biridir; Jest.fn() sıfırdan sahte bir fonksiyon oluştururken, Jest.spyOn var olan bir metodun üzerine bir “dinleyici” yerleştirir. Bu özellik, özellikle bir modülün kendi içinde başka bir metodunu çağırdığı senaryolarda veya dış bir bağımlılığın belirli bir metodunun davranışını izlemek istediğinizde çok değerli hale gelir.

Jest.spyOn’ın temel sözdizimi oldukça basittir: jest.spyOn(object, methodName). Burada object, üzerinde metodun bulunduğu nesne, methodName ise casuslamak istediğiniz metodun adıdır (string olarak). Bu fonksiyonu çağırdığınızda, size özel bir “jest mock function” döndürür. Bu mock fonksiyon, çağrı geçmişini izleme yeteneğine sahip olmasının yanı sıra, metodun davranışını geçici olarak değiştirmenize olanak tanıyan çeşitli yardımcı metotlara da sahiptir.

İşte Jest.spyOn ile birlikte sıkça kullanılan bazı yardımcı metotlar ve ne işe yaradıkları:

  • .mockReturnValue(value)

    : Casusladığınız metodun her çağrıldığında belirli bir değer döndürmesini sağlar. Metodun orijinal implementasyonu bu durumda çalışmaz. Örneğin, bir API çağrısının her zaman belirli bir başarı yanıtı döndürmesini simüle etmek isteyebilirsiniz.

  • .mockImplementation((...args) => { ... })

    : Metodun orijinal implementasyonunu tamamen özel bir fonksiyon ile değiştirir. Bu, daha karmaşık davranışları (örneğin, argümanlara bağlı olarak farklı değerler döndürme) simüle etmek için idealdir.

  • .mockResolvedValue(value)

    : Asenkron metotlar (Promise döndürenler) için kullanılır. Metodun başarılı bir şekilde çözümlenmesini (resolve) ve belirli bir değer döndürmesini sağlar. API çağrılarını veya veritabanı işlemlerini taklit etmek için vazgeçilmezdir.

  • .mockRejectedValue(error)

    : Yine asenkron metotlar için kullanılır, ancak metodun bir Promise hatasıyla reddedilmesini (reject) simüle eder. Hata senaryolarını test etmek için kritiktir.

  • .and.callThrough()

    : Bu özel bir durumdur. Eğer casusladıktan sonra metodun orijinal implementasyonunun yine de çalışmasını istiyorsanız bunu kullanırsınız. Varsayılan davranış budur ancak bazen başka bir .mock* metodu kullandıktan sonra geri dönmek için açıkça belirtmek gerekebilir.

  • .and.returnValue(value)

    : .mockReturnValue() ile benzerdir ancak daha çok Jest'in eski versiyonlarında veya belirli zincirleme senaryolarında görülebilir.

Jest.spyOn'ın gücü, hem çağrıları izleyebilme hem de metodun davranışını kontrol edebilme yeteneğinden gelir. Örneğin, bir veritabanı servisinin save() metodunu casuslayarak, bu metodun gerçekten veritabanına yazıp yazmadığını değil, sadece doğru argümanlarla çağrılıp çağrılmadığını test edebilirsiniz. Ardından, .mockResolvedValue({ id: 1 }) ile sahte bir dönüş değeri sağlayarak, veritabanı işleminin başarılı olduğunu simüle edebilirsiniz. Bu şekilde, testleriniz hızlanır ve dış etkenlerden arınır. Ayrıca, her testin sonunda casusladığınız metodu orijinal haline döndürmek için .mockRestore() metodunu kullanmak iyi bir uygulamadır. Bu, testler arasında "sızıntı" olmasını engeller ve test ortamının temiz kalmasını sağlar. Kısacası, Jest.spyOn ile kodunuzun bağımlılıklarını soyutlayarak daha güvenilir ve sürdürülebilir bir test altyapısı kurabilirsiniz. Bu esneklik, özellikle büyük ve karmaşık projelerde, birim testlerinin etkinliğini ve değerini katlayarak artırır.

İlk Adımlar: Basit Bir Metodu Nasıl Casuslarsınız?

Jest.spyOn ile çalışmaya başlamak, düşündüğünüzden çok daha kolaydır. Basit bir örnek üzerinden bu güçlü aracın nasıl kullanıldığını adım adım inceleyelim. Diyelim ki, basit matematiksel işlemler yapan bir calculator objemiz var ve bu objenin add metodunu casuslamak istiyoruz. Amacımız, add metodunun belirli argümanlarla çağrılıp çağrılmadığını kontrol etmek ve belki de geçici olarak farklı bir değer döndürmesini sağlamak.

Öncelikle, casuslayacağımız objeyi tanımlayalım. Bir calculator.js dosyamız olduğunu varsayalım:


// calculator.js
const calculator = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b,
};

module.exports = calculator;
  

Şimdi bu calculator objesini test etmek için bir calculator.test.js dosyası oluşturalım. Test dosyamızda Jest.spyOn kullanarak add metodunu nasıl casuslayacağımızı görelim:


// calculator.test.js
const calculator = require('./calculator');

describe('Calculator', () => {
  let addSpy;

  // Her testten önce casusu sıfırla veya yeniden oluştur
  beforeEach(() => {
    // calculator objesinin 'add' metodunu casusla
    addSpy = jest.spyOn(calculator, 'add');
  });

  // Her testten sonra casusu orijinal haline döndür
  afterEach(() => {
    addSpy.mockRestore(); // Bu satır çok önemlidir!
  });

  test('add metodu doğru argümanlarla çağrılmalı ve doğru sonuç dönmeli', () => {
    // Casuslanan metodu çağır
    const result = calculator.add(2, 3);

    // add metodunun çağrıldığını kontrol et
    expect(addSpy).toHaveBeenCalledTimes(1);
    expect(addSpy).toHaveBeenCalledWith(2, 3);

    // Orijinal implementasyonun çalıştığını doğrula
    expect(result).toBe(5);
  });

  test('add metodunu farklı bir değer döndürecek şekilde mock edebiliriz', () => {
    // add metodunun her çağrıldığında 100 döndürmesini sağla
    addSpy.mockReturnValue(100);

    const result1 = calculator.add(5, 5); // Orijinalde 10 dönecekti
    const result2 = calculator.add(10, 20); // Orijinalde 30 dönecekti

    expect(result1).toBe(100);
    expect(result2).toBe(100);

    expect(addSpy).toHaveBeenCalledTimes(2); // İki kez çağrıldı
    expect(addSpy).toHaveBeenCalledWith(5, 5); // İlk çağrı kontrolü
    expect(addSpy).toHaveBeenCalledWith(10, 20); // İkinci çağrı kontrolü
  });

  test('add metodunun implementasyonunu değiştirebiliriz', () => {
    // add metodunu özel bir implementasyon ile değiştir
    addSpy.mockImplementation((a, b) => a * b); // Şimdi toplama yerine çarpma yapacak

    const result = calculator.add(2, 3); // Orijinalde 5 dönecekti

    expect(result).toBe(6); // Çarpma sonucunu bekliyoruz
    expect(addSpy).toHaveBeenCalledTimes(1);
    expect(addSpy).toHaveBeenCalledWith(2, 3);
  });
});
  

Bu örnekte gördüğünüz gibi, jest.spyOn(calculator, 'add') ile calculator objesinin add metodunu başarıyla casusladık. beforeEach ve afterEach blokları, her testten önce casusu temizleyip yeniden oluşturarak testler arası bağımsızlığı sağlamak için kritik öneme sahiptir. Özellikle addSpy.mockRestore() çağrısı, her test bittiğinde casuslanan metodun orijinal implementasyonuna geri dönmesini garanti eder. Bu olmadan, bir testte yapılan mock işlemi diğer testleri etkileyebilir ve "side effects" denen istenmeyen durumlara yol açabilir. İlk test, metodun orijinal davranışını koruduğunu ve çağrı bilgilerini başarıyla yakaladığını gösterirken, ikinci test .mockReturnValue() ile metodun sahte bir değer döndürmesini sağladı. Üçüncü test ise .mockImplementation() ile metodun tamamen farklı bir işlev görmesini sağlayarak esnekliğini ortaya koydu. Bu basit adımlarla, herhangi bir objenin metodunu izleyebilir, davranışını değiştirebilir ve testlerinizi çok daha kontrol edilebilir bir hale getirebilirsiniz. Bu yetenek, birim testlerinizi yazarken size muazzam bir esneklik ve güç katacaktır.

Uzman İpucu: Jest.spyOn kullanırken

.mockRestore()

veya

jest.clearAllMocks()

/

jest.restoreAllMocks()

gibi temizlik fonksiyonlarını

afterEach

bloğunda kullanmayı asla unutmayın. Aksi takdirde, bir testteki mock işlemi diğer testleri etkileyebilir ve yanlış pozitif veya negatif sonuçlara yol açabilir. Bu, test süitinizin kararlılığı için hayati öneme sahiptir.

Gerçek Dünya Senaryoları: Jest.spyOn ile Karmaşık Bağımlılıkları Yönetme

Jest.spyOn'ın gerçek gücü, karmaşık uygulamalardaki dış bağımlılıkları yönetme ve izole etme yeteneğinde yatar. Üretim ortamında, bir fonksiyonun bir veritabanı servisiyle etkileşim kurması, dış bir API'ye istek göndermesi veya bir dosya sistemine yazma işlemi yapması oldukça yaygındır. Bu tür bağımlılıkları içeren fonksiyonları doğrudan test etmek, yukarıda bahsedildiği gibi yavaş, kararsız ve güvenilmez testlere yol açabilir. Jest.spyOn, bu senaryolarda devreye girerek testlerinizi kontrol edilebilir ve verimli hale getirir. Şimdi, birkaç gerçek dünya senaryosu üzerinden Jest.spyOn kullanımını inceleyelim.

Senaryo 1: Dış API Çağrısını Casuslama

Diyelim ki, kullanıcı bilgilerini almak için dış bir API'ye istek gönderen bir servisimiz var. Bu servisin testlerini yazarken, her seferinde gerçek API'ye istek göndermek istemeyiz. Hem yavaş olur hem de ağ bağlantısı veya API'nin durumu gibi dış etkenlere bağımlı hale geliriz. İşte burada Jest.spyOn devreye girer.

axios

gibi bir HTTP istemcisinin

get

metodunu casuslayarak, sahte bir API yanıtı döndürebiliriz.


// userService.js
const axios = require('axios');

const UserService = {
  async getUser(userId) {
    try {
      const response = await axios.get(https://api.example.com/users/${userId});
      return response.data;
    } catch (error) {
      console.error('Kullanıcı bilgileri alınamadı:', error.message);
      throw new Error('Kullanıcı bilgileri alınamadı.');
    }
  },

  async createUser(userData) {
    try {
      const response = await axios.post('https://api.example.com/users', userData);
      return response.data;
    } catch (error) {
      console.error('Kullanıcı oluşturulamadı:', error.message);
      throw new Error('Kullanıcı oluşturulamadı.');
    }
  }
};

module.exports = UserService;
  

Şimdi bu servisi test edelim. Axios'un get metodunu casuslayarak sahte bir yanıt döndüreceğiz:


// userService.test.js
const UserService = require('./userService');
const axios = require('axios');

describe('UserService', () => {
  let axiosGetSpy;
  let axiosPostSpy;

  beforeEach(() => {
    // axios.get metodunu casusla
    axiosGetSpy = jest.spyOn(axios, 'get');
    axiosPostSpy = jest.spyOn(axios, 'post');
  });

  afterEach(() => {
    axiosGetSpy.mockRestore(); // Casusu temizle
    axiosPostSpy.mockRestore(); // Casusu temizle
  });

  test('getUser, başarılı bir API yanıtı döndürmeli', async () => {
    const mockUserData = { id: 1, name: 'Alice', email: 'alice@example.com' };
    axiosGetSpy.mockResolvedValue({ data: mockUserData }); // Sahte yanıt ayarla

    const user = await UserService.getUser(1);

    expect(axiosGetSpy).toHaveBeenCalledTimes(1);
    expect(axiosGetSpy).toHaveBeenCalledWith('https://api.example.com/users/1');
    expect(user).toEqual(mockUserData);
  });

  test('getUser, API hatası durumunda bir hata fırlatmalı', async () => {
    const errorMessage = 'Network Error';
    axiosGetSpy.mockRejectedValue(new Error(errorMessage)); // Sahte hata ayarla

    await expect(UserService.getUser(1)).rejects.toThrow('Kullanıcı bilgileri alınamadı.');
    expect(axiosGetSpy).toHaveBeenCalledTimes(1);
    expect(axiosGetSpy).toHaveBeenCalledWith('https://api.example.com/users/1');
  });

  test('createUser, başarılı bir kullanıcı oluşturma yanıtı döndürmeli', async () => {
    const newUser = { name: 'Bob', email: 'bob@example.com' };
    const createdUser = { id: 2, ...newUser };
    axiosPostSpy.mockResolvedValue({ data: createdUser });

    const result = await UserService.createUser(newUser);

    expect(axiosPostSpy).toHaveBeenCalledTimes(1);
    expect(axiosPostSpy).toHaveBeenCalledWith('https://api.example.com/users', newUser);
    expect(result).toEqual(createdUser);
  });
});
  

Bu testlerde, axios.get ve axios.post metotlarını casuslayarak, gerçek API çağrılarını engelledik. Bunun yerine, .mockResolvedValue() veya .mockRejectedValue() kullanarak, sanki API'den yanıt geliyormuş gibi sahte senaryolar oluşturduk. Bu sayede, UserService'in API çağrısının başarılı veya başarısız olduğu durumlarda doğru davranıp davranmadığını hızlı ve güvenilir bir şekilde test edebiliriz.

Senaryo 2: Veritabanı Operasyonunu Casuslama

Veritabanı etkileşimleri, birim testlerinde sıkça karşılaşılan bir başka bağımlılıktır. Bir modelin veritabanına veri kaydetme veya sorgulama metodunu test ederken, gerçek veritabanı bağlantısı kurmak yerine bu operasyonları casuslamak çok daha etkilidir. Diyelim ki, bir

Product

modelimiz var ve bu modelin

save()

metodu, veritabanına yeni bir ürün kaydeder.


// productModel.js
const db = { // Basit bir veritabanı simülasyonu
  products: [],
  save: async (collection, data) => {
    // Gerçekte burada bir veritabanı işlemi olur
    return new Promise(resolve => {
      const newRecord = { id: db[collection].length + 1, ...data };
      db[collection].push(newRecord);
      setTimeout(() => resolve(newRecord), 50); // Asenkronluk için gecikme
    });
  },
  find: async (collection, query) => {
    return new Promise(resolve => {
      const result = db[collection].filter(item => Object.keys(query).every(key => item[key] === query[key]));
      setTimeout(() => resolve(result), 50);
    });
  }
};

class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price;
  }

  async save() {
    console.log(Ürün ${this.name} kaydediliyor...);
    const savedProduct = await db.save('products', { name: this.name, price: this.price });
    console.log(Ürün kaydedildi: ${savedProduct.id});
    return savedProduct;
  }

  static async findByName(name) {
    console.log(Ürün aranıyor: ${name});
    const products = await db.find('products', { name });
    return products;
  }
}

module.exports = Product;
  

Bu Product modelini test ederken, db.save ve db.find metotlarını casuslayarak veritabanı etkileşimini kontrol edebiliriz:


// productModel.test.js
const Product = require('./productModel');
const db = require('./productModel').db; // db objesini de import ediyoruz

describe('Product Model', () => {
  let dbSaveSpy;
  let dbFindSpy;

  beforeEach(() => {
    dbSaveSpy = jest.spyOn(db, 'save');
    dbFindSpy = jest.spyOn(db, 'find');
  });

  afterEach(() => {
    dbSaveSpy.mockRestore();
    dbFindSpy.mockRestore();
  });

  test('save metodu db.save metodunu doğru argümanlarla çağırmalı ve ürünü kaydetmeli', async () => {
    const mockSavedProduct = { id: 101, name: 'Test Product', price: 99.99 };
    dbSaveSpy.mockResolvedValue(mockSavedProduct); // Sahte DB yanıtı

    const product = new Product('Test Product', 99.99);
    const saved = await product.save();

    expect(dbSaveSpy).toHaveBeenCalledTimes(1);
    expect(dbSaveSpy).toHaveBeenCalledWith('products', { name: 'Test Product', price: 99.99 });
    expect(saved).toEqual(mockSavedProduct);
  });

  test('findByName metodu db.find metodunu doğru argümanlarla çağırmalı ve ürünleri döndürmeli', async () => {
    const mockProducts = [{ id: 201, name: 'Laptop', price: 1200 }];
    dbFindSpy.mockResolvedValue(mockProducts);

    const foundProducts = await Product.findByName('Laptop');

    expect(dbFindSpy).toHaveBeenCalledTimes(1);
    expect(dbFindSpy).toHaveBeenCalledWith('products', { name: 'Laptop' });
    expect(foundProducts).toEqual(mockProducts);
  });

  test('save metodu bir hata durumunda hatayı yukarı fırlatmalı', async () => {
    const errorMessage = 'Veritabanı bağlantı hatası!';
    dbSaveSpy.mockRejectedValue(new Error(errorMessage));

    const product = new Product('Hatalı Ürün', 10.00);
    await expect(product.save()).rejects.toThrow(errorMessage);
    expect(dbSaveSpy).toHaveBeenCalledTimes(1);
  });
});
  

Bu senaryolarda görüldüğü gibi, Jest.spyOn ile dış bağımlılıkları kontrol altına alarak sadece odaklandığınız birimin mantığını test edebilirsiniz. Bu, testlerinizi daha hızlı, daha güvenilir ve daha anlamlı hale getirir. Özellikle asenkron operasyonlarda

.mockResolvedValue()

ve

.mockRejectedValue()

kullanımı, başarı ve hata senaryolarını kolayca taklit etmenizi sağlayarak test kapsamınızı artırır.

Asenkron İşlemler ve Promise'lerle Çalışma

JavaScript ekosisteminde asenkron işlemler, özellikle Promise'ler ve async/await sözdizimi, modern uygulamaların vazgeçilmez bir parçasıdır. Bu asenkron yapıları test ederken, Jest.spyOn'ın sunduğu

.mockResolvedValue()

ve

.mockRejectedValue()

metotları hayati öneme sahiptir. Bu metotlar, casusladığınız asenkron bir metodun bir Promise'i başarıyla çözümlemesini veya bir hata ile reddetmesini taklit etmenizi sağlar.

Örneğin, bir e-posta gönderme servisimiz olsun:


// emailService.js
const Mailer = { // Harici bir mailer kütüphanesini simüle ediyoruz
  send: async (to, subject, body) => {
    return new Promise((resolve, reject) => {
      // Gerçekte burada bir API çağrısı veya SMTP bağlantısı olur
      console.log(E-posta gönderiliyor: ${subject} -> ${to});
      if (to && subject && body) {
        setTimeout(() => resolve({ success: true, message: 'E-posta başarıyla gönderildi.' }), 100);
      } else {
        setTimeout(() => reject(new Error('E-posta gönderme başarısız: Eksik bilgi.')), 100);
      }
    });
  }
};

class EmailService {
  async sendWelcomeEmail(userEmail) {
    if (!userEmail) {
      throw new Error('Kullanıcı e-posta adresi boş olamaz.');
    }
    const subject = 'Hoş Geldiniz!';
    const body = 'Uygulamamıza hoş geldiniz!';
    return await Mailer.send(userEmail, subject, body);
  }

  async sendPasswordResetEmail(userEmail, token) {
    if (!userEmail || !token) {
      throw new Error('E-posta veya token boş olamaz.');
    }
    const subject = 'Şifre Sıfırlama';
    const body = Şifrenizi sıfırlamak için şu linki kullanın: /reset?token=${token};
    return await Mailer.send(userEmail, subject, body);
  }
}

module.exports = new EmailService(); // Tekil bir örnek dışa aktarılıyor
  

Şimdi bu EmailService'i test edelim:


// emailService.test.js
const EmailService = require('./emailService');
const Mailer = require('./emailService').Mailer; // Mailer objesini de import ediyoruz

describe('EmailService', () => {
  let mailerSendSpy;

  beforeEach(() => {
    mailerSendSpy = jest.spyOn(Mailer, 'send');
  });

  afterEach(() => {
    mailerSendSpy.mockRestore();
  });

  test('sendWelcomeEmail başarılı olduğunda doğru mesajı döndürmeli', async () => {
    const successResponse = { success: true, message: 'E-posta başarıyla gönderildi.' };
    mailerSendSpy.mockResolvedValue(successResponse);

    const result = await EmailService.sendWelcomeEmail('test@example.com');

    expect(mailerSendSpy).toHaveBeenCalledTimes(1);
    expect(mailerSendSpy).toHaveBeenCalledWith('test@example.com', 'Hoş Geldiniz!', 'Uygulamamıza hoş geldiniz!');
    expect(result).toEqual(successResponse);
  });

  test('sendWelcomeEmail e-posta adresi boşsa hata fırlatmalı', async () => {
    // Bu durumda Mailer.send çağrılmayacak, sadece EmailService'in kendi kontrolü test ediliyor
    await expect(EmailService.sendWelcomeEmail(null)).rejects.toThrow('Kullanıcı e-posta adresi boş olamaz.');
    expect(mailerSendSpy).not.toHaveBeenCalled(); // Mailer.send metodu çağrılmamalı
  });

  test('sendPasswordResetEmail gönderim hatası durumunda bir hata fırlatmalı', async () => {
    const error = new Error('Mail sunucusuna bağlanılamadı.');
    mailerSendSpy.mockRejectedValue(error); // Hata durumunu simüle et

    await expect(EmailService.sendPasswordResetEmail('test@example.com', 'some-token-123')).rejects.toThrow(error);
    expect(mailerSendSpy).toHaveBeenCalledTimes(1);
    expect(mailerSendSpy).toHaveBeenCalledWith('test@example.com', 'Şifre Sıfırlama', expect.stringContaining('/reset?token=some-token-123'));
  });
});
  

Bu örnekte, Mailer.send metodu Promise döndürdüğü için, Jest.spyOn ile .mockResolvedValue() ve .mockRejectedValue() kullanarak asenkron başarı ve hata senaryolarını test ettik. Bu, testlerin gerçek bir e-posta sunucusuna bağlanmasını engeller ve testlerinizi çok daha hızlı ve güvenilir hale getirir. Asenkron testlerde await anahtar kelimesini ve .rejects.toThrow() gibi Jest eşleştiricilerini doğru kullanmak da önemlidir.

Jest.spyOn İleri Düzey Kullanım Teknikleri ve En İyi Pratikler

Jest.spyOn'ın temel kullanımı oldukça anlaşılır olsa da, bu aracı daha etkili ve verimli bir şekilde kullanmak için bazı ileri düzey teknikler ve en iyi pratikler mevcuttur. Özellikle karmaşık test senaryolarında, bu teknikler testlerinizin okunabilirliğini, bakımını ve güvenilirliğini artırabilir.

Jest.spyOn vs. jest.fn() vs. jest.mock(): Ne Zaman Hangisini Kullanmalı?

Jest'in mocking ekosisteminde üç ana araç vardır: jest.spyOn(), jest.fn() ve jest.mock(). Her birinin kendine özgü kullanım durumları vardır ve doğru aracı seçmek, testlerinizin kalitesi için kritiktir.

  • jest.fn()

    : Sıfırdan tamamen yeni bir sahte fonksiyon oluşturur. Genellikle bir callback'i test etmek, bir objeye yeni bir metot eklemek veya bir fonksiyonu parametre olarak geçirmek istediğinizde kullanılır. Orijinal bir implementasyonu yoktur.

    
            const mockCallback = jest.fn();
            someFunction(mockCallback);
            expect(mockCallback).toHaveBeenCalled();
          

  • jest.spyOn(object, methodName)

    : Var olan bir objenin bir metodunu casuslar. Metodun orijinal implementasyonunu varsayılan olarak korur, ancak çağrıları izlemenize ve metodun davranışını geçici olarak değiştirmenize olanak tanır. Genellikle, bir modül içindeki başka bir metodun veya bir dış bağımlılığın (örneğin

    console.log

    ,

    axios.get

    ) bir metodunun çağrıldığından emin olmak istediğinizde veya o metodun dönüş değerini kontrol etmek istediğinizde tercih edilir. Casuslanan metodun orijinal haline geri döndürülmesi (.mockRestore()) önemlidir.

    
            const consoleSpy = jest.spyOn(console, 'log');
            myFunction(); // myFunction içinde console.log çağrılıyor
            expect(consoleSpy).toHaveBeenCalledWith('Bir şeyler oldu.');
            consoleSpy.mockRestore();
          

  • jest.mock('modulePath')

    : Tüm bir modülü sahtelemenizi sağlar. Genellikle, test ettiğiniz modülün büyük ve karmaşık bir bağımlılığı (örneğin bir veritabanı kütüphanesi veya üçüncü taraf bir API istemcisi) olduğunda kullanılır. Bu, modülün tüm dışa aktarımlarını (export) sahte versiyonlarla değiştirir.

    
            // __mocks__/lodash.js (veya test dosyasında)
            jest.mock('lodash', () => ({
              ...jest.requireActual('lodash'), // Gerçek implementasyondan bazılarını koru
              chunk: jest.fn(() => ['mockedChunk'])
            }));
    
            const _ = require('lodash');
            _.chunk([1,2,3], 2);
            expect(_.chunk).toHaveBeenCalled();
          

Özetle: Bir fonksiyonu tamamen sahtelemeniz gerekiyorsa

jest.fn()

kullanın. Bir objenin mevcut bir metodunu izlemek ve/veya geçici olarak değiştirmek istiyorsanız

jest.spyOn()

kullanın. Tüm bir modülün bağımlılıklarını izole etmek istiyorsanız

jest.mock()

kullanın.

Zincirleme Spy'lar ve Karmaşık Senaryolar

Bazen bir metot, diğer metotları çağırabilir veya birden fazla ardışık işlemi tetikleyebilir. Jest.spyOn'ı zincirleme kullanarak bu karmaşık akışları test edebilirsiniz. Örneğin, bir metodun önce bir doğrulama metodunu, sonra bir kaydetme metodunu çağırdığından emin olmak isteyebilirsiniz. Bu tür senaryolarda, birden fazla casus oluşturabilir ve çağrı sırasını kontrol edebilirsiniz.


// dataProcessor.js
const validator = {
  isValid: (data) => data && data.value > 0
};

const dataStore = {
  save: async (data) => {
    if (!data.id) data.id = Math.random().toString(36).substr(2, 9);
    console.log(Veri kaydedildi: ${data.id});
    return Promise.resolve(data);
  }
};

class DataProcessor {
  async process(data) {
    if (!validator.isValid(data)) {
      throw new Error('Geçersiz veri.');
    }
    const result = await dataStore.save(data);
    return result;
  }
}

module.exports = new DataProcessor();
  

Test dosyasında zincirleme spy'lar:


// dataProcessor.test.js
const DataProcessor = require('./dataProcessor');
const validator = require('./dataProcessor').validator;
const dataStore = require('./dataProcessor').dataStore;

describe('DataProcessor', () => {
  let isValidSpy;
  let saveSpy;

  beforeEach(() => {
    isValidSpy = jest.spyOn(validator, 'isValid');
    saveSpy = jest.spyOn(dataStore, 'save');
  });

  afterEach(() => {
    isValidSpy.mockRestore();
    saveSpy.mockRestore();
  });

  test('process metodu geçerli veriyi başarıyla işlemeli', async () => {
    const testData = { value: 10 };
    const savedData = { id: 'abc', value: 10 };

    isValidSpy.mockReturnValue(true); // Doğrulamanın başarılı olduğunu varsay
    saveSpy.mockResolvedValue(savedData); // Kaydetmenin başarılı olduğunu varsay

    const result = await DataProcessor.process(testData);

    expect(isValidSpy).toHaveBeenCalledTimes(1);
    expect(isValidSpy).toHaveBeenCalledWith(testData);
    expect(saveSpy).toHaveBeenCalledTimes(1);
    expect(saveSpy).toHaveBeenCalledWith(testData);
    expect(result).toEqual(savedData);

    // Call order (çağrı sırası) da Jest'te kontrol edilebilir,
    // ancak bunun için daha özel bir yaklaşım gerekebilir veya
    // metotların çağrılma sırasını dolaylı yollardan test edilebilir.
    // Örneğin, mock fonksiyonlarının çağrılma sırasını dizi olarak tutarak karşılaştırabilirsiniz.
  });

  test('process metodu geçersiz veri için hata fırlatmalı', async () => {
    const invalidData = { value: -5 };
    isValidSpy.mockReturnValue(false); // Doğrulamanın başarısız olduğunu varsay

    await expect(DataProcessor.process(invalidData)).rejects.toThrow('Geçersiz veri.');

    expect(isValidSpy).toHaveBeenCalledTimes(1);
    expect(isValidSpy).toHaveBeenCalledWith(invalidData);
    expect(saveSpy).not.toHaveBeenCalled(); // Geçersiz veri olduğunda save metodu çağrılmamalı
  });
});
  

Bu senaryoda, DataProcessor'ın iç bağımlılıkları olan

validator.isValid

ve

dataStore.save

metotlarını casusladık. Bu, DataProcessor.process metodunun hem geçerli hem de geçersiz veri akışlarını izole bir şekilde test etmemizi sağladı. Özellikle

saveSpy.not.toHaveBeenCalled()

gibi asercyonlar, negatif test senaryolarında beklenen yan etkilerin gerçekten oluşmadığını doğrulamak için çok değerlidir.

Hafıza Yönetimi ve Temizleme: mockRestore, clearAllMocks, resetAllMocks

Testler arasında "sızıntı" olmaması için spy'larınızı her testten sonra temizlemek hayati önem taşır. Jest, bu konuda size yardımcı olacak birkaç metot sunar:

  • spy.mockRestore()

    : Belirli bir casuslanan metodun orijinal implementasyonuna geri dönmesini sağlar. Bu, en hedefli temizleme yöntemidir. Genellikle

    afterEach

    içinde tek tek spy'lar için kullanılır.

  • jest.clearAllMocks()

    : Tüm mock fonksiyonlarının (hem

    jest.fn()

    hem de

    jest.spyOn()

    tarafından oluşturulanlar) çağrı geçmişlerini (.mock.calls, .mock.results vb.) sıfırlar, ancak orijinal implementasyonları geri getirmez veya mock edilmiş değerleri kaldırmaz. Fonksiyonlar hala mock edilmiş durumda kalır.

  • jest.resetAllMocks()

    : Tüm mock fonksiyonlarının çağrı geçmişlerini sıfırlar VE mock implementasyonlarını da sıfırlar (yani

    .mockReturnValue()

    ,

    .mockImplementation()

    ile belirlenen değerleri kaldırır). Ancak

    jest.spyOn()

    ile casuslanan metotların orijinal implementasyonlarını geri getirmez.

  • jest.restoreAllMocks()

    : Tüm mock fonksiyonlarının çağrı geçmişlerini sıfırlar, mock implementasyonlarını kaldırır VE

    jest.spyOn()

    ile casuslanan tüm metotları orijinal implementasyonlarına geri döndürür. Bu, genellikle test süitinizin başında veya sonunda, tüm mock'ları tam olarak temizlemek için en kapsamlı ve güvenli seçenektir.

En iyi pratik olarak, her test bloğu içinde

jest.spyOn()

ile oluşturduğunuz spy'ları

afterEach(() => spy.mockRestore())

ile geri yüklemeniz önerilir. Eğer çok sayıda spy veya mock fonksiyonunuz varsa,

jest.restoreAllMocks()

metodunu

afterEach

bloğunda kullanmak, tüm casusları otomatik olarak orijinal hallerine döndürmek için pratik bir yoldur. Bu, testlerinizin bağımsızlığını garanti eder ve olası yan etkileri ortadan kaldırır.

Sonuç: Jest.spyOn ile Testlerinizi Güçlendirin

Bu kapsamlı rehber boyunca, Jest.spyOn'ın birim test stratejiniz için neden bu kadar önemli bir araç olduğunu, temelden ileri düzeye kadar nasıl kullanıldığını ve gerçek dünya senaryolarında karşılaşılan karmaşık bağımlılıkların üstesinden nasıl gelindiğini detaylı bir şekilde inceledik. Jest.spyOn, kodunuzdaki belirli metotların davranışlarını izlemenize, bu metotların çağrı geçmişlerini kaydetmenize ve hatta geçici olarak dönüş değerlerini veya implementasyonlarını değiştirmenize olanak tanır. Bu yetenek, özellikle dış bağımlılıkları (API çağrıları, veritabanı etkileşimleri, dosya sistemi işlemleri vb.) olan fonksiyonları test ederken hayati bir rol oynar. Testlerinizin daha hızlı, daha güvenilir ve daha izole olmasını sağlayarak, geliştirme süreçlerinizin verimliliğini artırır.

Özellikle, asenkron işlemlerle çalışırken

.mockResolvedValue()

ve

.mockRejectedValue()

gibi yardımcı metotların sağladığı esneklik, başarı ve hata senaryolarını kolayca simüle etmenizi mümkün kılar. Bu sayede, uygulamanızın farklı koşullar altında nasıl davrandığını güvenle test edebilirsiniz. Ayrıca,

jest.spyOn()

,

jest.fn()

ve

jest.mock()

arasındaki farkları anlamak, her senaryo için doğru aracı seçmenize yardımcı olur ve test kodunuzun daha temiz ve sürdürülebilir olmasını sağlar. Testler arasında istenmeyen yan etkileri önlemek için

.mockRestore()

veya

jest.restoreAllMocks()

gibi temizlik metotlarını

afterEach

bloklarında kullanmak, her zaman iyi bir uygulama olarak karşımıza çıkar.

Sonuç olarak, Jest.spyOn, modern JavaScript geliştiricileri için vazgeçilmez bir birim testi aracıdır. Kodunuzun modülerliğini artırır, hata ayıklama süreçlerini basitleştirir ve uygulamanızın güvenilirliğini önemli ölçüde yükseltir. Bu kılavuzda edindiğiniz bilgilerle, Jest.spyOn'ı kendi projelerinizde etkin bir şekilde kullanarak daha sağlam ve bakımı kolay bir kod tabanı oluşturmaya başlayabilirsiniz. Testlerinizi güçlendirin, kod kalitenizi artırın ve geliştirme süreçlerinizde daha fazla güvenle ilerleyin. Unutmayın, iyi yazılmış testler, sadece hataları yakalamakla kalmaz, aynı zamanda kodunuzun bir nevi canlı dokümantasyonu olarak da hizmet eder.

Sıkça Sorulan Sorular

  • Jest.spyOn nedir ve neden kullanmalıyım?

    Jest.spyOn, bir objenin mevcut bir metodunu casuslamak (çağrılarını izlemek) ve/veya geçici olarak davranışını değiştirmek için kullanılan bir Jest aracıdır. Dış bağımlılıkları olan fonksiyonları izole bir şekilde test etmek, testleri hızlandırmak ve kararsız testleri önlemek için kullanmalısınız. Metodun orijinal implementasyonunu korurken, çağrı geçmişini (hangi argümanlarla çağrıldı, kaç kez çağrıldı vb.) kaydetmenize olanak tanır.

  • Jest.spyOn ile jest.fn() arasındaki temel fark nedir?

    Jest.fn() sıfırdan tamamen yeni bir sahte (mock) fonksiyon oluştururken, jest.spyOn var olan bir objenin mevcut bir metodunu casuslar. jest.spyOn ile casusladığınız metodun orijinal implementasyonu varsayılan olarak çalışmaya devam ederken, jest.fn() bir "boş" fonksiyonla başlar ve ona özel bir implementasyon atamanız gerekir. spyOn, özellikle bir objenin içindeki bir metodun çağrılıp çağrılmadığını kontrol etmek veya davranışını geçici olarak taklit etmek istediğinizde daha uygundur.

  • jest.spyOn ile mockResolvedValue ve mockRejectedValue ne zaman kullanılır?

    Bu metotlar, asenkron metotlar (Promise döndüren fonksiyonlar) ile çalışırken kullanılır. mockResolvedValue, casuslanan metodun sanki başarılı bir Promise çözümlenmiş gibi belirli bir değer döndürmesini sağlar. mockRejectedValue ise, metodun sanki bir Promise hatasıyla reddedilmiş gibi belirli bir hata fırlatmasını simüle eder. Özellikle API çağrıları veya veritabanı işlemleri gibi asenkron bağımlılıkları test ederken vazgeçilmezdirler.

  • Jest.spyOn kullanırken .mockRestore() neden önemlidir?

    .mockRestore() metodu, casusladığınız metodun orijinal implementasyonuna geri dönmesini sağlar. Eğer bunu yapmazsanız, bir testte mockladığınız bir metot, test süitinizdeki diğer testleri de etkileyebilir. Bu durum, "test sızıntısı" olarak adlandırılır ve testlerin kararsız ve güvenilmez olmasına neden olur. Bu nedenle, her test bloğunun sonunda veya afterEach hook'unda .mockRestore() kullanarak casusları temizlemek iyi bir pratik olarak kabul edilir.

  • Jest.spyOn ile private metotları test edebilir miyim?

    JavaScript'te gerçek "private" metot kavramı doğrudan yoktur (class private fields hariç). Ancak genellikle bir objenin dışa aktarılmayan (internal) metotlarını test etme ihtiyacı doğabilir. Jest.spyOn, sadece objenin erişilebilir (genellikle public) metotları üzerinde çalışabilir. Eğer bir metot gerçekten dışarıdan erişilemezse, doğrudan spyOn ile casuslayamazsınız. Bu durumda, ya test amacıyla o metodu dışa aktarmak (ki bu encapsulation prensiplerine aykırı olabilir) ya da o metodu çağıran public metot üzerinden dolaylı yollarla test etmek daha uygun olabilir. Genellikle, private metotların kendi başına birim testine tabi tutulması yerine, bu metotları kullanan public metotların davranışları üzerinden test edilmesi önerilir.

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.