Bir sahte yönteme ardışık çağrılar iddia etme


176

Sahte bir sahiptir yararlı assert_called_with()bir yöntem . Ancak, anladığım kadarıyla bu sadece bir yönteme son çağrıyı kontrol eder .
Alaycı yöntemi 3 kez art arda çağıran kodum varsa, her seferinde farklı parametrelerle, bu 3 çağrıyı belirli parametreleriyle nasıl onaylayabilirim?

Yanıtlar:


180

assert_has_calls bu soruna başka bir yaklaşımdır.

Dokümanlardan:

assert_has_calls (çağrılar, any_order = Yanlış)

iddia belirtilen aramalarla çağrıldı iddia. Mock_calls listesi çağrılar için kontrol edilir.

Any_order Yanlış (varsayılan) ise, çağrılar sıralı olmalıdır. Belirtilen aramalardan önce veya sonra ekstra aramalar yapılabilir.

Any_order True ise, çağrılar herhangi bir sırada olabilir, ancak tümü mock_calls içinde görünmelidir.

Misal:

>>> from unittest.mock import call, Mock
>>> mock = Mock(return_value=None)
>>> mock(1)
>>> mock(2)
>>> mock(3)
>>> mock(4)
>>> calls = [call(2), call(3)]
>>> mock.assert_has_calls(calls)
>>> calls = [call(4), call(2), call(3)]
>>> mock.assert_has_calls(calls, any_order=True)

Kaynak: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_has_calls


9
Biraz garip onlar da sadece bir liste veya bir demet kullanılmış olabilir yeni bir "çağrı" türü eklemeyi seçti ...
jaapz

@jaapz Alt sınıflar tuple: isinstance(mock.call(1), tuple)verir True. Ayrıca bazı yöntemler ve nitelikler eklediler.
jpmc26

13
Mock'un ilk sürümleri düz bir demet kullandı, ancak kullanımı garip çıktı. Her işlev çağrısı bir demet alır (args, kwargs), bu nedenle "foo (123)" öğesinin doğru çağrıldığını kontrol etmek için "mock.call_args == ((123,), {})" ifadesini kullanmanız gerekir. "123 (arama)" ile karşılaştırıldığında bir ağız dolusu
Jonathan Hartley

Her çağrı örneğinde farklı bir dönüş değeri beklediğinizde ne yaparsınız?
CodeWithPride

2
@CodeWithPride daha fazla iş side_effect
arıyor

109

Genellikle, aramaların sırasını umursamıyorum, sadece olduklarını. Bu durumda, assert_any_callhakkında bir iddia ile birleşiyorum call_count.

>>> import mock
>>> m = mock.Mock()
>>> m(1)
<Mock name='mock()' id='37578160'>
>>> m(2)
<Mock name='mock()' id='37578160'>
>>> m(3)
<Mock name='mock()' id='37578160'>
>>> m.assert_any_call(1)
>>> m.assert_any_call(2)
>>> m.assert_any_call(3)
>>> assert 3 == m.call_count
>>> m.assert_any_call(4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "[python path]\lib\site-packages\mock.py", line 891, in assert_any_call
    '%s call not found' % expected_string
AssertionError: mock(4) call not found

Bunu tek bir yönteme aktarılan çağrıların büyük bir listesinden daha kolay okunur ve anlaşılır buluyorum.

Siparişe önem veriyorsanız veya birden fazla özdeş çağrı bekliyorsanız assert_has_callsdaha uygun olabilir.

Düzenle

Bu yanıtı yayınladığımdan beri, genel olarak test etme yaklaşımımı tekrar düşündüm. Testiniz bu kadar karmaşık hale geliyorsa, uygun olmayan testler yapıyor olabileceğinizi veya bir tasarım sorununuz olabileceğini belirtmek gerekir. Alaycılar, nesne yönelimli bir tasarımda nesneler arası iletişimi test etmek için tasarlanmıştır. Tasarımınıza itiraz edilmezse (daha prosedürel veya işlevsel olduğu gibi), sahte tamamen uygunsuz olabilir. Ayrıca, yöntemin içinde çok fazla şey olabilir veya en iyi kilitsiz bırakılan iç ayrıntıları test ediyor olabilirsiniz. Kodum çok nesne odaklı olmadığında bu yöntemde belirtilen stratejiyi geliştirdim ve ayrıca en iyi kilitsiz bırakılan iç detayları da test ettiğime inanıyorum.


@ jpmc26 düzenlemeniz hakkında daha fazla ayrıntı verebilir misiniz? 'En iyi solda engelsiz' ile ne demek istiyorsun? Bir yöntem içinde çağrı yapılıp yapılmadığını başka nasıl test edersiniz
otgw

Çoğu zaman, gerçek yöntemin çağrılmasına izin vermek daha iyidir. Diğer yöntem bozulursa, testi bozabilir, ancak bundan kaçınma değeri daha basit, daha sürdürülebilir bir teste sahip olmaktan daha küçüktür. Alay etmek için en iyi zaman, diğer yönteme yapılan harici çağrı, test etmek istediğiniz şeydir (Genellikle, bu, bir tür sonucun kendisine aktarıldığı ve test edilen kodun bir sonuç döndürmediği anlamına gelir.) Veya diğer yöntemdir. ortadan kaldırmak istediğiniz harici bağımlılıklar (veritabanı, web siteleri) vardır. (Teknik olarak, son dava daha fazla bir
saplamadır

@ jpmc26 alay, bağımlılık enjeksiyonundan veya başka bir çalışma zamanı stratejisi seçim yönteminden kaçınmak istediğinizde yararlıdır. Sizin de bahsettiğiniz gibi, dış hizmetleri çağırmadan ve daha da önemlisi, çevreye duyarlı olmadan (iyi bir kodun olması için bir hayır do() if TEST_ENV=='prod' else dont()), yöntemlerin iç mantığını test etmek, önerdiğiniz şekilde alay ederek kolayca elde edilir. bunun bir yan etkisi, sürüm başına testleri sürdürmektir (diyelim ki google arama api v1 ve v2 arasındaki kod değişiklikleri, kodunuz ne olursa olsun sürüm 1'i test edecektir)
Daniel Dubovski

@DanielDubovski Testlerinizin çoğu girdi / çıktıya dayalı olmalıdır. Bu her zaman mümkün değildir, ancak çoğu zaman mümkün değilse, muhtemelen bir tasarım sorununuz vardır. Normalde başka bir kod parçasından gelen ve bir bağımlılığı kesmek istediğinizde döndürülen bir değere ihtiyacınız olduğunda, genellikle bir saplama yapar. Mocks, yalnızca bazı durum değiştirme işlevinin (muhtemelen dönüş değeri olmayan) çağrıldığını doğrulamanız gerektiğinde gereklidir. (Bir sahte ve bir saplama arasındaki fark, bir saplama ile bir çağrıda iddia etmemenizdir.) Saplamaların yapacağı alayları kullanmak, testlerinizi daha az sürdürülebilir hale getirir.
jpmc26

@ jpmc26 harici bir hizmeti bir tür çıktı olarak çağırmıyor mu? elbette gönderilecek mesajı oluşturan kodu yeniden düzenleyebilir ve çağrı parametrelerini iddia etmek yerine test edebilirsiniz, ancak IMHO, hemen hemen aynı. Harici API'leri çağırarak yeniden tasarlamayı nasıl önerirsiniz? Alay etmenin en aza indirilmesi gerektiğini kabul ediyorum, tüm söylüyorum, mantığın beklendiği gibi davrandığından emin olmak için harici hizmetlere gönderdiğiniz verileri test edemediğiniz.
Daniel Dubovski


17

Buna her zaman tekrar tekrar bakmak zorundayım, işte cevabım.


Aynı sınıftan farklı nesnelerde birden çok yöntem çağrısı iddia etme

Bir ağır hizmet sınıfımız olduğunu varsayalım (ki bunu yapmak istiyoruz):

In [1]: class HeavyDuty(object):
   ...:     def __init__(self):
   ...:         import time
   ...:         time.sleep(2)  # <- Spends a lot of time here
   ...:     
   ...:     def do_work(self, arg1, arg2):
   ...:         print("Called with %r and %r" % (arg1, arg2))
   ...:  

HeavyDutysınıfın iki örneğini kullanan bazı kodlar şunlardır:

In [2]: def heavy_work():
   ...:     hd1 = HeavyDuty()
   ...:     hd1.do_work(13, 17)
   ...:     hd2 = HeavyDuty()
   ...:     hd2.do_work(23, 29)
   ...:    


Şimdi, heavy_workfonksiyon için bir test örneği :

In [3]: from unittest.mock import patch, call
   ...: def test_heavy_work():
   ...:     expected_calls = [call.do_work(13, 17),call.do_work(23, 29)]
   ...:     
   ...:     with patch('__main__.HeavyDuty') as MockHeavyDuty:
   ...:         heavy_work()
   ...:         MockHeavyDuty.return_value.assert_has_calls(expected_calls)
   ...:  

HeavyDutySınıfı alay ediyoruz MockHeavyDuty. Yöneltmek yerine, her HeavyDutyörnekten gelen yöntem çağrılarını iddia etmek için . Buna ek olarak, listede hangi yöntem adını çağırmakla ilgilendiğimizi belirtmeliyiz. Bu yüzden listemiz, sadeceMockHeavyDuty.return_value.assert_has_callsMockHeavyDuty.assert_has_callsexpected_callscall.do_workcall .

Test senaryosunu kullanmak bize bunun başarılı olduğunu gösterir:

In [4]: print(test_heavy_work())
None


heavy_workİşlevi değiştirirsek, test başarısız olur ve yardımcı bir hata mesajı verir:

In [5]: def heavy_work():
   ...:     hd1 = HeavyDuty()
   ...:     hd1.do_work(113, 117)  # <- call args are different
   ...:     hd2 = HeavyDuty()
   ...:     hd2.do_work(123, 129)  # <- call args are different
   ...:     

In [6]: print(test_heavy_work())
---------------------------------------------------------------------------
(traceback omitted for clarity)

AssertionError: Calls not found.
Expected: [call.do_work(13, 17), call.do_work(23, 29)]
Actual: [call.do_work(113, 117), call.do_work(123, 129)]


Bir işleve birden çok çağrı iddia etme

Yukarıdakilerle karşılaştırmak için, bir işleve birden çok çağrının alay edilmesini gösteren bir örnek:

In [7]: def work_function(arg1, arg2):
   ...:     print("Called with args %r and %r" % (arg1, arg2))

In [8]: from unittest.mock import patch, call
   ...: def test_work_function():
   ...:     expected_calls = [call(13, 17), call(23, 29)]    
   ...:     with patch('__main__.work_function') as mock_work_function:
   ...:         work_function(13, 17)
   ...:         work_function(23, 29)
   ...:         mock_work_function.assert_has_calls(expected_calls)
   ...:    

In [9]: print(test_work_function())
None


İki ana fark vardır. Birincisi, bir işlevi alay ederken call, kullanmak yerine, beklenen çağrılarımızı kullanarak ayarladığımızdır call.some_method. İkincisi, assert_has_callson mock_work_functionyerine çağırıyoruz mock_work_function.return_value.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.