Python'da Alay Etmeye Giriş

Yayınlanan: 2022-03-11

Sabrınızı Test Etmeden Python'da Birim Testleri Nasıl Çalıştırılır

Yazdığımız yazılım, çoğu zaman “kirli” hizmetler olarak adlandırdığımız şeylerle doğrudan etkileşime girer. Layman'ın terimleriyle: uygulamamız için çok önemli olan, ancak etkileşimleri amaçlanan ancak istenmeyen yan etkilere sahip olan, yani otonom bir test çalıştırması bağlamında istenmeyen hizmetler.

Örneğin: belki bir sosyal uygulama yazıyoruz ve yeni 'Facebook'ta Gönder özelliğimizi' test etmek istiyoruz, ancak test takımımızı her çalıştırdığımızda gerçekten Facebook'ta gönderi paylaşmak istemiyoruz.

Python birim test kitaplığı, unittest adlı bir alt unittest.mock içerir - veya bunu bir bağımlılık olarak bildirirseniz, yalnızca mock - bu istenmeyen yan etkilerle alay etmek ve saplamak için son derece güçlü ve kullanışlı araçlar sağlar.

piton unittest kitaplığında alay ve birim testleri

Not: mock , Python 3.3'ten itibaren standart kitaplığa yeni dahil edilmiştir; önceki dağıtımlar, PyPI aracılığıyla indirilebilen Mock kitaplığını kullanmak zorunda kalacak.

Sistem Çağrıları ve Python Alaycılığı

Size ve makalenin geri kalanında birlikte çalışacağımız başka bir örnek vermek için, sistem çağrılarını düşünün. Bunların alay etmek için başlıca adaylar olduğunu görmek zor değil: İster bir CD sürücüsünü çıkarmak için bir komut dosyası yazıyor, ister eski önbellek dosyalarını /tmp öğesinden kaldıran bir web sunucusu veya bir TCP bağlantı noktasına bağlanan bir soket sunucusu yazıyor olun, bunlar birim testleri bağlamında tüm özellik istenmeyen yan etkileri çağırır.

Bir geliştirici olarak, her test çalıştırıldığında CD tepsinizin açık olduğunu deneyimlemek yerine, kitaplığınızın bir CD'yi çıkarmak için sistem işlevini başarıyla çağırmasına daha çok önem verirsiniz.

Bir geliştirici olarak, her test çalıştırıldığında CD tepsinizin açılmasını gerçekten deneyimlemek yerine, kitaplığınızın bir CD'yi çıkarmak için sistem işlevini (doğru argümanlarla vb.) başarıyla çağırmasını daha çok önemsiyorsunuz. (Daha da kötüsü, birden çok test, tek bir birim test çalıştırması sırasında çıkarma koduna başvurduğundan, birden çok kez!)

Benzer şekilde, birim testlerinizi verimli ve performanslı tutmak, dosya sistemi ve ağ erişimi gibi otomatikleştirilmiş test çalıştırmalarından olabildiğince fazla "yavaş kod" tutmak anlamına gelir.

İlk örneğimiz için, standart bir Python test senaryosunu orijinal formdan mock kullanarak birine dönüştüreceğiz. Örneklerle bir test senaryosu yazmanın, testlerimizi nasıl daha akıllı, daha hızlı ve yazılımın nasıl çalıştığı hakkında daha fazla bilgi verebilecek hale getireceğini göstereceğiz.

Basit Bir Silme İşlevi

Hepimizin zaman zaman dosya sistemimizden dosyaları silmemiz gerekiyor, bu yüzden Python'da betiklerimizin bunu yapmasını biraz daha kolaylaştıracak bir fonksiyon yazalım.

 #!/usr/bin/env python # -*- coding: utf-8 -*- import os def rm(filename): os.remove(filename)

Açıkçası, bu noktada rm yöntemimiz, temeldeki os.remove yönteminden çok daha fazlasını sağlamaz, ancak kod tabanımız gelişecek ve buraya daha fazla işlevsellik eklememize izin verecektir.

Geleneksel bir test senaryosu yazalım, yani alaysız:

 #!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import rm import os.path import tempfile import unittest class RmTestCase(unittest.TestCase): tmpfilepath = os.path.join(tempfile.gettempdir(), "tmp-testfile") def setUp(self): with open(self.tmpfilepath, "wb") as f: f.write("Delete me!") def test_rm(self): # remove the file rm(self.tmpfilepath) # test that it was actually removed self.assertFalse(os.path.isfile(self.tmpfilepath), "Failed to remove the file.")

Test durumumuz oldukça basittir, ancak her çalıştırıldığında geçici bir dosya oluşturulur ve ardından silinir. Ek olarak, rm yöntemimizin argümanı os.remove çağrısına doğru şekilde aktarıp aktarmadığını test etmenin hiçbir yolu yok. Yukarıdaki teste dayanarak bunun işe yaradığını varsayabiliriz , ancak arzulanan çok şey kaldı.

Python Mocks ile Yeniden Düzenleme

Test durumumuzu mock kullanarak yeniden düzenleyelim:

 #!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import rm import mock import unittest class RmTestCase(unittest.TestCase): @mock.patch('mymodule.os') def test_rm(self, mock_os): rm("any path") # test that rm called os.remove with the right parameters mock_os.remove.assert_called_with("any path")

Bu yeniden düzenleyicilerle, testin çalışma şeklini temelden değiştirdik. Şimdi, bir diğerinin işlevselliğini doğrulamak için kullanabileceğimiz bir içeriden öğrenen nesnemiz var.

Potansiyel Python Alaycı Tuzakları

Dikkat çekmesi gereken ilk şeylerden biri, mymodule.os konumunda bulunan bir nesneyle alay etmek için mock.patch yöntemi dekoratörünü kullanmamız ve bu alayı test senaryosu yöntemimize enjekte etmemizdir. mymodule.os os alay etmek daha mantıklı olmaz mıydı?

Python, modülleri içe aktarma ve yönetme söz konusu olduğunda biraz sinsi bir yılandır. Çalışma zamanında, mymodule modülünün, modüldeki kendi yerel kapsamına aktarılan kendi os sistemi vardır. Böylece, os ile alay edersek, alayın etkilerini mymodule modülünde göremeyiz.

Sürekli tekrar etmeniz gereken mantra şudur:

Bir eşyayı nereden geldiğiyle değil, kullanıldığı yerde alay edin.

myproject.app.MyElaborateClass için tempfile modülüyle alay etmeniz gerekiyorsa, her modül kendi içe myproject.app.tempfile için muhtemelen alayı myproject.app.tempfile öğesine uygulamanız gerekir.

Bu tuzak ortadan kalktıktan sonra, alay etmeye devam edelim.

'rm' için Doğrulama Ekleme

Daha önce tanımlanan rm yöntemi oldukça basitleştirilmiştir. Sadece körü körüne kaldırmaya çalışmadan önce bir yolun var olduğunu ve bir dosya olduğunu doğrulamasını istiyoruz. Biraz daha akıllı olmak için rm yeniden düzenleyelim:

 #!/usr/bin/env python # -*- coding: utf-8 -*- import os import os.path def rm(filename): if os.path.isfile(filename): os.remove(filename)

Harika. Şimdi, kapsamı geniş tutmak için test senaryomuzu ayarlayalım.

 #!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import rm import mock import unittest class RmTestCase(unittest.TestCase): @mock.patch('mymodule.os.path') @mock.patch('mymodule.os') def test_rm(self, mock_os, mock_path): # set up the mock mock_path.isfile.return_value = False rm("any path") # test that the remove call was NOT called. self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.") # make the file 'exist' mock_path.isfile.return_value = True rm("any path") mock_os.remove.assert_called_with("any path")

Test paradigmamız tamamen değişti. Artık herhangi bir yan etki olmadan yöntemlerin dahili işlevselliğini doğrulayabilir ve doğrulayabiliriz.

Sahte Yama ile Hizmet Olarak Dosya Kaldırma

Şimdiye kadar, yalnızca işlevler için taklitler sağlamaya çalıştık, ancak nesneler veya alaycılığın parametre göndermek için gerekli olduğu durumlardaki yöntemler için değil. Önce nesne yöntemlerini ele alalım.

rm yönteminin bir hizmet sınıfına yeniden düzenlenmesiyle başlayacağız. Bu kadar basit bir işlevi bir nesneye yerleştirmek için gerçekten haklı bir ihtiyaç yoktur, ancak en azından mock anahtar kavramları göstermemize yardımcı olacaktır. Yeniden düzenleyelim:

 #!/usr/bin/env python # -*- coding: utf-8 -*- import os import os.path class RemovalService(object): """A service for removing objects from the filesystem.""" def rm(filename): if os.path.isfile(filename): os.remove(filename)

Test durumumuzda pek bir şeyin değişmediğini fark edeceksiniz:

 #!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import RemovalService import mock import unittest class RemovalServiceTestCase(unittest.TestCase): @mock.patch('mymodule.os.path') @mock.patch('mymodule.os') def test_rm(self, mock_os, mock_path): # instantiate our service reference = RemovalService() # set up the mock mock_path.isfile.return_value = False reference.rm("any path") # test that the remove call was NOT called. self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.") # make the file 'exist' mock_path.isfile.return_value = True reference.rm("any path") mock_os.remove.assert_called_with("any path")

Harika, artık RemovalService planlandığı gibi çalıştığını biliyoruz. Bağımlılık olarak bildiren başka bir hizmet oluşturalım:

 #!/usr/bin/env python # -*- coding: utf-8 -*- import os import os.path class RemovalService(object): """A service for removing objects from the filesystem.""" def rm(self, filename): if os.path.isfile(filename): os.remove(filename) class UploadService(object): def __init__(self, removal_service): self.removal_service = removal_service def upload_complete(self, filename): self.removal_service.rm(filename)

RemovalService üzerinde zaten test kapsamımız olduğundan, UploadService testlerimizde rm yönteminin dahili işlevselliğini doğrulamayacağız. Bunun yerine, RemovalService.rm UploadService çağırdığını (elbette yan etkiler olmadan) test edeceğiz.

Bu konuda gitmenin iki yolu vardır:

  1. RemovalService.rm yönteminin kendisiyle alay edin.
  2. UploadService yapıcısında alaylı bir örnek sağlayın.

Her iki yöntem de birim testinde genellikle önemli olduğundan, her ikisini de gözden geçireceğiz.

Seçenek 1: Alaycı Örnek Yöntemleri

mock kitaplığın, nesne örneği yöntemleri ve özellikleriyle alay etmek için özel bir yöntem dekoratörü vardır, @mock.patch.object dekoratörü:

 #!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import RemovalService, UploadService import mock import unittest class RemovalServiceTestCase(unittest.TestCase): @mock.patch('mymodule.os.path') @mock.patch('mymodule.os') def test_rm(self, mock_os, mock_path): # instantiate our service reference = RemovalService() # set up the mock mock_path.isfile.return_value = False reference.rm("any path") # test that the remove call was NOT called. self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.") # make the file 'exist' mock_path.isfile.return_value = True reference.rm("any path") mock_os.remove.assert_called_with("any path") class UploadServiceTestCase(unittest.TestCase): @mock.patch.object(RemovalService, 'rm') def test_upload_complete(self, mock_rm): # build our dependencies removal_service = RemovalService() reference = UploadService(removal_service) # call upload_complete, which should, in turn, call `rm`: reference.upload_complete("my uploaded file") # check that it called the rm method of any RemovalService mock_rm.assert_called_with("my uploaded file") # check that it called the rm method of _our_ removal_service removal_service.rm.assert_called_with("my uploaded file")

Harika! UploadService rm yöntemini başarıyla çağırdığını doğruladık. Orada ilginç bir şey fark ettiniz mi? Yama mekanizması, test yöntemimizdeki tüm RemovalService örneklerinin rm yönteminin yerini aldı. Bu, örneklerin kendilerini gerçekten inceleyebileceğimiz anlamına gelir. Daha fazlasını görmek istiyorsanız, yama mekanizmasının nasıl çalıştığına dair iyi bir fikir edinmek için alaycı kodunuzda bir kesme noktası bırakmayı deneyin.

Sahte Yama Tuzağı: Dekoratör Siparişi

Test yöntemlerinizde birden fazla dekoratör kullanırken, sıralama önemlidir ve bu biraz kafa karıştırıcıdır. Temel olarak, dekoratörleri yöntem parametrelerine eşlerken geriye doğru çalışın. Bu örneği düşünün:

 @mock.patch('mymodule.sys') @mock.patch('mymodule.os') @mock.patch('mymodule.os.path') def test_something(self, mock_os_path, mock_os, mock_sys): pass

Parametrelerimizin dekoratörlerin ters sırasına göre nasıl eşleştirildiğine dikkat edin? Bu kısmen Python'un çalışma şeklinden kaynaklanmaktadır. Birden çok yöntem dekoratörüyle, sözde kodda yürütme sırası şöyledir:

 patch_sys(patch_os(patch_os_path(test_something)))

sys giden yama en dıştaki yama olduğundan, en son yürütülecek ve bu da onu gerçek test yöntemi argümanlarında son parametre haline getirecektir. Bunu iyi not edin ve doğru parametrelerin doğru sırada enjekte edildiğinden emin olmak için testlerinizi çalıştırırken bir hata ayıklayıcı kullanın.

2. Seçenek: Sahte Örnekler Oluşturma

Belirli örnek yöntemiyle alay etmek yerine, bunun yerine UploadService alaylı bir örnek sağlayabiliriz. Çok daha kesin olduğu için yukarıdaki 1. seçeneği tercih ediyorum, ancak 2. seçeneğin verimli veya gerekli olabileceği birçok durum var. Testimizi tekrar gözden geçirelim:

 #!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import RemovalService, UploadService import mock import unittest class RemovalServiceTestCase(unittest.TestCase): @mock.patch('mymodule.os.path') @mock.patch('mymodule.os') def test_rm(self, mock_os, mock_path): # instantiate our service reference = RemovalService() # set up the mock mock_path.isfile.return_value = False reference.rm("any path") # test that the remove call was NOT called. self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.") # make the file 'exist' mock_path.isfile.return_value = True reference.rm("any path") mock_os.remove.assert_called_with("any path") class UploadServiceTestCase(unittest.TestCase): def test_upload_complete(self, mock_rm): # build our dependencies mock_removal_service = mock.create_autospec(RemovalService) reference = UploadService(mock_removal_service) # call upload_complete, which should, in turn, call `rm`: reference.upload_complete("my uploaded file") # test that it called the rm method mock_removal_service.rm.assert_called_with("my uploaded file")

Bu örnekte, herhangi bir işlevselliğe yama uygulamamız bile gerekmedi, yalnızca RemovalService sınıfı için bir otomatik belirtim oluşturduk ve ardından işlevselliği doğrulamak için bu örneği UploadService enjekte ettik.

mock.create_autospec yöntemi, sağlanan sınıfa işlevsel olarak eşdeğer bir örnek oluşturur. Pratik olarak bunun anlamı, döndürülen örnekle etkileşime girdiğinde, yasadışı yollarla kullanılması durumunda istisnalar oluşturacağıdır. Daha spesifik olarak, bir yöntem yanlış sayıda argümanla çağrılırsa, bir istisna ortaya çıkar. Refactors gerçekleştiği için bu son derece önemlidir. Bir kitaplık değiştikçe testler bozulur ve bu beklenir. Bir otomatik özellik kullanmadan, temeldeki uygulama bozulsa bile testlerimiz yine de geçecek.

Pitfall: mock.Mock ve mock.MagicMock Sınıfları

mock kitaplık ayrıca dahili işlevlerin çoğunun üzerine inşa edildiği iki önemli sınıf içerir: mock.Mock ve mock.MagicMock . Bir mock.Mock örneği, bir mock.MagicMock örneği veya bir otomatik belirtim kullanma seçeneği verildiğinde, gelecekteki değişiklikler için testlerinizi aklı başında tutmaya yardımcı olduğundan, her zaman bir otomatik belirtim kullanmayı tercih edin. Bunun nedeni, mock.Mock ve mock.MagicMock , temel alınan API'den bağımsız olarak tüm yöntem çağrılarını ve özellik atamalarını kabul etmesidir. Aşağıdaki kullanım durumunu göz önünde bulundurun:

 class Target(object): def apply(value): return value def method(target, value): return target.apply(value)

Bunu şöyle bir mock.Mock örneğiyle test edebiliriz:

 class MethodTestCase(unittest.TestCase): def test_method(self): target = mock.Mock() method(target, "value") target.apply.assert_called_with("value")

Bu mantık mantıklı görünüyor, ancak daha fazla parametre almak için Target.apply yöntemini değiştirelim:

 class Target(object): def apply(value, are_you_sure): if are_you_sure: return value else: return None

Testinizi yeniden çalıştırın ve hala geçtiğini göreceksiniz. Bunun nedeni, gerçek API'nize karşı oluşturulmamış olmasıdır. Bu nedenle @patch ve @patch.object dekoratörleriyle her zaman create_autospec yöntemini ve autospec parametresini kullanmalısınız.

Python Mock Örneği: Bir Facebook API Çağrısını Alay Etmek

Bitirmek için, girişte bahsettiğimiz daha uygulanabilir bir gerçek dünya python sahte örneği yazalım: Facebook'a bir mesaj göndermek. Güzel bir sarmalayıcı sınıfı ve buna karşılık gelen bir test durumu yazacağız.

 import facebook class SimpleFacebook(object): def __init__(self, oauth_token): self.graph = facebook.GraphAPI(oauth_token) def post_message(self, message): """Posts a message to the Facebook wall.""" self.graph.put_object("me", "feed", message=message)

İletiyi gerçekten göndermeden gönderip göndermediğimizi kontrol eden test durumumuz:

 import facebook import simple_facebook import mock import unittest class SimpleFacebookTestCase(unittest.TestCase): @mock.patch.object(facebook.GraphAPI, 'put_object', autospec=True) def test_post_message(self, mock_put_object): sf = simple_facebook.SimpleFacebook("fake oauth token") sf.post_message("Hello World!") # verify mock_put_object.assert_called_with(message="Hello World!")

Şimdiye kadar gördüğümüz gibi, Python'da mock ile daha akıllı testler yazmaya başlamak gerçekten çok basit.

Çözüm

Python'un mock kitaplığı, biraz kafa karıştırıcı olsa da, birim testi için bir oyun değiştiricidir. Birim testinde mock kullanmaya başlamak için yaygın kullanım örnekleri gösterdik ve umarım bu makale Python geliştiricilerinin ilk engelleri aşmasına ve mükemmel, test edilmiş kodlar yazmasına yardımcı olur.