RSpec ile Test Doubles: Mock ve Stub Kavramları

Ahmet Kaptan
Passgage Tech
Published in
8 min readApr 8, 2024

Herkese selamlar 👐. Bugün daha önce Ruby Türkiye akşam oturmasında sunumunu yapmış olduğum RSpec ile Test Doubles: Mock ve Stub Kavramlarına Giriş başlığını makele olarak da paylaşmak istedim. Makaleme başlamadan sunumumu aşağıdan izleyebilirsiniz :

Hadi o zaman başlayalım 🙋‍♂.

Makalemde Test Doubles (mock ve stub gibi) kavramsal tanıtımını yaparak, neden test süreçlerinde bu tür araçlara ihtiyaç duyulduğuna dair temel bir anlayış sunup, Test Doubles nasıl kullanılacağına dair temel kavramları kodlar ile açıklayamaya çalışacağım.

Ama öncelikle ufak bir neden teste ihtiyaç duyarız sorusuna kendi bakış açımla ifade etmek istiyorum.

Bundan bahsetmek istiyorum çünkü özellikle start-up tecrübesi olan biriyim. Bu nedenle test konusu bu tür yapılara en çok baş ağrıtan konulardan biri diye düşünüyorum.

Çünkü bir şeyleri çok hızlı deliver etmemiz gerekiyor ve test’i bazen önemsiz veya sonra yapılabilir olarak görüyoruz ama aslında her sonra dediğimizde çalışmayan bir kodun veya istenen de farklı çalışan bir kodun maliyeti testi ertelememizden daha fazla oluyor.

Ufak bir giriş…

Daha önce test hakkında ufak bir sunum yapmıştım şirket içinde, bu sunumu yaparken beni derinden etkilemiş 2 insanın test ile alakalı yorumlarını sizle paylaşmak istiyorum:

“Tests are stories we tell the next generation of programmers on a project.” — Roy Osherove

Testler, bir projedeki gelecek nesil programcılara anlattığımız hikayelerdir.

Bence bu çok değerli bir yorum çünkü yeni bir projeye adımımı attığımda ilk baktığım yer test dosyaları oluyor. Bir projeye eğer gerçekten hakim olmak istiyorsam “test yazılıyorsa” ilk olarak testlere bakarım. Ara ara Ruby on Rails ile geliştirilmiş open source projelere bakıyorum. Hatta daha önce serializer testleri hakkında bir bilgim yokken https://github.com/discourse/discourse reposunu incelerken bu konu hakkında bilgi sahibi oldum ve özellikle API geliştirmeleri yaparken sayısız defa beni kurtardı.

Örnek vermek gerekirse şuan bulunduğum şikette Public API’miz var ve birçok şirket bununla bağlantı kuruyor. Bu noktada serializer testlerinin olması ve yazılması güven veriyor bana.

“Code without tests is bad code. It doesn’t matter how well written it is; it doesn’t matter how pretty or object-oriented or well-encapsulated it is. With tests, we can change the behavior of our code quickly and verifiably. Without them, we really don’t know if our code is getting better or worse.” — Michael Feathers

“Testleri olmayan kod kötü koddur. Ne kadar iyi yazılmış olduğu önemli değildir; ne kadar güzel, nesne yönelimli veya iyi kapsüllenmiş olduğu önemli değildir. Testlerle, kodumuzun davranışını hızlı ve doğrulanabilir bir şekilde değiştirebiliriz. Testlerisz, kodumuzun daha iyi mi yoksa daha kötü mü olduğunu gerçekten bilmiyoruz.”

Evet burada sert bir ifade var fakat aslında bence vurgulamak istediği nokta test yazın abi 😄

Testleri sadece maliyetten kurtulmak gibi düşünmemek gerekiyor ayrıca gerçekten iyi yazılmış testler geliştiriciye güven verir. Çünkü projenin bir çok noktasına dokunuyoruz.

Tamamdır buradan sonra sakin bir şekilde ana başlığımıza dönebiliriz. Test’e neden ihtiyacımız var konusuna çok derinlemesine girmeden benim teste bakış açımı sizlere biraz olsun aktarmak istedim.

RSpec Nedir?

RSpec, Ruby programlama dilinde kullanılan bir test framework’tür ve yazılım projelerinde testlerin yazılmasını, çalıştırılmasını ve analiz edilmesini kolaylaştıran bir araçtır.

Test Doubles Nedir?

Test Doubles, bir testin diğer bileşenlerle etkileşimde bulunmak zorunda olduğunda, gerçek uygulama nesnelerinin yerine geçen, kontrol edilebilir ve öngörülebilir nesnelerdir.

“A test double is an object that can stand in for a real object in a test, similar to how a stunt double stands in for an actor in a movie.”

Film endüstrisi, başrol oyuncusunun gerçekleştirmesi potansiyel olarak riskli veya tehlikeli olan bir şeyi filme almak istediğinde, sahnedeki oyuncunun yerini alması için bir “dublör” kiralar. Dublör, sahnenin özel gereksinimlerini karşılayabilecek yüksek eğitimli bir kişidir. Rol yapamayabilirler ama yüksekten nasıl düşeceklerini, bir arabaya nasıl çarpacaklarını ya da sahnenin gerektirdiği her şeyi bilirler. Dublörün oyuncuya ne kadar benzemesi gerektiği sahnenin niteliğine bağlıdır. Genellikle işler, oyuncuya boy olarak belli belirsiz benzeyen birinin onun yerini alabileceği şekilde düzenlenebilir.

Buna basit bir örnek payment gateway’dir. Bir kodun gateway ile etkileşimini test ettiğimizde, elbette test kodumuzun gerçekten Stripe API’sine bağlanmasını ve insanların kredi kartlarından ücret almasını istemeyiz. Bu noktada başröl oyuncumuz Stripe API’dir ve test ettiğimiz kod ise bizim belirlediğimiz senaryoda kullandığımız dublör yani test doubles’dır. Kısaca Stripe API’e Tom Cruise diyebiliriz ama Tom Cruise bildiğim kadarı ile dublör kullanmıyor 😅.

Mock ve stub kavramları arasındaki farka geçmeden önce test doubles ile alakalı ufak bir örnek yaparak buraya kadar olan tanımları kod üzerinde örneklendirelim ne dersiniz?

Paylaştığım örneği Conner Jensen nın youtube videosundan aldım çünkü çok basit ama test doubles’ı efektif kullanması gerçekten sade ve anlaşılır olduğunu düşünüyorum.

Aşağıdaki örnekte görüldüğü gibi bir Car ve Pump sınıflarım mevcut. Amaç Pump sınıfında dispense_fuel methodunu çağırdığımda aracımın belirtilen seviyede yakıtının dolması. Hadi bunun testini yazalım.

#main.rb
class Car
attr_accessor :fuel_level
def initialize(fuel)
@fuel_level = fuel
end
def fill_up(pump)
@fuel_level = pump.dispense_fuel
end
end
class Pump
def dispense_fuel
100
end
end

describe: describe bloğu, bir test dosyası veya bir test grubu için bir bağlamı tanımlar. Bu blok içinde test edilecek nesnelerin veya davranışların tanımları yapılır. Genellikle, test edilen sınıf veya metodun adı describe bloğu içinde verilir.

it: it bloğu, bir test senaryosunu tanımlar. Bu blok içinde, test edilecek belirli bir davranış veya özelliğin beklentisi açıklanır.

Test’i koştuğumuzda yeşil ışıkların yandığını görebiliriz.

Ama burada bir problem var. Bizim test etmek istediğimiz Car sınıfı ama Pump sınıfı işe bağımlılıklarımız mevcut. Mesala Pump#dispense_fuel dönen değeri 100 değilde 75 yaparsak?

describe Car do
describe '#fill_up' do
it 'the should have maximum fuel' do
car = Car.new(50)
pump = Pump.new
car.fill_up(pump)
expect(car.fuel_level).to eq(100)
end
end
end

Evet testimiz hata verecektir. Amacım Car#fill_up’ı test etmek ama Pump sınıfındaki değişiklik benim testimi etkilemekte. Bu nedenle bu bağımlılıktan kurtulamalıyız. Nasıl mı?

double: Genel bir double oluşturur ve herhangi bir nesneyi sarmaz. Bu nedenle, üzerinde çağrılacak olan metotları önceden tanımlamanıza gerek yoktur.

describe Car do
describe '#fill_up' do
it 'the should have maximum fuel' do
car = Car.new(50)
pump = double('Pump', dispense_fuel: 100)
car.fill_up(pump)
expect(car.fuel_level).to eq(100)
end
end
end

Double kullanarak bir Pump sınıfının örneğini oluşturduk ve dispense_fuel methodundan ne dönmesi gerektiğini ben senaryolaştırıyorum. Böylece testim bağımlılıktan kurtulmuş oluyor ve her testi koştuğumda Pump sınıfına gitmeme gerek kalmıyor.

Burada Car sınıfımın testindeki bağımlılığı azaltmış olduk ama Pump sınıfında olurda bir ekip arkadaşımız dispense_fuel methodunun isminini değiştirirse?

Testimiz hata vermeyecektir 😨 . Unit testler genellikle kodun belirli davranışlarını test etmek için yazılır ve bu davranışlar değişikliklere karşı dayanıklı olmalıdır. Dolayısıyla, Car sınıfı testleri, Pump sınıfının dispense_fuel metodunun isminin değişmesini kapsamalıdır. Buradaki testmizde method ile ilgili değişiklik testimizde yakalamak için instance_double kullanmalıyız. Böylece bu tarzda yaşadığımız case’leri daha test aşamasında düzeltebiliriz.

instance_double: Bir nesneyi sarmalayan ve bu nesnenin gerçek bir örneği gibi davranan bir double oluşturur. instance_double kullanırken, double üzerinde çağrılacak olan metotları önceden tanımlamanız gerekir. Bu, sarmalanan nesnenin örneğinin sahip olduğu metotları kontrol etmenizi sağlar ve yanlışlıkla olmayan bir metodu çağırmayı engeller.

Testimizin son hali:

describe Car do
describe '#fill_up' do
it 'the should have maximum fuel' do
car = Car.new(50)
pump = instance_double('Pump', dispense_fuel: 100)
car.fill_up(pump)
expect(car.fuel_level).to eq(100)
end
end
end

Ve sonuç:

MOCK ve STUB nedir ? Farkları Nedir?

Mocking, test odaklı geliştirmede (TDD) bir test yazmak için sahte bağımlı nesneler veya yöntemler kullanmayı içeren bir tekniktir.

Test Driven Development (TDD), yazılım geliştirme sürecinde bir kod parçası yazmadan önce testlerin yazılmasını öngören bir yazılım geliştirme yaklaşımıdır. Bu yaklaşım, yazılımın daha sağlam, hata oranı düşük ve bakımı kolay olmasını hedefler.

Mock

Tanım: Gerçek bir uygulama yerine geçen, ancak genellikle daha basitleştirilmiş bir şekilde çalışan bir test double’dır. Bu, özellikle test süreçlerini hızlandırmak veya dış bağımlılıkları yönetmek için kullanılır.

Kullanım: Bir sınıfın belirli bir metodu çağrıldığında, bu çağrının beklenen parametrelerle gerçekleşip gerçekleşmediğini kontrol etmek için kullanılır.

Only mocks insist upon behavior verification — Martin Fowler

“Behavior verification” (davranış doğrulaması), bir test double (test dublörü) üzerinde belirli bir davranışın çağrılıp çağrılmadığını ve belirli parametrelerle çağrıldığını kontrol etmeyi ifade eder. Bir testin belirli bir sınıf veya nesneyle etkileşimde bulunurken beklenen davranışları doğrulamasını sağlar.

Stubs

Tanım: Bir metodu çağrıldığında belirli bir değeri döndürmek veya belirli bir davranışı taklit etmek için kullanılır.

Kullanım: Veritabanı çağrılarını simüle etmek, harici servis çağrılarından gerçek sonuçlar almak gibi durumlarda kullanılır.

The other doubles can, and usually do, use state verification. The main job of a Mock Object is to ensure that the right methods get called on it. — Martin Fowler

“State verification” (durum doğrulaması), bir test double (test dublörü) veya gerçek nesne üzerinde bir metod çağrıldıktan sonra nesnenin durumunu kontrol etmeyi ifade eder. Bir metodun çağrıldıktan sonra nesnenin iç durumunun beklenen şekilde değişip değişmediğini doğrulamak için kullanılır.

Test Doubles, yazılım testlerini daha etkili, izole edilebilir ve güvenilir hale getirmek için kullanılır. İşte temel nedenleri:

  • Yazılım projeleri genellikle birbirine bağlı çok sayıda bileşeni içerir. Bileşenin test edilirken diğer bağımlılıklardan izole edilmesini sağlar. Testin sadece belirli bir bileşenin davranışını değil, aynı zamanda bağımlılıklarının nasıl etkileşimde bulunduğunu da kontrol etmesine olanak tanır.
  • Modüler testlerin yapılabilmesini sağlar. Yani, bir bileşenin izole edilmiş bir versiyonu (mock, stub veya spy) kullanılarak, bu bileşenin davranışının belirli senaryolar altında nasıl olduğunu test edebiliriz.
  • Test ortamında belirli durumları simüle etmek için kullanılabilir. Örneğin, bir servis çağrısının başarılı veya başarısız olma senaryolarını kontrol etmek için bir mock kullanabilir veya bir metot çağrısının belirli bir değeri dönmesini sağlamak için bir stub kullanabiliriz.
  • Testlerde kullanılacak veriyi üretmek veya manipüle etmek için kullanılabilir. Örneğin, bir test senaryosunda belirli bir veri seti üzerinde çalışmak için bir mock nesnesi oluşturabiliriz.

Kaynaklar

--

--