unittest.mock — ξεκινώντας¶
Added in version 3.3.
Χρήση της Προσομοίωσης (Mock)¶
Προσομοίωση Μεθόδων με Επιδιόρθωση (Patching)¶
Κοινές χρήσεις για αντικείμενα Mock περιλαμβάνουν:
Επιδιόρθωση (Patching) μεθόδων
Καταγραφή κλήσεων μεθόδων σε αντικείμενα
Ίσως θέλετε να αντικαταστήσετε μια μέθοδο σε ένα αντικείμενο για να ελέγξετε ότι καλείται με τα σωστά ορίσματα από ένα άλλο μέρος του συστήματος:
>>> real = SomeClass()
>>> real.method = MagicMock(name='method')
>>> real.method(3, 4, 5, key='value')
<MagicMock name='method()' id='...'>
Μόλις χρησιμοποιηθεί η προσομοίωσή μας (real.method σε αυτό το παράδειγμα) θα έχει μεθόδους και χαρακτηριστικά που σας επιτρέπουν να κάνετε δηλώσεις σχετικά με το πώς έχει χρησιμοποιηθεί.
Σημείωση
Στα περισσότερα από αυτά τα παραδείγματα οι κλάσεις Mock και MagicMock είναι εναλλάξιμες. Καθώς η MagicMock είναι η πιο ικανή κλάση, αποτελεί μια λογική επιλογή για χρήση από προεπιλογή.
Μόλις κληθεί η προσομοίωση, το χαρακτηριστικό called ορίζεται σε True. Πιο σημαντικό είναι ότι μπορούμε να χρησιμοποιήσουμε τη μέθοδο assert_called_with() ή assert_called_once_with() για να ελέγξουμε ότι κλήθηκε με τα σωστά ορίσματα.
Αυτό το παράδειγμα ελέγχει ότι η κλήση της ProductionClass().method έχει ως αποτέλεσμα μια κλήση στη μέθοδο something:
>>> class ProductionClass:
... def method(self):
... self.something(1, 2, 3)
... def something(self, a, b, c):
... pass
...
>>> real = ProductionClass()
>>> real.something = MagicMock()
>>> real.method()
>>> real.something.assert_called_once_with(1, 2, 3)
Προσομοίωση για Κλήσεις Μεθόδων σε ένα Αντικείμενο¶
Στο τελευταίο παράδειγμα επιδιορθώσαμε μια μέθοδο απευθείας σε ένα αντικείμενο για να ελέγξουμε ότι κλήθηκε σωστά. Μια άλλη κοινή χρήση είναι να περάσουμε ένα αντικείμενο σε μια μέθοδο (ή σε κάποιο μέρος του συστήματος υπό δοκιμή) και στη συνέχεια να ελέγξουμε ότι χρησιμοποιείται με τον σωστό τρόπο.
Η απλή ProductionClass παρακάτω έχει μια μέθοδο closer. Εάν κληθεί με ένα αντικείμενο, τότε καλεί τη μέθοδο close σε αυτό.
>>> class ProductionClass:
... def closer(self, something):
... something.close()
...
Έτσι, για να το δοκιμάσουμε, πρέπει να περάσουμε ένα αντικείμενο με μια μέθοδο close και να ελέγξουμε ότι κλήθηκε σωστά.
>>> real = ProductionClass()
>>> mock = Mock()
>>> real.closer(mock)
>>> mock.close.assert_called_with()
Δεν χρειάζεται να κάνουμε καμία εργασία για να παρέχουμε τη μέθοδο “close” στην προσομοίωσή μας. Η πρόσβαση στο close τη δημιουργεί. Έτσι, αν το “close” δεν έχει ήδη κληθεί, τότε η πρόσβαση σε αυτό στη δοκιμή θα το δημιουργήσει, αλλά η assert_called_with() θα κάνει raise μια εξαίρεση αποτυχίας.
Προσομοίωση Κλάσεων¶
Μια συνηθισμένη περίπτωση χρήσης είναι η προσομοίωση κλάσεων που δημιουργούνται από τον κώδικα υπό δοκιμή. Όταν επιδιορθώνετε (patch) μια κλάση, τότε αυτή η κλάση αντικαθίσταται με μια προσομοίωση. Τα στιγμιότυπα δημιουργούνται με την κλήση της κλάσης. Αυτό σημαίνει ότι έχετε πρόσβαση στο «στιγμιότυπο προσομοίωσης» εξετάζοντας την τιμή επιστροφής της προσομοιωμένης κλάσης.
Στο παρακάτω παράδειγμα έχουμε μια συνάρτηση some_function που δημιουργεί ένα στιγμιότυπο της Foo και καλεί μια μέθοδο σε αυτό. Η κλήση στη patch() αντικαθιστά την κλάση Foo με μια προσομοίωση. Το στιγμιότυπο της Foo είναι το αποτέλεσμα της κλήσης της προσομοίωσης, οπότε διαμορφώνεται τροποποιώντας το χαρακτηριστικό return_value της προσομοίωσης.
>>> def some_function():
... instance = module.Foo()
... return instance.method()
...
>>> with patch('module.Foo') as mock:
... instance = mock.return_value
... instance.method.return_value = 'the result'
... result = some_function()
... assert result == 'the result'
Ονομασία των προσομοιώσεών σας¶
Μπορεί να είναι χρήσιμο να δώσετε στις προσομοιώσεις σας ένα όνομα. Το όνομα εμφανίζεται στην αναπαράσταση (repr) της προσομοίωσης και μπορεί να είναι χρήσιμο όταν η προσομοίωση εμφανίζεται σε μηνύματα αποτυχίας δοκιμών. Το όνομα επίσης μεταδίδεται σε χαρακτηριστικά ή μεθόδους της προσομοίωσης:
>>> mock = MagicMock(name='foo')
>>> mock
<MagicMock name='foo' id='...'>
>>> mock.method
<MagicMock name='foo.method' id='...'>
Παρακολούθηση όλων των Κλήσεων¶
Συχνά θέλετε να παρακολουθείτε περισσότερες από μία κλήσεις σε μια μέθοδο. Το χαρακτηριστικό mock_calls καταγράφει όλες τις κλήσεις σε θυγατρικά χαρακτηριστικά της προσομοίωσης - και επίσης στα παιδιά τους.
>>> mock = MagicMock()
>>> mock.method()
<MagicMock name='mock.method()' id='...'>
>>> mock.attribute.method(10, x=53)
<MagicMock name='mock.attribute.method()' id='...'>
>>> mock.mock_calls
[call.method(), call.attribute.method(10, x=53)]
Εάν κάνετε μια δήλωση σχετικά με το mock_calls και έχουν κληθεί απροσδόκητες μέθοδοι, τότε η δήλωση θα αποτύχει. Αυτό είναι χρήσιμο επειδή εκτός από το να δηλώνετε ότι οι κλήσεις που περιμένατε έχουν γίνει, ελέγχετε επίσης ότι έγιναν με τη σωστή σειρά και χωρίς επιπλέον κλήσεις:
Χρησιμοποιήστε το αντικείμενο call για να κατασκευάσετε λίστες για σύγκριση με το mock_calls:
>>> expected = [call.method(), call.attribute.method(10, x=53)]
>>> mock.mock_calls == expected
True
Ωστόσο, οι παράμετροι σε κλήσεις που επιστρέφουν προσομοιώσεις δεν καταγράφονται, που σημαίνει ότι δεν είναι δυνατή η παρακολούθηση εμφωλευμένων κλήσεων όπου οι παράμετροι που χρησιμοποιούνται για τη δημιουργία προγόνων είναι σημαντικοί:
>>> m = Mock()
>>> m.factory(important=True).deliver()
<Mock name='mock.factory().deliver()' id='...'>
>>> m.mock_calls[-1] == call.factory(important=False).deliver()
True
Ορισμός Τιμών Επιστροφής και Χαρακτηριστικών¶
Ορισμός των τιμών επιστροφής σε ένα αντικείμενο προσομοίωσης είναι εξαιρετικά εύκολος:
>>> mock = Mock()
>>> mock.return_value = 3
>>> mock()
3
Φυσικά μπορείτε να κάνετε το ίδιο για μεθόδους στην προσομοίωση:
>>> mock = Mock()
>>> mock.method.return_value = 3
>>> mock.method()
3
Η τιμή επιστροφής μπορεί επίσης να οριστεί στον κατασκευαστή:
>>> mock = Mock(return_value=3)
>>> mock()
3
Εάν χρειάζεστε έναν ορισμό χαρακτηριστικού στην προσομοίωσή σας, απλώς κάντε το:
>>> mock = Mock()
>>> mock.x = 3
>>> mock.x
3
Μερικές φορές θέλετε να προσομοιώσετε μια πιο σύνθετη κατάσταση, όπως για παράδειγμα mock.connection.cursor().execute("SELECT 1"). Εάν θέλαμε αυτή τη κλήση να επιστρέψει μια λίστα, τότε πρέπει να διαμορφώσουμε το αποτέλεσμα της εμφωλευμένης κλήσης.
Μπορούμε να χρησιμοποιήσουμε το αντικείμενο call για να κατασκευάσουμε την λίστα των κλήσεων σε μια «αλυσιδωτή κλήση» όπως αυτή για εύκολη δήλωση αργότερα:
>>> mock = Mock()
>>> cursor = mock.connection.cursor.return_value
>>> cursor.execute.return_value = ['foo']
>>> mock.connection.cursor().execute("SELECT 1")
['foo']
>>> expected = call.connection.cursor().execute("SELECT 1").call_list()
>>> mock.mock_calls
[call.connection.cursor(), call.connection.cursor().execute('SELECT 1')]
>>> mock.mock_calls == expected
True
Είναι η κλήση στο .call_list() που μετατρέπει το αντικείμενο κλήσης σε μια λίστα κλήσεων που αντιπροσωπεύουν τις αλυσιδωτές κλήσεις.
Πρόκληση εξαιρέσεων με προσομοιώσεις¶
Ένα χρήσιμο χαρακτηριστικό είναι το side_effect. Εάν το ορίσετε σε μια κλάση εξαίρεσης ή ένα στιγμιότυπο, τότε η εξαίρεση θα γίνει raise όταν κληθεί η προσομοίωση.
>>> mock = Mock(side_effect=Exception('Boom!'))
>>> mock()
Traceback (most recent call last):
...
Exception: Boom!
Συναρτήσεις παρενέργειας και iterables¶
Το side_effect μπορεί επίσης να οριστεί σε μια συνάρτηση ή ένα iterable. Η χρήση του side_effect ως iterable είναι όταν η προσομοίωσή σας πρόκειται να κληθεί αρκετές φορές, και θέλετε κάθε κλήση να επιστρέφει μια διαφορετική τιμή. Όταν ορίζετε το side_effect σε ένα iterable, κάθε κλήση στην προσομοίωση επιστρέφει την επόμενη τιμή από το iterable:
>>> mock = MagicMock(side_effect=[4, 5, 6])
>>> mock()
4
>>> mock()
5
>>> mock()
6
Για πιο προηγμένες χρήσεις, όπως η δυναμική μεταβολή των τιμών επιστροφής ανάλογα με το πώς καλείται η προσομοίωση, το side_effect μπορεί να είναι μια συνάρτηση. Η συνάρτηση θα κληθεί με τα ίδια ορίσματα με την προσομοίωση. Ό,τι επιστρέφει η συνάρτηση είναι αυτό που επιστρέφει η κλήση:
>>> vals = {(1, 2): 1, (2, 3): 2}
>>> def side_effect(*args):
... return vals[args]
...
>>> mock = MagicMock(side_effect=side_effect)
>>> mock(1, 2)
1
>>> mock(2, 3)
2
Προσομοίωση ασύγχρονων επαναληπτών¶
Από την έκδοση Python 3.8, οι AsyncMock και MagicMock έχουν υποστήριξη για την προσομοίωση ασύγχρονων επαναληπτών μέσω του __aiter__. Το χαρακτηριστικό return_value του __aiter__ μπορεί να χρησιμοποιηθεί για να ορίσει τις τιμές επιστροφής που θα χρησιμοποιηθούν για την επανάληψη.
>>> mock = MagicMock() # AsyncMock also works here
>>> mock.__aiter__.return_value = [1, 2, 3]
>>> async def main():
... return [i async for i in mock]
...
>>> asyncio.run(main())
[1, 2, 3]
Προσομοίωση ασύγχρονου διαχειριστή περιεχομένου¶
Από την έκδοση Python 3.8, οι AsyncMock και MagicMock έχουν υποστήριξη για την προσομοίωση ασύγχρονων διαχειριστών μέσω των __aenter__ και __aexit__. Από προεπιλογή, οι __aenter__ και __aexit__ είναι στιγμιότυπα AsyncMock που επιστρέφουν μια ασύγχρονη συνάρτηση.
>>> class AsyncContextManager:
... async def __aenter__(self):
... return self
... async def __aexit__(self, exc_type, exc, tb):
... pass
...
>>> mock_instance = MagicMock(AsyncContextManager()) # AsyncMock also works here
>>> async def main():
... async with mock_instance as result:
... pass
...
>>> asyncio.run(main())
>>> mock_instance.__aenter__.assert_awaited_once()
>>> mock_instance.__aexit__.assert_awaited_once()
Δημιουργία Προσομοίωσης από ένα Υφιστάμενο Αντικείμενο¶
Ένα πρόβλημα με την υπερβολική χρήση της προσομοίωσης είναι ότι συνδέει τις δοκιμές σας με την υλοποίηση των προσομοιώσεών σας αντί με τον πραγματικό κώδικα. Υποθέστε ότι έχετε μια κλάση που υλοποιεί την some_method. Σε μια δοκιμή για μια άλλη κλάση, παρέχετε μια προσομοίωση αυτού του αντικειμένου που επίσης παρέχει την some_method. Εάν αργότερα αναδιαρθρώσετε την πρώτη κλάση, έτσι ώστε να μην έχει πλέον την some_method - τότε οι δοκιμές σας θα συνεχίσουν να περνούν, παρόλο που ο κώδικάς σας είναι τώρα σπασμένος!
Mock σας επιτρέπει να παρέχετε ένα αντικείμενο ως προδιαγραφή για την προσομοίωση, χρησιμοποιώντας το όρισμα λέξης-κλειδιού spec. Η πρόσβαση σε μεθόδους / χαρακτηριστικά στην προσομοίωση που δεν υπάρχουν στο αντικείμενο προδιαγραφής σας θα γίνει raise αμέσως ένα σφάλμα χαρακτηριστικού. Εάν αλλάξετε την υλοποίηση της προδιαγραφής σας, τότε οι δοκιμές που χρησιμοποιούν αυτή την κλάση θα αρχίσουν να αποτυγχάνουν αμέσως χωρίς να χρειάζεται να δημιουργήσετε ένα στιγμιότυπο της κλάσης σε αυτές τις δοκιμές.
>>> mock = Mock(spec=SomeClass)
>>> mock.old_method()
Traceback (most recent call last):
...
AttributeError: Mock object has no attribute 'old_method'. Did you mean: 'class_method'?
Η χρήση μιας προδιαγραφής επιτρέπει επίσης μια πιο έξυπνη αντιστοίχιση των κλήσεων που γίνονται στην προσομοίωση, ανεξάρτητα από το αν ορισμένες παράμετροι περάστηκαν ως ορισμένες ή ονομαστικές παραμέτρους:
>>> def f(a, b, c): pass
...
>>> mock = Mock(spec=f)
>>> mock(1, 2, 3)
<Mock name='mock()' id='140161580456576'>
>>> mock.assert_called_with(a=1, b=2, c=3)
Εάν θέλετε αυτή η πιο έξυπνη αντιστοίχιση να λειτουργεί επίσης με κλήσεις μεθόδων στην προσομοίωση, μπορείτε να χρησιμοποιήσετε auto-speccing.
Εάν θέλετε μια πιο ισχυρή μορφή προδιαγραφής που αποτρέπει τον ορισμό αυθαίρετων χαρακτηριστικών καθώς και την πρόσβαση σε αυτά, τότε μπορείτε να χρησιμοποιήσετε το spec_set αντί για το spec.
Χρήση του side_effect για επιστροφή περιεχομένου ανά αρχείο¶
Η mock_open() χρησιμοποιείται για την επιδιόρθωση (patch) της open() μεθόδου. Το side_effect μπορεί να χρησιμοποιηθεί για την επιστροφή ενός νέου αντικειμένου Mock ανά κλήση. Αυτό μπορεί να χρησιμοποιηθεί για την επιστροφή διαφορετικών περιεχομένων ανά αρχείο που αποθηκεύονται σε ένα λεξικό:
DEFAULT = "default"
data_dict = {"file1": "data1",
"file2": "data2"}
def open_side_effect(name):
return mock_open(read_data=data_dict.get(name, DEFAULT))()
with patch("builtins.open", side_effect=open_side_effect):
with open("file1") as file1:
assert file1.read() == "data1"
with open("file2") as file2:
assert file2.read() == "data2"
with open("file3") as file2:
assert file2.read() == "default"
Διακοσμητές Επιδιόρθωσης (Patch Decorators)¶
Σημείωση
Με την patch() έχει σημασία να επιδιορθώνετε αντικείμενα στο χώρο ονομάτων όπου αναζητούνται. Αυτό είναι συνήθως απλό, αλλά για έναν γρήγορο οδηγό διαβάστε where to patch.
Μια κοινή ανάγκη στις δοκιμές είναι να επιδιορθώσετε ένα χαρακτηριστικό κλάσης ή ένα χαρακτηριστικό μονάδας, για παράδειγμα επιδιόρθωση ενός ενσωματωμένου ή επιδιόρθωση μιας κλάσης σε μια μονάδα για να ελέγξετε ότι δημιουργείται. Οι μονάδες και οι κλάσεις είναι ουσιαστικά παγκόσμιες, οπότε η επιδιόρθωση σε αυτές πρέπει να αναιρεθεί μετά τη δοκιμή ή η επιδιόρθωση θα επιμείνει σε άλλες δοκιμές και θα προκαλέσει προβλήματα που είναι δύσκολα να διαγνωστούν.
Η προσομοίωση παρέχει τρεις βολικούς διακοσμητές για αυτό: patch(), patch.object() και patch.dict(). Η patch παίρνει μια μοναδική συμβολοσειρά, της μορφής package.module.Class.attribute για να καθορίσει το χαρακτηριστικό που επιδιορθώνετε. Επίσης παίρνει προαιρετικά μια τιμή με την οποία θέλετε να αντικατασταθεί το χαρακτηριστικό (ή η κλάση ή οτιδήποτε). “patch.object” παίρνει ένα αντικείμενο και το όνομα του χαρακτηριστικού που θα θέλατε να επιδιορθώσετε, συν προαιρετικά την τιμή με την οποία θα το επιδιορθώσετε.
patch.object:
>>> original = SomeClass.attribute
>>> @patch.object(SomeClass, 'attribute', sentinel.attribute)
... def test():
... assert SomeClass.attribute == sentinel.attribute
...
>>> test()
>>> assert SomeClass.attribute == original
>>> @patch('package.module.attribute', sentinel.attribute)
... def test():
... from package.module import attribute
... assert attribute is sentinel.attribute
...
>>> test()
Εάν επιδιορθώνετε ένα module (συμπεριλαμβανομένου του builtins), τότε χρησιμοποιήστε την patch() αντί για την patch.object():
>>> mock = MagicMock(return_value=sentinel.file_handle)
>>> with patch('builtins.open', mock):
... handle = open('filename', 'r')
...
>>> mock.assert_called_with('filename', 'r')
>>> assert handle == sentinel.file_handle, "incorrect file handle returned"
Το όνομα του module μπορεί να είναι “τελεία”, με τη μορφή package.module εάν χρειάζεται:
>>> @patch('package.module.ClassName.attribute', sentinel.attribute)
... def test():
... from package.module import ClassName
... assert ClassName.attribute == sentinel.attribute
...
>>> test()
Ένα ωραίο μοτίβο είναι να διακοσμείτε πραγματικά τις μεθόδους δοκιμής:
>>> class MyTest(unittest.TestCase):
... @patch.object(SomeClass, 'attribute', sentinel.attribute)
... def test_something(self):
... self.assertEqual(SomeClass.attribute, sentinel.attribute)
...
>>> original = SomeClass.attribute
>>> MyTest('test_something').test_something()
>>> assert SomeClass.attribute == original
Εάν θέλετε να επιδιορθώσετε με μια προσομοίωση, μπορείτε να χρησιμοποιήσετε patch() με μόνο ένα όρισμα (ή patch.object() με δύο ορίσματα). Η προσομοίωση θα δημιουργηθεί για εσάς και θα περαστεί στη συνάρτηση / μέθοδο δοκιμής:
>>> class MyTest(unittest.TestCase):
... @patch.object(SomeClass, 'static_method')
... def test_something(self, mock_method):
... SomeClass.static_method()
... mock_method.assert_called_with()
...
>>> MyTest('test_something').test_something()
Μπορείτε να στοιβάξετε πολλαπλούς διακοσμητές επιδιόρθωσης χρησιμοποιώντας
>>> class MyTest(unittest.TestCase):
... @patch('package.module.ClassName1')
... @patch('package.module.ClassName2')
... def test_something(self, MockClass2, MockClass1):
... self.assertIs(package.module.ClassName1, MockClass1)
... self.assertIs(package.module.ClassName2, MockClass2)
...
>>> MyTest('test_something').test_something()
Όταν εμφωλεύετε διακοσμητές επιδιόρθωσης, οι προσομοιώσεις περνιούνται στη διακοσμημένη συνάρτηση με την ίδια σειρά που εφαρμόστηκαν (η κανονική τάξη Python με την οποία εφαρμόζονται οι διακοσμητές). Αυτό σημαίνει από κάτω προς τα πάνω, οπότε στο παραπάνω παράδειγμα η προσομοίωση για το test_module.ClassName2 περνάει πρώτη.
Υπάρχει επίσης η patch.dict() για τον ορισμό τιμών σε ένα λεξικό μόνο κατά τη διάρκεια ενός πεδίου και την αποκατάσταση του λεξικού στην αρχική του κατάσταση όταν τελειώνει η δοκιμή:
>>> foo = {'key': 'value'}
>>> original = foo.copy()
>>> with patch.dict(foo, {'newkey': 'newvalue'}, clear=True):
... assert foo == {'newkey': 'newvalue'}
...
>>> assert foo == original
Η patch, η patch.object και η patch.dict μπορούν όλες να χρησιμοποιηθούν ως διαχειριστές συμφραζομένων.
Όπου χρησιμοποιείτε την patch() για να δημιουργήσετε μια προσομοίωση για εσάς, μπορείτε να λάβετε μια αναφορά στην προσομοίωση χρησιμοποιώντας τη μορφή «as» της δήλωσης with:
>>> class ProductionClass:
... def method(self):
... pass
...
>>> with patch.object(ProductionClass, 'method') as mock_method:
... mock_method.return_value = None
... real = ProductionClass()
... real.method(1, 2, 3)
...
>>> mock_method.assert_called_with(1, 2, 3)
Ως εναλλακτική, η patch, η patch.object και η patch.dict μπορούν να χρησιμοποιηθούν ως διακοσμητές κλάσης. Όταν χρησιμοποιούνται με αυτόν τον τρόπο, είναι το ίδιο με την εφαρμογή του διακοσμητή ξεχωριστά σε κάθε μέθοδο του οποίου το όνομα ξεκινά με «test».
Περαιτέρω Παραδείγματα¶
Εδώ είναι μερικά ακόμη παραδείγματα για μερικά ελαφρώς πιο προηγμένα σενάρια.
Προσομοίωση αλυσιδωτών κλήσεων¶
Η προσομοίωση αλυσιδωτών κλήσεων είναι στην πραγματικότητα απλή με την προσομοίωση μόλις κατανοήσετε το χαρακτηριστικό return_value. Όταν καλείται μια προσομοίωση για πρώτη φορά, ή λαμβάνετε την return_value της πριν καλεστεί, δημιουργείται μια νέα Mock.
Αυτό σημαίνει ότι μπορείτε να δείτε πώς έχει χρησιμοποιηθεί το αντικείμενο που επιστρέφεται από μια κλήση σε ένα προσομοιωμένο αντικείμενο, εξετάζοντας την προσομοίωση return_value:
>>> mock = Mock()
>>> mock().foo(a=2, b=3)
<Mock name='mock().foo()' id='...'>
>>> mock.return_value.foo.assert_called_with(a=2, b=3)
Από εδώ είναι ένα απλό βήμα για τη διαμόρφωση και στη συνέχεια τη δημιουργία δηλώσεων σχετικά με τις αλυσιδωτές κλήσεις. Φυσικά μια άλλη εναλλακτική είναι να γράψετε τον κώδικά σας με πιο δοκιμαστικό τρόπο από την αρχή…
Λοιπόν, υποθέτουμε ότι έχουμε κάποιο κώδικα που μοιάζει λίγο με αυτό:
>>> class Something:
... def __init__(self):
... self.backend = BackendProvider()
... def method(self):
... response = self.backend.get_endpoint('foobar').create_call('spam', 'eggs').start_call()
... # more code
Υποθέτοντας ότι το BackendProvider έχει ήδη δοκιμαστεί καλά, πώς δοκιμάζουμε τη method(); Συγκεκριμένα, θέλουμε να δοκιμάσουμε ότι η ενότητα κώδικα # more code χρησιμοποιεί το αντικείμενο απόκρισης με τον σωστό τρόπο.
Καθώς αυτή η αλυσίδα κλήσεων γίνεται από ένα χαρακτηριστικό στιγμιότυπου, μπορούμε να επιδιορθώσουμε (monkey patch) το χαρακτηριστικό backend σε ένα στιγμιότυπο Something. Σε αυτή την συγκεκριμένη περίπτωση, μας ενδιαφέρει μόνο η τιμή επιστροφής από την τελική κλήση στο start_call, οπότε δεν έχουμε πολλή διαμόρφωση να κάνουμε. Ας υποθέσουμε ότι το αντικείμενο που επιστρέφει είναι “σαν αρχείο”, οπότε θα διασφαλίσουμε ότι το αντικείμενο απόκρισης μας χρησιμοποιεί την ενσωματωμένη open() ως το spec της.
Για να το κάνουμε αυτό, δημιουργούμε ένα στιγμιότυπο προσομοίωσης ως το mock backend μας και δημιουργούμε ένα αντικείμενο προσομοίωσης απόκρισης για αυτό. Για να ορίσουμε την απόκριση ως την τιμή επιστροφής για εκείνο το τελικό start_call, θα μπορούσαμε να κάνουμε αυτό:
mock_backend.get_endpoint.return_value.create_call.return_value.start_call.return_value = mock_response
Μπορούμε να το κάνουμε με έναν ελαφρώς πιο ωραίο τρόπο χρησιμοποιώντας τη configure_mock() μέθοδο για να ορίσουμε απευθείας την τιμή επιστροφής για εμάς:
>>> something = Something()
>>> mock_response = Mock(spec=open)
>>> mock_backend = Mock()
>>> config = {'get_endpoint.return_value.create_call.return_value.start_call.return_value': mock_response}
>>> mock_backend.configure_mock(**config)
Με αυτά, επιδιορθώνουμε (monkey patch) το «mock backend» στη θέση του και μπορούμε να κάνουμε την πραγματική κλήση:
>>> something.backend = mock_backend
>>> something.method()
Με το χαρακτηριστικό mock_calls μπορούμε να ελέγξουμε την αλυσιδωτή κλήση με μια μοναδική δήλωση. Μια αλυσιδωτή κλήση είναι πολλές κλήσεις σε μία γραμμή κώδικα, οπότε θα υπάρχουν πολλές εισαγωγές στο mock_calls. Μπορούμε να χρησιμοποιήσουμε τη call.call_list() για να δημιουργήσουμε αυτή τη λίστα κλήσεων για εμάς:
>>> chained = call.get_endpoint('foobar').create_call('spam', 'eggs').start_call()
>>> call_list = chained.call_list()
>>> assert mock_backend.mock_calls == call_list
Μερική προσομοίωση¶
Για ορισμένες δοκιμές, μπορεί να θέλετε να προσομοιώσετε μια κλήση στη datetime.date.today() για να επιστρέψετε μια γνωστή ημερομηνία, αλλά δεν θέλετε να αποτρέψετε τον κώδικα υπό δοκιμή από τη δημιουργία νέων αντικειμένων ημερομηνίας. Δυστυχώς, η datetime.date είναι γραμμένη σε C, οπότε δεν μπορείτε απλώς να κάνετε monkey-patch στη στατική datetime.date.today() μέθοδο.
Αντίθετα, μπορείτε ουσιαστικά να τυλίξετε την κλάση ημερομηνίας με μια προσομοίωση, ενώ περνάτε κλήσεις στον κατασκευαστή στην πραγματική κλάση (και επιστρέφοντας πραγματικά στιγμιότυπα).
Η patch decorator χρησιμοποιείται εδώ για να επιδιορθώσετε την κλάση ημερομηνίας στο μοντέλο υπό δοκιμή. Το χαρακτηριστικό side_effect στην προσομοίωση της κλάσης ημερομηνίας ρυθμίζεται στη συνέχεια σε μια συνάρτηση λάμδα που επιστρέφει μια πραγματική ημερομηνία. Όταν καλείται η προσομοίωση της κλάσης ημερομηνίας, θα κατασκευαστεί και θα επιστραφεί μια πραγματική ημερομηνία από το side_effect.
>>> from datetime import date
>>> with patch('mymodule.date') as mock_date:
... mock_date.today.return_value = date(2010, 10, 8)
... mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
...
... assert mymodule.date.today() == date(2010, 10, 8)
... assert mymodule.date(2009, 6, 8) == date(2009, 6, 8)
Σημειώστε ότι δεν επιδιορθώνουμε παγκοσμίως την datetime.date, αλλά επιδιορθώνουμε την date στο μοντέλο που τη χρησιμοποιεί. Δείτε where to patch.
Όταν καλείται το date.today(), επιστρέφεται μια γνωστή ημερομηνία, αλλά οι κλήσεις στον κατασκευαστή date(...) εξακολουθούν να επιστρέφουν κανονικές ημερομηνίες. Χωρίς αυτό, μπορεί να βρεθείτε να πρέπει να υπολογίσετε ένα αναμενόμενο αποτέλεσμα χρησιμοποιώντας ακριβώς τον ίδιο αλγόριθμο με τον κώδικα υπό δοκιμή, που είναι ένα κλασικό αντι-μοτίβο δοκιμών.
Οι κλήσεις στον κατασκευαστή ημερομηνίας καταγράφονται στα χαρακτηριστικά του mock_date (call_count και φίλοι), τα οποία μπορεί επίσης να είναι χρήσιμα για τις δοκιμές σας.
Ένας εναλλακτικός τρόπος αντιμετώπισης της προσομοίωσης ημερομηνιών ή άλλων ενσωματωμένων κλάσεων, συζητείται σε αυτή την καταχώρηση ιστολογίου.
Προσομοίωση μιας Μεθόδου Γεννήτριας¶
Μια γεννήτρια Python είναι μια συνάρτηση ή μέθοδος που χρησιμοποιεί τη yield δήλωση για να επιστρέψει μια σειρά τιμών όταν γίνεται επανάληψη πάνω της [1].
Μια μέθοδος / συνάρτηση γεννήτριας καλείται για να επιστρέψει το αντικείμενο γεννήτριας. Είναι το αντικείμενο γεννήτριας που στη συνέχεια επαναλαμβάνεται. Η μέθοδος πρωτοκόλλου για την επανάληψη είναι η __iter__(), οπότε μπορούμε να την προσομοιώσουμε χρησιμοποιώντας μια MagicMock.
Εδώ είναι ένα παράδειγμα κλάσης με μια μέθοδο «iter» που υλοποιείται ως γεννήτρια:
>>> class Foo:
... def iter(self):
... for i in [1, 2, 3]:
... yield i
...
>>> foo = Foo()
>>> list(foo.iter())
[1, 2, 3]
Πώς θα προσομοιώναμε αυτή την κλάση, και συγκεκριμένα τη μέθοδο της «iter»;
Για να διαμορφώσουμε τις τιμές που επιστρέφονται από την επανάληψη (ρητά στην κλήση στη list), πρέπει να διαμορφώσουμε το αντικείμενο που επιστρέφεται από την κλήση στο foo.iter().
>>> mock_foo = MagicMock()
>>> mock_foo.iter.return_value = iter([1, 2, 3])
>>> list(mock_foo.iter())
[1, 2, 3]
Εφαρμογή της ίδιας επιδιόρθωσης σε κάθε μέθοδο δοκιμής¶
Εάν θέλετε να υπάρχουν πολλές επιδιορθώσεις για πολλαπλές μεθόδους δοκιμής, ο προφανής τρόπος είναι να εφαρμόσετε τους διακοσμητές επιδιόρθωσης σε κάθε μέθοδο. Αυτό μπορεί να φαίνεται σαν περιττή επανάληψη. Αντίθετα, μπορείτε να χρησιμοποιήσετε την patch() (σε όλες τις διάφορες μορφές της) ως διακοσμητή κλάσης. Αυτό εφαρμόζει τις επιδιορθώσεις σε όλες τις μεθόδους δοκιμής στην κλάση. Μια μέθοδος δοκιμής αναγνωρίζεται από μεθόδους των οποίων τα ονόματα ξεκινούν με test:
>>> @patch('mymodule.SomeClass')
... class MyTest(unittest.TestCase):
...
... def test_one(self, MockSomeClass):
... self.assertIs(mymodule.SomeClass, MockSomeClass)
...
... def test_two(self, MockSomeClass):
... self.assertIs(mymodule.SomeClass, MockSomeClass)
...
... def not_a_test(self):
... return 'something'
...
>>> MyTest('test_one').test_one()
>>> MyTest('test_two').test_two()
>>> MyTest('test_two').not_a_test()
'something'
Ένας εναλλακτικός τρόπος διαχείρισης των επιδιορθώσεων είναι η χρήση των patch methods: start and stop. Αυτά σας επιτρέπουν να μετακινήσετε την επιδιόρθωση στις μεθόδους σας setUp και tearDown.
>>> class MyTest(unittest.TestCase):
... def setUp(self):
... self.patcher = patch('mymodule.foo')
... self.mock_foo = self.patcher.start()
...
... def test_foo(self):
... self.assertIs(mymodule.foo, self.mock_foo)
...
... def tearDown(self):
... self.patcher.stop()
...
>>> MyTest('test_foo').run()
Εάν χρησιμοποιήσετε αυτήν την τεχνική, πρέπει να διασφαλίσετε ότι η επιδιόρθωση αναιρείται καλώντας το stop. Αυτό μπορεί να είναι πιο περίπλοκο από ό,τι μπορεί να νομίζετε, επειδή εάν γίνει raise μια εξαίρεση στο setUp, τότε το tearDown δεν καλείται. Η unittest.TestCase.addCleanup() καθιστά αυτό πιο εύκολο:
>>> class MyTest(unittest.TestCase):
... def setUp(self):
... patcher = patch('mymodule.foo')
... self.addCleanup(patcher.stop)
... self.mock_foo = patcher.start()
...
... def test_foo(self):
... self.assertIs(mymodule.foo, self.mock_foo)
...
>>> MyTest('test_foo').run()
Προσομοίωση Μη Δεσμευμένων Μεθόδων¶
Μερικές φορές μια δοκιμή χρειάζεται να επιδιορθώσει μια μη δεσμευμένη μέθοδο, που σημαίνει την επιδιόρθωση της μεθόδου στην κλάση αντί για το στιγμιότυπο. Για να κάνετε δηλώσεις σχετικά με το ποια αντικείμενα καλούσαν αυτή τη συγκεκριμένη μέθοδο, πρέπει να περάσετε το self ως πρώτο όρισμα. Το πρόβλημα είναι ότι δεν μπορείτε να επιδιορθώσετε με μια προσομοίωση για αυτό, επειδή αν αντικαταστήσετε μια μη δεσμευμένη μέθοδο με μια προσομοίωση, δεν γίνεται δεσμευμένη μέθοδος όταν ανακτάται από το στιγμιότυπο, και έτσι δεν περνάει το self. Η λύση είναι να επιδιορθώσετε τη μη δεσμευμένη μέθοδο με μια πραγματική συνάρτηση αντί για αυτό. Ο διακοσμητής patch() το καθιστά τόσο απλό να επιδιορθώνετε μεθόδους με μια προσομοίωση που η ανάγκη δημιουργίας μιας πραγματικής συνάρτησης γίνεται ενοχλητική.
Εάν περάσετε το autospec=True στην επιδιόρθωση, τότε γίνεται η επιδιόρθωση με ένα πραγματικό αντικείμενο συνάρτησης. Αυτό το αντικείμενο συνάρτησης έχει την ίδια υπογραφή με αυτό που αντικαθιστά, αλλά αναθέτει σε μια προσομοίωση κάτω από την κουκούλα. Ακόμα λαμβάνετε την προσομοίωσή σας που δημιουργείται αυτόματα με ακριβώς τον ίδιο τρόπο όπως πριν. Αυτό που σημαίνει, όμως, είναι ότι αν το χρησιμοποιήσετε για να επιδιορθώσετε μια μη δεσμευμένη μέθοδο σε μια κλάση, η προσομοιωμένη συνάρτηση θα μετατραπεί σε δεσμευμένη μέθοδο εάν ανακτηθεί από ένα στιγμιότυπο. Θα έχει το self που περνάει ως πρώτο όρισμα, που είναι ακριβώς αυτό που χρειαζόταν:
>>> class Foo:
... def foo(self):
... pass
...
>>> with patch.object(Foo, 'foo', autospec=True) as mock_foo:
... mock_foo.return_value = 'foo'
... foo = Foo()
... foo.foo()
...
'foo'
>>> mock_foo.assert_called_once_with(foo)
Εάν δεν χρησιμοποιήσουμε το autospec=True, τότε η μη δεσμευμένη μέθοδος επιδιορθώνεται με ένα στιγμιότυπο Mock αντίθετα, και δεν καλείται με το self.
Έλεγχος πολλαπλών κλήσεων με προσομοίωση¶
Η προσομοίωση έχει ένα ωραίο API για τη δημιουργία δηλώσεων σχετικά με το πώς χρησιμοποιούνται τα αντικείμενα προσομοίωσής σας.
>>> mock = Mock()
>>> mock.foo_bar.return_value = None
>>> mock.foo_bar('baz', spam='eggs')
>>> mock.foo_bar.assert_called_with('baz', spam='eggs')
Εάν η προσομοίωσή σας καλείται μόνο μία φορά, μπορείτε να χρησιμοποιήσετε τη assert_called_once_with() μέθοδο που επίσης δηλώνει ότι το call_count είναι ένα.
>>> mock.foo_bar.assert_called_once_with('baz', spam='eggs')
>>> mock.foo_bar()
>>> mock.foo_bar.assert_called_once_with('baz', spam='eggs')
Traceback (most recent call last):
...
AssertionError: Expected 'foo_bar' to be called once. Called 2 times.
Calls: [call('baz', spam='eggs'), call()].
Και οι δύο assert_called_with και assert_called_once_with κάνουν δηλώσεις σχετικά με την τελευταία κλήση. Εάν η προσομοίωσή σας πρόκειται να καλείται πολλές φορές, και θέλετε να κάνετε δηλώσεις σχετικά με όλες αυτές τις κλήσεις, μπορείτε να χρησιμοποιήσετε το call_args_list:
>>> mock = Mock(return_value=None)
>>> mock(1, 2, 3)
>>> mock(4, 5, 6)
>>> mock()
>>> mock.call_args_list
[call(1, 2, 3), call(4, 5, 6), call()]
Ο βοηθός call καθιστά εύκολο να κάνετε δηλώσεις σχετικά με αυτές τις κλήσεις. Μπορείτε να δημιουργήσετε μια λίστα με τις αναμενόμενες κλήσεις και να τη συγκρίνετε με το call_args_list. Αυτό μοιάζει αξιοσημείωτα με το repr του call_args_list:
>>> expected = [call(1, 2, 3), call(4, 5, 6), call()]
>>> mock.call_args_list == expected
True
Αντιμετώπιση μεταβλητών ορισμάτων¶
Μια άλλη κατάσταση που είναι σπάνια, αλλά μπορεί να σας δαγκώσει, είναι όταν η προσομοίωσή σας καλείται με μεταβλητά ορίσματα. Το call_args και το call_args_list αποθηκεύουν αναφορές στα ορίσματα. Εάν τα ορίσματα μεταβλητοποιηθούν από τον κώδικα υπό δοκιμή, τότε δεν μπορείτε πλέον να κάνετε δηλώσεις σχετικά με το ποιες ήταν οι τιμές όταν κλήθηκε η προσομοίωση.
Εδώ είναι μερικοί παραδείγματα κώδικα που δείχνουν το πρόβλημα. Φανταστείτε τις παρακάτω συναρτήσεις ορισμένες στο “mymodule”:
def frob(val):
pass
def grob(val):
"First frob and then clear val"
frob(val)
val.clear()
Όταν προσπαθήσουμε να δοκιμάσουμε ότι το grob καλεί το frob με το σωστό όρισμα, δείτε τι συμβαίνει:
>>> with patch('mymodule.frob') as mock_frob:
... val = {6}
... mymodule.grob(val)
...
>>> val
set()
>>> mock_frob.assert_called_with({6})
Traceback (most recent call last):
...
AssertionError: Expected: (({6},), {})
Called with: ((set(),), {})
Μια πιθανότητα θα ήταν για την προσομοίωση να αντιγράψει τα ορίσματα που περνάτε. Αυτό θα μπορούσε στη συνέχεια να προκαλέσει προβλήματα εάν κάνετε δηλώσεις που βασίζονται στην ταυτότητα αντικειμένου για την ισότητα.
Εδώ είναι μια λύση που χρησιμοποιεί τη λειτουργικότητα side_effect. Εάν παρέχετε μια συνάρτηση side_effect για μια προσομοίωση, τότε το side_effect θα κληθεί με τα ίδια ορίσματα με την προσομοίωση. Αυτό μας δίνει μια ευκαιρία να αντιγράψουμε τα ορίσματα και να τα αποθηκεύσουμε για μετέπειτα δηλώσεις. Σε αυτό το παράδειγμα, χρησιμοποιώ άλλη προσομοίωση για να αποθηκεύσω τα ορίσματα, ώστε να μπορώ να χρησιμοποιήσω τις μεθόδους προσομοίωσης για να κάνω τη δήλωση. Και πάλι, μια βοηθητική συνάρτηση το ρυθμίζει για μένα.
>>> from copy import deepcopy
>>> from unittest.mock import Mock, patch, DEFAULT
>>> def copy_call_args(mock):
... new_mock = Mock()
... def side_effect(*args, **kwargs):
... args = deepcopy(args)
... kwargs = deepcopy(kwargs)
... new_mock(*args, **kwargs)
... return DEFAULT
... mock.side_effect = side_effect
... return new_mock
...
>>> with patch('mymodule.frob') as mock_frob:
... new_mock = copy_call_args(mock_frob)
... val = {6}
... mymodule.grob(val)
...
>>> new_mock.assert_called_with({6})
>>> new_mock.call_args
call({6})
Το copy_call_args καλείται με την προσομοίωση που θα κληθεί. Επιστρέφει μια νέα προσομοίωση στην οποία κάνουμε τη δήλωση. Η συνάρτηση side_effect κάνει ένα αντίγραφο των ορισμάτων και καλεί τη new_mock με το αντίγραφο.
Σημείωση
Εάν η προσομοίωσή σας πρόκειται να χρησιμοποιηθεί μόνο μία φορά, υπάρχει ένας ευκολότερος τρόπος ελέγχου των ορισμάτων στο σημείο που καλούνται. Μπορείτε απλά να κάνετε τον έλεγχο μέσα σε μια side_effect συνάρτηση.
>>> def side_effect(arg):
... assert arg == {6}
...
>>> mock = Mock(side_effect=side_effect)
>>> mock({6})
>>> mock(set())
Traceback (most recent call last):
...
AssertionError
Μια εναλλακτική προσέγγιση είναι να δημιουργήσετε μια υποκλάση της Mock ή MagicMock που αντιγράφει (χρησιμοποιώντας τη copy.deepcopy()) τα ορίσματα. Εδώ είναι ένα παράδειγμα υλοποίησης:
>>> from copy import deepcopy
>>> class CopyingMock(MagicMock):
... def __call__(self, /, *args, **kwargs):
... args = deepcopy(args)
... kwargs = deepcopy(kwargs)
... return super().__call__(*args, **kwargs)
...
>>> c = CopyingMock(return_value=None)
>>> arg = set()
>>> c(arg)
>>> arg.add(1)
>>> c.assert_called_with(set())
>>> c.assert_called_with(arg)
Traceback (most recent call last):
...
AssertionError: expected call not found.
Expected: mock({1})
Actual: mock(set())
>>> c.foo
<CopyingMock name='mock.foo' id='...'>
Όταν υποκλάσετε το Mock ή το MagicMock, όλα τα δυναμικά δημιουργημένα χαρακτηριστικά, και το return_value θα χρησιμοποιούν την υποκλάση σας αυτόματα. Αυτό σημαίνει ότι όλα τα παιδιά ενός CopyingMock θα έχουν επίσης τον τύπο CopyingMock.
Εμφωλευμένες Επιδιορθώσεις¶
Η χρήση της επιδιόρθωσης ως διαχειριστής περιβάλλοντος είναι ωραία, αλλά αν κάνετε πολλαπλές επιδιορθώσεις, μπορεί να καταλήξετε με επικαλυπτόμενες δηλώσεις with που εσοχίζουν όλο και περισσότερο προς τα δεξιά:
>>> class MyTest(unittest.TestCase):
...
... def test_foo(self):
... with patch('mymodule.Foo') as mock_foo:
... with patch('mymodule.Bar') as mock_bar:
... with patch('mymodule.Spam') as mock_spam:
... assert mymodule.Foo is mock_foo
... assert mymodule.Bar is mock_bar
... assert mymodule.Spam is mock_spam
...
>>> original = mymodule.Foo
>>> MyTest('test_foo').test_foo()
>>> assert mymodule.Foo is original
Με τις λειτουργίες καθαρισμού του unittest και το patch methods: start and stop μπορούμε να επιτύχουμε το ίδιο αποτέλεσμα χωρίς την εμφωλευμένη εσοχή. Μια απλή βοηθητική μέθοδος, create_patch, τοποθετεί την επιδιόρθωση στη θέση της και επιστρέφει την δημιουργημένη προσομοίωση για εμάς:
>>> class MyTest(unittest.TestCase):
...
... def create_patch(self, name):
... patcher = patch(name)
... thing = patcher.start()
... self.addCleanup(patcher.stop)
... return thing
...
... def test_foo(self):
... mock_foo = self.create_patch('mymodule.Foo')
... mock_bar = self.create_patch('mymodule.Bar')
... mock_spam = self.create_patch('mymodule.Spam')
...
... assert mymodule.Foo is mock_foo
... assert mymodule.Bar is mock_bar
... assert mymodule.Spam is mock_spam
...
>>> original = mymodule.Foo
>>> MyTest('test_foo').run()
>>> assert mymodule.Foo is original
Προσομοίωση ενός λεξικού με το MagicMock¶
Ίσως θέλετε να προσομοιώσετε ένα λεξικό ή άλλο αντικείμενο κοντέινερ, καταγράφοντας όλη την πρόσβαση σε αυτό, ενώ εξακολουθεί να συμπεριφέρεται σαν λεξικό.
Μπορούμε να το κάνουμε αυτό με την MagicMock, η οποία θα συμπεριφέρεται σαν λεξικό, και χρησιμοποιώντας το side_effect για να αναθέσουμε την πρόσβαση στο λεξικό σε ένα πραγματικό υποκείμενο λεξικό που είναι υπό τον έλεγχό μας.
Όταν οι μέθοδοι __getitem__() και __setitem__() του MagicMock μας καλούνται (κανονική πρόσβαση λεξικού), τότε το side_effect καλείται με το κλειδί (και στην περίπτωση του __setitem__ και την τιμή επίσης). Μπορούμε επίσης να ελέγξουμε τι επιστρέφεται.
Μετά τη χρήση του MagicMock, μπορούμε να χρησιμοποιήσουμε χαρακτηριστικά όπως το call_args_list για να κάνουμε δηλώσεις σχετικά με το πώς χρησιμοποιήθηκε το λεξικό:
>>> my_dict = {'a': 1, 'b': 2, 'c': 3}
>>> def getitem(name):
... return my_dict[name]
...
>>> def setitem(name, val):
... my_dict[name] = val
...
>>> mock = MagicMock()
>>> mock.__getitem__.side_effect = getitem
>>> mock.__setitem__.side_effect = setitem
Σημείωση
Μια εναλλακτική λύση στη χρήση του MagicMock είναι να χρησιμοποιήσετε το Mock και να παρέχετε μόνο τις μαγικές μεθόδους που θέλετε συγκεκριμένα:
>>> mock = Mock()
>>> mock.__getitem__ = Mock(side_effect=getitem)
>>> mock.__setitem__ = Mock(side_effect=setitem)
Μια τρίτη επιλογή είναι να χρησιμοποιήσετε το MagicMock αλλά περνώντας το dict ως όρισμα spec (ή spec_set), έτσι ώστε το δημιουργημένο MagicMock να έχει διαθέσιμες μόνο τις μαγικές μεθόδους λεξικού:
>>> mock = MagicMock(spec_set=dict)
>>> mock.__getitem__.side_effect = getitem
>>> mock.__setitem__.side_effect = setitem
Με αυτές τις λειτουργίες side effect στη θέση τους, το mock θα συμπεριφέρεται σαν ένα κανονικό λεξικό αλλά καταγράφοντας την πρόσβαση. Ακόμα κάνει raise μια KeyError εάν προσπαθήσετε να αποκτήσετε πρόσβαση σε ένα κλειδί που δεν υπάρχει.
>>> mock['a']
1
>>> mock['c']
3
>>> mock['d']
Traceback (most recent call last):
...
KeyError: 'd'
>>> mock['b'] = 'fish'
>>> mock['d'] = 'eggs'
>>> mock['b']
'fish'
>>> mock['d']
'eggs'
Μετά τη χρήση του, μπορείτε να κάνετε δηλώσεις σχετικά με την πρόσβαση χρησιμοποιώντας τις κανονικές μεθόδους και χαρακτηριστικά προσομοίωσης:
>>> mock.__getitem__.call_args_list
[call('a'), call('c'), call('d'), call('b'), call('d')]
>>> mock.__setitem__.call_args_list
[call('b', 'fish'), call('d', 'eggs')]
>>> my_dict
{'a': 1, 'b': 'fish', 'c': 3, 'd': 'eggs'}
Υποκλάσεις Mock και τα χαρακτηριστικά τους¶
Υπάρχουν διάφοροι λόγοι για τους οποίους μπορεί να θέλετε να υποκλάσετε την Mock. Ένας λόγος μπορεί να είναι η προσθήκη βοηθητικών μεθόδων. Εδώ είναι ένα ανόητο παράδειγμα:
>>> class MyMock(MagicMock):
... def has_been_called(self):
... return self.called
...
>>> mymock = MyMock(return_value=None)
>>> mymock
<MyMock id='...'>
>>> mymock.has_been_called()
False
>>> mymock()
>>> mymock.has_been_called()
True
Η τυπική συμπεριφορά για τα στιγμιότυπα Mock είναι ότι τα χαρακτηριστικά και οι επιστρεφόμενες τιμές προσομοιώσεων είναι του ίδιου τύπου με την προσομοίωση στην οποία έχουν πρόσβαση. Αυτό διασφαλίζει ότι τα χαρακτηριστικά Mock είναι Mocks και τα χαρακτηριστικά MagicMock είναι MagicMocks [2]. Έτσι, εάν υποκλάσετε για να προσθέσετε βοηθητικές μεθόδους, τότε θα είναι επίσης διαθέσιμες στα χαρακτηριστικά και στην επιστρεφόμενη τιμή προσομοίωσης των στιγμιότυπων της υποκλάσης σας.
>>> mymock.foo
<MyMock name='mock.foo' id='...'>
>>> mymock.foo.has_been_called()
False
>>> mymock.foo()
<MyMock name='mock.foo()' id='...'>
>>> mymock.foo.has_been_called()
True
Μερικές φορές αυτό είναι άβολο. Για παράδειγμα, ένας χρήστης υποκλάσει την προσομοίωση για να δημιουργήσει έναν προσαρμογέα Twisted. Η εφαρμογή αυτού και στα χαρακτηριστικά προκαλεί πραγματικά σφάλματα.
Mock (σε όλες τις παραλλαγές του) χρησιμοποιεί μια μέθοδο που ονομάζεται _get_child_mock για να δημιουργήσει αυτές τις «υπο-προσομοιώσεις» για χαρακτηριστικά και επιστρεφόμενες τιμές. Μπορείτε να αποτρέψετε τη χρήση της υποκλάσης σας για χαρακτηριστικά, υπερκαλύπτοντας αυτή τη μέθοδο. Η υπογραφή είναι ότι λαμβάνει αυθαίρετα ορίσματα λέξεων-κλειδιών (**kwargs) που στη συνέχεια περνιούνται στον κατασκευαστή προσομοιώσεων:
>>> class Subclass(MagicMock):
... def _get_child_mock(self, /, **kwargs):
... return MagicMock(**kwargs)
...
>>> mymock = Subclass()
>>> mymock.foo
<MagicMock name='mock.foo' id='...'>
>>> assert isinstance(mymock, Subclass)
>>> assert not isinstance(mymock.foo, Subclass)
>>> assert not isinstance(mymock(), Subclass)
Μια εξαίρεση σε αυτόν τον κανόνα είναι οι μη κλητές προσομοιώσεις. Τα χαρακτηριστικά χρησιμοποιούν την κλητή παραλλαγή επειδή διαφορετικά οι μη κλητές προσομοιώσεις δεν θα μπορούσαν να έχουν κλητές μεθόδους.
Προσομοίωση εισαγωγών με το patch.dict¶
Μια κατάσταση όπου η προσομοίωση μπορεί να είναι δύσκολη είναι όταν έχετε μια τοπική εισαγωγή μέσα σε μια συνάρτηση. Αυτές είναι πιο δύσκολες για προσομοίωση επειδή δεν χρησιμοποιούν ένα αντικείμενο από το χώρο ονομάτων του module που μπορούμε να επιδιορθώσουμε.
Γενικά, οι τοπικές εισαγωγές πρέπει να αποφεύγονται. Κάποιες φορές γίνονται για να αποτραπούν οι κυκλικές εξαρτήσεις, για τις οποίες υπάρχει συνήθως ένας πολύ καλύτερος τρόπος για να λυθεί το πρόβλημα (αναδιαμόρφωση του κώδικα) ή για να αποτραπούν τα «προκαταβολικά κόστη» καθυστερώντας την εισαγωγή. Αυτό μπορεί επίσης να λυθεί με καλύτερους τρόπους από μια άνευ όρων τοπική εισαγωγή (αποθηκεύστε το module ως χαρακτηριστικό κλάσης ή module και κάντε την εισαγωγή μόνο στην πρώτη χρήση).
Ανεξάρτητα από αυτό, υπάρχει ένας τρόπος να χρησιμοποιήσετε το mock για να επηρεάσετε τα αποτελέσματα μιας εισαγωγής. Η εισαγωγή ανακτά ένα αντικείμενο από το λεξικό sys.modules. Σημειώστε ότι ανακτά ένα αντικείμενο, το οποίο δεν χρειάζεται να είναι ένα module. Η εισαγωγή ενός module για πρώτη φορά έχει ως αποτέλεσμα τη τοποθέτηση ενός αντικειμένου module στο sys.modules, οπότε συνήθως όταν εισάγετε κάτι, λαμβάνετε πίσω ένα module. Ωστόσο, αυτό δεν χρειάζεται να είναι η περίπτωση.
Αυτό σημαίνει ότι μπορείτε να χρησιμοποιήσετε το patch.dict() για να τοποθετήσετε προσωρινά μια προσομοίωση στο sys.modules. Οποιεσδήποτε εισαγωγές ενώ αυτή η επιδιόρθωση είναι ενεργή θα ανακτούν την προσομοίωση. Όταν ολοκληρωθεί η επιδιόρθωση (η διακοσμημένη συνάρτηση εξέρχεται, το σώμα της δήλωσης with ολοκληρώνεται ή καλείται το patcher.stop()), τότε ό,τι υπήρχε προηγουμένως θα αποκατασταθεί με ασφάλεια.
Εδώ είναι ένα παράδειγμα που προσομοιώνει το module “fooble”.
>>> import sys
>>> mock = Mock()
>>> with patch.dict('sys.modules', {'fooble': mock}):
... import fooble
... fooble.blob()
...
<Mock name='mock.blob()' id='...'>
>>> assert 'fooble' not in sys.modules
>>> mock.blob.assert_called_once_with()
Όπως μπορείτε να δείτε, το import fooble πετυχαίνει, αλλά κατά την έξοδο δεν παραμένει κανένα “fooble” στο sys.modules.
Αυτό λειτουργεί επίσης για τη μορφή from module import name:
>>> mock = Mock()
>>> with patch.dict('sys.modules', {'fooble': mock}):
... from fooble import blob
... blob.blip()
...
<Mock name='mock.blob.blip()' id='...'>
>>> mock.blob.blip.assert_called_once_with()
Με λίγη περισσότερη δουλειά, μπορείτε επίσης να προσομοιώσετε εισαγωγές πακέτων:
>>> mock = Mock()
>>> modules = {'package': mock, 'package.module': mock.module}
>>> with patch.dict('sys.modules', modules):
... from package.module import fooble
... fooble()
...
<Mock name='mock.module.fooble()' id='...'>
>>> mock.module.fooble.assert_called_once_with()
Παρακολούθηση της σειράς κλήσεων και λιγότερο λεκτικές δηλώσεις κλήσεων¶
Η Mock σας επιτρέπει να παρακολουθείτε τη σειρά των κλήσεων μεθόδων στις προσομοιώσεις σας μέσω του χαρακτηριστικού method_calls. Αυτό δεν σας επιτρέπει να παρακολουθείτε τη σειρά των κλήσεων μεταξύ ξεχωριστών αντικειμένων προσομοίωσης, ωστόσο μπορούμε να χρησιμοποιήσουμε το mock_calls για να επιτύχουμε το ίδιο αποτέλεσμα.
Επειδή οι προσομοιώσεις παρακολουθούν τις κλήσεις σε υπο-προσομοιώσεις στο mock_calls, και η πρόσβαση σε ένα αυθαίρετο χαρακτηριστικό μιας προσομοίωσης δημιουργεί μια υπο-προσομοίωση, μπορούμε να δημιουργήσουμε τις ξεχωριστές προσομοιώσεις μας από μια γονική. Οι κλήσεις σε αυτές τις υπο-προσομοιώσεις θα καταγράφονται στη συνέχεια, με σειρά, στο mock_calls της γονικής:
>>> manager = Mock()
>>> mock_foo = manager.foo
>>> mock_bar = manager.bar
>>> mock_foo.something()
<Mock name='mock.foo.something()' id='...'>
>>> mock_bar.other.thing()
<Mock name='mock.bar.other.thing()' id='...'>
>>> manager.mock_calls
[call.foo.something(), call.bar.other.thing()]
Μπορούμε στη συνέχεια να κάνουμε δηλώσεις σχετικά με τις κλήσεις, συμπεριλαμβανομένης της σειράς, συγκρίνοντας με το χαρακτηριστικό mock_calls στην προσομοίωση διαχειριστή:
>>> expected_calls = [call.foo.something(), call.bar.other.thing()]
>>> manager.mock_calls == expected_calls
True
Αν το patch δημιουργεί και τοποθετεί τις προσομοιώσεις σας, τότε μπορείτε να τις επισυνάψετε σε μια διαχειριστική προσομοίωση χρησιμοποιώντας τη μέθοδο attach_mock(). Μετά την επισύναψη, οι κλήσεις θα καταγράφονται στο mock_calls του διαχειριστή.
>>> manager = MagicMock()
>>> with patch('mymodule.Class1') as MockClass1:
... with patch('mymodule.Class2') as MockClass2:
... manager.attach_mock(MockClass1, 'MockClass1')
... manager.attach_mock(MockClass2, 'MockClass2')
... MockClass1().foo()
... MockClass2().bar()
<MagicMock name='mock.MockClass1().foo()' id='...'>
<MagicMock name='mock.MockClass2().bar()' id='...'>
>>> manager.mock_calls
[call.MockClass1(),
call.MockClass1().foo(),
call.MockClass2(),
call.MockClass2().bar()]
Εάν έχουν γίνει πολλές κλήσεις, αλλά σας ενδιαφέρει μόνο μια συγκεκριμένη ακολουθία από αυτές, τότε μια εναλλακτική είναι να χρησιμοποιήσετε τη μέθοδο assert_has_calls(). Αυτή λαμβάνει μια λίστα κλήσεων (κατασκευασμένη με το call αντικείμενο). Εάν αυτή η ακολουθία κλήσεων βρίσκεται στο mock_calls, τότε η δήλωση πετυχαίνει.
>>> m = MagicMock()
>>> m().foo().bar().baz()
<MagicMock name='mock().foo().bar().baz()' id='...'>
>>> m.one().two().three()
<MagicMock name='mock.one().two().three()' id='...'>
>>> calls = call.one().two().three().call_list()
>>> m.assert_has_calls(calls)
Ακόμα κι αν η αλυσιδωτή κλήση m.one().two().three() δεν είναι οι μόνες κλήσεις που έχουν γίνει στην προσομοίωση, η δήλωση εξακολουθεί να πετυχαίνει.
Μερικές φορές μια προσομοίωση μπορεί να έχει γίνει πολλές κλήσεις σε αυτήν, και σας ενδιαφέρει μόνο να κάνετε δηλώσεις για μερικές από αυτές τις κλήσεις. Μπορεί να μην σας ενδιαφέρει καν η σειρά. Σε αυτή την περίπτωση, μπορείτε να περάσετε το any_order=True στο assert_has_calls:
>>> m = MagicMock()
>>> m(1), m.two(2, 3), m.seven(7), m.fifty('50')
(...)
>>> calls = [call.fifty('50'), call(1), call.seven(7)]
>>> m.assert_has_calls(calls, any_order=True)
Πιο σύνθετη αντιστοίχιση ορισμάτων¶
Χρησιμοποιώντας την ίδια βασική έννοια με το ANY, μπορούμε να υλοποιήσουμε αντιστοιχίσεις για να κάνουμε πιο σύνθετες δηλώσεις σχετικά με αντικείμενα που χρησιμοποιούνται ως ορίσματα σε προσομοιώσεις.
Ας υποθέσουμε ότι αναμένουμε να περαστεί κάποιο αντικείμενο σε μια προσομοίωση που κανονικά συγκρίνεται ως ίσο βάσει της ταυτότητας του αντικειμένου (που είναι το προεπιλεγμένο της Python για καθορισμένες από τον χρήστη κλάσεις). Για να χρησιμοποιήσουμε τη assert_called_with(), θα πρέπει να περάσουμε το ακριβώς ίδιο αντικείμενο. Εάν μας ενδιαφέρουν μόνο μερικά από τα χαρακτηριστικά αυτού του αντικειμένου, τότε μπορούμε να δημιουργήσουμε έναν αντιστοιχιστή που θα ελέγχει αυτά τα χαρακτηριστικά για εμάς.
Μπορείτε να δείτε σε αυτό το παράδειγμα πώς μια “τυπική” κλήση στο assert_called_with δεν είναι επαρκής:
>>> class Foo:
... def __init__(self, a, b):
... self.a, self.b = a, b
...
>>> mock = Mock(return_value=None)
>>> mock(Foo(1, 2))
>>> mock.assert_called_with(Foo(1, 2))
Traceback (most recent call last):
...
AssertionError: expected call not found.
Expected: mock(<__main__.Foo object at 0x...>)
Actual: mock(<__main__.Foo object at 0x...>)
Μια συνάρτηση σύγκρισης για την κλάση μας Foo μπορεί να μοιάζει κάπως έτσι:
>>> def compare(self, other):
... if not type(self) == type(other):
... return False
... if self.a != other.a:
... return False
... if self.b != other.b:
... return False
... return True
...
Και ένα αντικείμενο αντιστοίχισης που μπορεί να χρησιμοποιεί συναρτήσεις σύγκρισης όπως αυτή για τη λειτουργία ισότητας του θα μοιάζει κάπως έτσι:
>>> class Matcher:
... def __init__(self, compare, some_obj):
... self.compare = compare
... self.some_obj = some_obj
... def __eq__(self, other):
... return self.compare(self.some_obj, other)
...
Βάζοντας όλα αυτά μαζί:
>>> match_foo = Matcher(compare, Foo(1, 2))
>>> mock.assert_called_with(match_foo)
Το Matcher δημιουργείται με τη συνάρτηση σύγκρισης και το αντικείμενο που θέλουμε να συγκρίνουμε. Στο assert_called_with, θα κληθεί η μέθοδος ισότητας του Matcher, η οποία συγκρίνει το αντικείμενο με το οποίο κλήθηκε η προσομοίωση με αυτό που δημιουργήσαμε τον αντιστοιχιστή μας. Εάν ταιριάζουν, τότε το assert_called_with περνάει, και αν δεν ταιριάζουν, γίνεται raise μια AssertionError:
>>> match_wrong = Matcher(compare, Foo(3, 4))
>>> mock.assert_called_with(match_wrong)
Traceback (most recent call last):
...
AssertionError: Expected: ((<Matcher object at 0x...>,), {})
Called with: ((<Foo object at 0x...>,), {})
Με λίγη τροποποίηση, θα μπορούσατε να κάνετε τη συνάρτηση σύγκρισης να κάνει raise απευθείας την AssertionError και να παρέχει ένα πιο χρήσιμο μήνυμα αποτυχίας.
Από την έκδοση 1.5, η βιβλιοθήκη δοκιμών Python PyHamcrest παρέχει παρόμοια λειτουργικότητα, που μπορεί να είναι χρήσιμη εδώ, με τη μορφή του αντιστοιχιστή ισότητας (hamcrest.library.integration.match_equality).