Γράφουμε ένα unit test. Ο κώδικάς μας χρησιμοποιεί ένα repository, μια βάση δεδομένων ή μια υπηρεσία ειδοποιήσεων.
Και κάπου εκεί εμφανίζεται η ερώτηση:
Να χρησιμοποιήσω fake ή mock; Ποια είναι η διαφορά και ποιο κάνει καλύτερο το test μου;
Ένα fake μάς δίνει μια απλοποιημένη υλοποίηση που λειτουργεί. Ένα mock μάς επιτρέπει να επαληθεύσουμε συγκεκριμένες κλήσεις προς έναν συνεργάτη.
Η επιλογή εξαρτάται από αυτό που θέλουμε να αποδείξουμε:
- Αν ενδιαφερόμαστε για το αποτέλεσμα μιας αλλαγής, ένα fake συχνά κάνει το test πιο φυσικό.
- Αν ενδιαφερόμαστε για μια συγκεκριμένη εξωτερική ενέργεια, ένα mock μπορεί να εκφράζει ακριβώς την απαίτηση.
- Αν μια πραγματική υλοποίηση είναι ήδη απλή, γρήγορη και προβλέψιμη, μπορούμε να τη χρησιμοποιήσουμε απευθείας.
Ας δούμε τι σημαίνουν αυτά, πάνω στο ίδιο παράδειγμα. Ο κώδικας χρησιμοποιεί Python 3.10+ και το ενσωματωμένο unittest, χωρίς επιπλέον βιβλιοθήκες. Οι αρχές εφαρμόζονται και σε άλλες γλώσσες και frameworks.
Τι ελέγχει ένα unit test
Ένα unit test ελέγχει μια συγκεκριμένη συμπεριφορά μιας μικρής μονάδας λογισμικού.
Η μονάδα μπορεί να είναι μια συνάρτηση, μια κλάση ή ένα μικρό σύνολο αντικειμένων που συνεργάζονται. Δεν υπάρχει ένας καθολικά αποδεκτός κανόνας ότι «ένα unit test επιτρέπεται να εκτελεί μόνο μία κλάση». Ο Martin Fowler περιγράφει τόσο tests με πραγματικούς συνεργάτες όσο και tests που τους αντικαθιστούν με test doubles. Unit Test — Martin Fowler
Για παράδειγμα, σε μια εφαρμογή διαχείρισης εργασιών, διαφορετικές συμπεριφορές είναι:
- Μια ανολοκλήρωτη εργασία γίνεται ολοκληρωμένη.
- Η νέα κατάσταση αποθηκεύεται.
- Στέλνεται ειδοποίηση ολοκλήρωσης.
- Μια εργασία που δεν υπάρχει προκαλεί συγκεκριμένο σφάλμα.
- Η επανάληψη της ενέργειας δεν στέλνει δεύτερη ειδοποίηση.
Παρότι όλες μπορεί να σχετίζονται με μία μέθοδο, αξίζουν ξεχωριστά, εστιασμένα tests.
Ένα καλό test περιγράφει ποια απαίτηση ισχύει και υπό ποιες συνθήκες.
Η οικογένεια των test doubles
Το test double είναι ο γενικός όρος για ένα αντικείμενο που αντικαθιστά κάποιο άλλο κατά τη διάρκεια ενός test.
Δεν είναι όλα τα test doubles mocks.
Η παρακάτω ορολογία ακολουθεί τη διάκριση που παρουσιάζει ο Fowler, βασισμένος στο έργο του Gerard Meszaros. Mocks Aren’t Stubs
| Είδος | Ρόλος | Απλό παράδειγμα |
|---|---|---|
| Dummy | Συμπληρώνει μια παράμετρο και δεν χρησιμοποιείται στο συγκεκριμένο test. | Ένα αντικείμενο που απαιτεί ο constructor, αλλά δεν καλείται. |
| Stub | Δίνει προκαθορισμένες απαντήσεις. | «Όταν ζητηθεί η εργασία, επέστρεψε αυτή την τιμή». |
| Fake | Παρέχει μια απλοποιημένη λειτουργική υλοποίηση. | Repository που αποθηκεύει αντικείμενα σε ένα dictionary. |
| Spy | Καταγράφει κλήσεις ώστε να τις εξετάσουμε. | Καταγραφέας των ειδοποιήσεων που ζητήθηκαν. |
| Mock | Χρησιμοποιείται για επαλήθευση προσδοκιών σχετικά με κλήσεις. | «Η ειδοποίηση πρέπει να κληθεί μία φορά με αυτό το ID». |
Οι ονομασίες των βιβλιοθηκών δεν αντιστοιχούν πάντα ακριβώς σε αυτούς τους ρόλους.
Ένα αντικείμενο που δημιουργήθηκε από το unittest.mock μπορεί να χρησιμοποιείται μόνο ως stub. Αν απλώς επιστρέφει μια τιμή, κάνουμε stubbing. Αν επαληθεύουμε πώς κλήθηκε, κάνουμε interaction verification.
Αντίστοιχα, ένα χειροποίητο αντικείμενο που καταγράφει κλήσεις μπορεί να λειτουργεί ως spy.
Ο ρόλος μέσα στο test έχει περισσότερη σημασία από τον τρόπο δημιουργίας του αντικειμένου.
Η βασική διαφορά Fake και Mock
Ας υποθέσουμε ότι ολοκληρώνουμε μια εργασία.
Με fake repository μπορούμε να ελέγξουμε:
Μετά την εκτέλεση, το repository επιστρέφει την εργασία ως ολοκληρωμένη.
Με mock repository μπορούμε να ελέγξουμε:
Το use case κάλεσε τη μέθοδο αποθήκευσης με την ολοκληρωμένη εργασία.
Στην πρώτη περίπτωση εξετάζουμε την κατάσταση που προέκυψε, δηλαδή κάνουμε state verification. Στη δεύτερη εξετάζουμε μια αλληλεπίδραση, δηλαδή κάνουμε interaction verification.
Αυτά είναι διαφορετικά αποδεικτικά στοιχεία.
Το fake repository μάς επιτρέπει να αποθηκεύσουμε και μετά να διαβάσουμε δεδομένα. Το mock repository καταγράφει ότι ζητήθηκε μια αποθήκευση.
Κανένα από τα δύο, από μόνο του, δεν αποδεικνύει ότι λειτουργεί το πραγματικό SQL query.
Επίσης, οι άξονες δεν είναι απόλυτοι: ένα test μπορεί να χρησιμοποιεί fake και ταυτόχρονα να ελέγχει αλληλεπιδράσεις σε έναν δεύτερο συνεργάτη.
Το παράδειγμα που θα χρησιμοποιήσουμε
Θα υλοποιήσουμε το use case CompleteTask.
Οι απαιτήσεις του είναι:
- Φορτώνει την εργασία.
- Αν δεν υπάρχει, σηκώνει
TaskNotFound. - Αν είναι ήδη ολοκληρωμένη, την επιστρέφει χωρίς νέα ειδοποίηση.
- Διαφορετικά, την ολοκληρώνει και την αποθηκεύει.
- Μετά την επιτυχημένη αποθήκευση, ζητά την αποστολή ειδοποίησης.
Για το repository ορίζουμε ένα μικρό συμβόλαιο:
- Το
get()επιστρέφειNoneόταν το ID δεν υπάρχει. - Το
save()δημιουργεί ή αντικαθιστά την εργασία με το συγκεκριμένο ID. - Μια αποτυχία διαθεσιμότητας αποθήκευσης μπορεί να εκφραστεί ως
RepositoryUnavailable.
Θα χρησιμοποιήσουμε δύο αρχεία:
tasks.py test_tasks.py Όλα τα blocks των tests που ακολουθούν μπαίνουν διαδοχικά στο ίδιο test_tasks.py.
Ο κώδικας της εφαρμογής
Στο tasks.py:
from dataclasses import dataclass, replace from typing import Protocol @dataclass(frozen=True) class Task: id: str completed: bool = False def complete(self) -> "Task": return replace(self, completed=True) class TaskNotFound(Exception): pass class RepositoryUnavailable(Exception): pass class TaskRepository(Protocol): def get(self, task_id: str) -> Task | None: ... def save(self, task: Task) -> None: ... class CompletionNotifier(Protocol): def send(self, task_id: str) -> None: ... class CompleteTask: def __init__( self, repository: TaskRepository, notifier: CompletionNotifier, ) -> None: self._repository = repository self._notifier = notifier def execute(self, task_id: str) -> Task: task = self._repository.get(task_id) if task is None: raise TaskNotFound(task_id) if task.completed: return task completed_task = task.complete() self._repository.save(completed_task) self._notifier.send(completed_task.id) return completed_task Το Task εκφράζει την κατάσταση και τη μετάβαση ολοκλήρωσης.
Το CompleteTask συντονίζει τη φόρτωση, την αποθήκευση και την ειδοποίηση.
Τα TaskRepository και CompletionNotifier περιγράφουν τις δυνατότητες που χρειάζεται το use case. Δεν περιέχουν λεπτομέρειες SQL, HTTP ή κάποιου SDK.
Το Protocol επιτρέπει να περιγράψουμε αυτή τη δομή χωρίς να απαιτούμε κοινή βασική κλάση από κάθε υλοποίηση. Οι δηλώσεις τύπων δεν επιβάλλουν από μόνες τους το επιχειρησιακό συμβόλαιο κατά την εκτέλεση.
Πρώτα δοκιμάζουμε τον απλό κώδικα χωρίς doubles
Η ολοκλήρωση του ίδιου του Task δεν χρειάζεται repository ή υπηρεσία ειδοποιήσεων.
Στην αρχή του test_tasks.py βάζουμε τα imports:
import unittest from collections.abc import Iterable from unittest.mock import create_autospec from tasks import ( CompleteTask, CompletionNotifier, RepositoryUnavailable, Task, TaskNotFound, TaskRepository, ) Και προσθέτουμε:
class TaskTest(unittest.TestCase): def test_task_can_be_completed_without_test_doubles(self): task = Task("task-1") result = task.complete() self.assertEqual(Task("task-1", completed=True), result) self.assertFalse(task.completed) Το test ελέγχει την πραγματική υλοποίηση της συμπεριφοράς.
Ελέγχει επίσης ότι το αρχικό αντικείμενο παραμένει ανολοκλήρωτο: η μέθοδος επιστρέφει νέο αντικείμενο.
Δεν χρειάζεται να αντικαταστήσουμε με mock κάτι που είναι ήδη εύκολο να χρησιμοποιήσουμε.
Ο ίδιος έλεγχος με Fake
Για να ελέγξουμε το use case χωρίς πραγματική βάση, δημιουργούμε ένα repository που κρατά δεδομένα στη μνήμη.
Στο test_tasks.py:
class InMemoryTaskRepository: def __init__(self, tasks: Iterable[Task] = ()) -> None: self._tasks = {task.id: task for task in tasks} def get(self, task_id: str) -> Task | None: return self._tasks.get(task_id) def save(self, task: Task) -> None: self._tasks[task.id] = task Αυτό είναι fake επειδή έχει λειτουργική συμπεριφορά:
- Αποθηκεύει διαφορετικές εργασίες.
- Αναζητά με βάση το ID.
- Διατηρεί αλλαγές μεταξύ κλήσεων.
- Αντικαθιστά υπάρχουσες εγγραφές.
Δεν χρειάζεται να ορίσουμε ξεχωριστά την απάντηση κάθε κλήσης.
Χρειαζόμαστε επίσης έναν απλό συνεργάτη για τις ειδοποιήσεις:
class RecordingNotifier: def __init__(self) -> None: self.sent_task_ids: list[str] = [] def send(self, task_id: str) -> None: self.sent_task_ids.append(task_id) Ο RecordingNotifier είναι spy: καταγράφει τις κλήσεις που δέχεται. Δεν υλοποιεί πραγματική παράδοση ειδοποιήσεων.
Το test με το fake repository είναι:
class CompleteTaskWithFakeTest(unittest.TestCase): def test_completed_task_is_persisted(self): # Arrange repository = InMemoryTaskRepository([Task("task-1")]) notifier = RecordingNotifier() complete_task = CompleteTask(repository, notifier) # Act result = complete_task.execute("task-1") # Assert expected = Task("task-1", completed=True) self.assertEqual(expected, result) self.assertEqual(expected, repository.get("task-1")) def test_missing_task_raises(self): repository = InMemoryTaskRepository() notifier = RecordingNotifier() complete_task = CompleteTask(repository, notifier) with self.assertRaises(TaskNotFound): complete_task.execute("missing") self.assertEqual([], notifier.sent_task_ids) Το πρώτο test περιγράφει μια ολοκληρωμένη ακολουθία:
Υπάρχει μια ανολοκλήρωτη εργασία. Την ολοκληρώνω. Διαβάζω την αποθηκευμένη κατάστασή της και είναι ολοκληρωμένη.
Δεν καθορίζει πόσες φορές επιτρέπεται να γίνει ανάγνωση από το repository.
Αν η υλοποίηση αλλάξει εσωτερικά, αλλά διατηρήσει το αποτέλεσμα και το συμβόλαιο, το test έχει περισσότερες πιθανότητες να παραμείνει χρήσιμο χωρίς αλλαγές.
Μια σημαντική παγίδα με mutable αντικείμενα
Το παράδειγμα χρησιμοποιεί Task με frozen=True και πεδία που είναι αμετάβλητες τιμές.
Αυτό αποτρέπει μια συνηθισμένη παγίδα των fake repositories.
Αν το repository επέστρεφε το ίδιο mutable αντικείμενο που κρατά εσωτερικά, ο κώδικας θα μπορούσε να κάνει:
task.completed = True Η αλλαγή θα φαινόταν αμέσως μέσα στο fake, ακόμη και αν ξεχνούσαμε να καλέσουμε save().
Έτσι, ένα test θα μπορούσε να περνά ενώ η πραγματική αποθήκευση δεν θα λειτουργούσε.
Στο δικό μας παράδειγμα δημιουργείται νέο Task. Αν αφαιρέσουμε το save(), το fake συνεχίζει να περιέχει το παλιό αντικείμενο και το test αποτυγχάνει.
Η συμπεριφορά του fake πρέπει να σχεδιάζεται με προσοχή.
Ο ίδιος έλεγχος με Mock
Ας αντικαταστήσουμε τώρα το repository με ένα mock.
Στο test_tasks.py προσθέτουμε:
class CompleteTaskWithMockTest(unittest.TestCase): def test_completed_task_is_saved(self): # Arrange repository = create_autospec( TaskRepository, instance=True, spec_set=True ) repository.get.return_value = Task("task-1") notifier = create_autospec( CompletionNotifier, instance=True, spec_set=True ) complete_task = CompleteTask(repository, notifier) # Act result = complete_task.execute("task-1") # Assert expected = Task("task-1", completed=True) self.assertEqual(expected, result) repository.save.assert_called_once_with(expected) def test_missing_task_does_not_save_or_notify(self): repository = create_autospec( TaskRepository, instance=True, spec_set=True ) repository.get.return_value = None notifier = create_autospec( CompletionNotifier, instance=True, spec_set=True ) complete_task = CompleteTask(repository, notifier) with self.assertRaises(TaskNotFound): complete_task.execute("missing") repository.save.assert_not_called() notifier.send.assert_not_called() def test_failed_save_does_not_notify(self): repository = create_autospec( TaskRepository, instance=True, spec_set=True ) repository.get.return_value = Task("task-1") repository.save.side_effect = RepositoryUnavailable() notifier = create_autospec( CompletionNotifier, instance=True, spec_set=True ) complete_task = CompleteTask(repository, notifier) with self.assertRaises(RepositoryUnavailable): complete_task.execute("task-1") notifier.send.assert_not_called() Εδώ η γραμμή:
repository.get.return_value = Task("task-1") κάνει stubbing: ορίζει μια απάντηση.
Η γραμμή:
repository.save.assert_called_once_with(expected) κάνει interaction verification: ελέγχει μια κλήση.
Το mock δεν αποθηκεύει την εργασία σε κάποιο λειτουργικό αποθετήριο. Καταγράφει ότι η save() κλήθηκε με το αναμενόμενο αντικείμενο.
Επίσης, το assert_called_once_with() προσθέτει την απαίτηση της μίας κλήσης. Αν το μόνο που ενδιαφέρει το προϊόν είναι η τελική κατάσταση, αυτό μπορεί να είναι αυστηρότερος περιορισμός από όσο χρειάζεται.
Αυτός είναι ένας λόγος να προτιμήσουμε το fake για τον συγκεκριμένο έλεγχο αποθήκευσης.
Γιατί χρησιμοποιούμε autospec και spec_set
Το create_autospec() δημιουργεί doubles με βάση το API που δίνουμε και ελέγχει τις υπογραφές των κλήσεων.
Το spec_set=True περιορίζει την πρόσβαση και την ανάθεση σε γνωστά attributes.
Έτσι, ένα λάθος όνομα μεθόδου ή λανθασμένος αριθμός παραμέτρων είναι πιθανότερο να αποκαλυφθεί άμεσα. Δεν επαληθεύονται, όμως, οι επιχειρησιακοί κανόνες ή οι τύποι επιστροφής από τα type hints. Τεκμηρίωση του unittest.mock
Το autospec μειώνει ορισμένα λάθη στη χρήση του API. Δεν αποδεικνύει ότι το double συμπεριφέρεται όπως ο πραγματικός adapter.
Πότε το mock είναι ιδιαίτερα βολικό
Το τρίτο test προκαλεί αποτυχία αποθήκευσης:
repository.save.side_effect = RepositoryUnavailable() Και ελέγχει μια σαφή απαίτηση:
Αν η αποθήκευση αποτύχει, δεν πρέπει να ζητηθεί ειδοποίηση ολοκλήρωσης.
Δεν χρειάζεται να προσθέσουμε στο fake repository μηχανισμούς αποτυχίας για κάθε πιθανό σενάριο.
Για έναν μεμονωμένο έλεγχο σφάλματος, ένα μικρό stub ή ένα mock με side_effect μπορεί να είναι η πιο απλή λύση.
Fake και Mock στο ίδιο test
Δεν χρειάζεται να διαλέξουμε μία τεχνική για ολόκληρη την εφαρμογή.
Μπορούμε να χρησιμοποιήσουμε:
- Fake repository για μια λειτουργική διαχείριση κατάστασης.
- Mock notifier για να ελέγξουμε την απαίτηση αποστολής ειδοποίησης.
Προσθέτουμε στο test_tasks.py:
class CompleteTaskHybridTest(unittest.TestCase): def test_completion_sends_one_notification(self): repository = InMemoryTaskRepository([Task("task-1")]) notifier = create_autospec( CompletionNotifier, instance=True, spec_set=True ) complete_task = CompleteTask(repository, notifier) complete_task.execute("task-1") notifier.send.assert_called_once_with("task-1") def test_repeated_completion_does_not_notify_again(self): repository = InMemoryTaskRepository([Task("task-1")]) notifier = create_autospec( CompletionNotifier, instance=True, spec_set=True ) complete_task = CompleteTask(repository, notifier) complete_task.execute("task-1") complete_task.execute("task-1") notifier.send.assert_called_once_with("task-1") Εδώ το mock ελέγχει κάτι που έχει άμεση επιχειρησιακή σημασία: να μη ζητηθεί δεύτερη ειδοποίηση για την ίδια ολοκλήρωση.
Το fake repository εξασφαλίζει ότι η δεύτερη εκτέλεση διαβάζει την κατάσταση που αποθήκευσε η πρώτη.
Δεν προγραμματίζουμε χειροκίνητα μια διαφορετική απάντηση για κάθε ανάγνωση. Η αλλαγή προκύπτει από τη λειτουργία του fake.
Το test αποδεικνύει τη συμπεριφορά για αυτές τις δύο διαδοχικές κλήσεις. Οι ταυτόχρονες κλήσεις είναι διαφορετικό σενάριο.
Τα Fakes χρειάζονται δικά τους tests
Ένα fake είναι επιπλέον κώδικας. Μπορεί να περιέχει bugs ή να αποκλίνει από την πραγματική υλοποίηση.
Χρειάζεται επομένως να ελέγχουμε το συμβόλαιό του.
Προσθέτουμε:
class InMemoryTaskRepositoryContractTest(unittest.TestCase): def setUp(self): self.repository = InMemoryTaskRepository() def test_unknown_id_returns_none(self): self.assertIsNone(self.repository.get("missing")) def test_saved_task_can_be_read(self): self.repository.save(Task("task-1")) self.assertEqual(Task("task-1"), self.repository.get("task-1")) def test_save_replaces_task_with_same_id(self): self.repository.save(Task("task-1")) self.repository.save(Task("task-1", completed=True)) self.assertEqual( Task("task-1", completed=True), self.repository.get("task-1"), ) Αυτά ελέγχουν τρία σημεία του συμβολαίου μας:
- Την απουσία εγγραφής.
- Την ανάγνωση αποθηκευμένης εγγραφής.
- Την αντικατάσταση εγγραφής με ίδιο ID.
Προς το παρόν εκτελούνται μόνο στο fake.
Όταν υπάρχει πραγματικός adapter, μπορούμε να οργανώσουμε αυτούς τους ίδιους ελέγχους ώστε να εκτελούνται και στις δύο υλοποιήσεις, με κατάλληλο setup και καθαρισμό.
Αυτή είναι η αξία των κοινών contract tests: ελέγχουν ότι οι υλοποιήσεις συμφωνούν στις συμπεριφορές που υπόσχεται το API. Δεν απαιτούν από το fake να αναπαράγει κάθε λεπτομέρεια της βάσης. Software Engineering at Google — Test Doubles
Για παράδειγμα, ένα dictionary δεν αποδεικνύει από μόνο του σωστή συμπεριφορά συναλλαγών, constraints ή ταυτόχρονων ενημερώσεων.
Αυτά χρειάζονται ελέγχους με την κατάλληλη πραγματική υποδομή.
Εκτέλεση των παραδειγμάτων
Αφού τοποθετήσουμε όλα τα προηγούμενα blocks στα δύο αρχεία, εκτελούμε από τον φάκελό τους:
python -m unittest -v Η ολοκληρωμένη σουίτα περιλαμβάνει 11 tests.
Μπορούμε επίσης να δοκιμάσουμε μικρές σκόπιμες αλλαγές:
- Αν αφαιρέσουμε το
save(), αποτυγχάνουν τόσο το test αποθήκευσης με fake όσο και εκείνο με mock. - Αν αφαιρέσουμε το
send(), αποτυγχάνει το test ειδοποίησης. - Αν αφαιρέσουμε τον έλεγχο
task.completed, αποτυγχάνει το test της επανάληψης.
Αυτή είναι μια χρήσιμη ερώτηση για κάθε test:
Ποιο συγκεκριμένο λάθος στον κώδικα θα έκανε αυτό το test να αποτύχει;
Πότε προτιμάμε κάθε προσέγγιση
| Ανάγκη | Συνήθως κατάλληλη επιλογή | Γιατί |
|---|---|---|
| Απλός υπολογισμός ή domain object | Πραγματική υλοποίηση | Έχει μικρό κόστος και ελέγχουμε τον πραγματικό κώδικα. |
| Αποθήκευση και ανάγνωση μέσα σε ένα σενάριο | Fake | Διατηρεί λειτουργική κατάσταση μεταξύ κλήσεων. |
| Πολλά tests χρειάζονται το ίδιο απλό repository | Κοινό fake | Μειώνει επαναλαμβανόμενο setup. |
| Μία συγκεκριμένη απάντηση από dependency | Stub | Εκφράζει απλά τη συνθήκη του σεναρίου. |
| Έλεγχος ότι ζητήθηκε ειδοποίηση | Mock ή spy | Η αλληλεπίδραση αποτελεί μέρος της απαίτησης. |
| Μεμονωμένη αποτυχία μιας εξάρτησης | Stub ή mock με σφάλμα | Το σενάριο ρυθμίζεται χωρίς πολύπλοκο fake. |
| SQL, mappings, constraints, συναλλαγές | Integration test | Χρειάζεται να εκτελεστεί ο πραγματικός adapter. |
| Ολόκληρη διαδρομή χρήστη | Επιλεγμένο end-to-end test | Ελέγχει τη σύνδεση των επιμέρους τμημάτων. |
Ο πίνακας είναι πρακτικός οδηγός. Η τελική επιλογή εξαρτάται από το συμβόλαιο και το κόστος του test.
Ένα fake έχει αρχικό κόστος κατασκευής και συντήρησης. Συμφέρει περισσότερο όταν η απλή συμπεριφορά του επαναχρησιμοποιείται σε αρκετά σενάρια.
Ένα mock έχει μικρό αρχικό κόστος. Αν όμως κάθε test χρειάζεται μεγάλη λίστα ρυθμίσεων και επαληθεύσεων, το συνολικό κόστος συντήρησης αυξάνεται.
Η Google επισημαίνει ότι η υπερβολική χρήση mocks μπορεί να δυσκολεύει την κατανόηση και το refactoring, επειδή τα tests αρχίζουν να γνωρίζουν πολλές λεπτομέρειες της υλοποίησης. Don’t Overuse Mocks
Πώς συνδέεται αυτό με την Clean Architecture
Στην Clean Architecture οι εξαρτήσεις του πηγαίου κώδικα κατευθύνονται προς τις εσωτερικές πολιτικές και τους κανόνες της εφαρμογής.
Το use case δεν χρειάζεται να γνωρίζει αν η αποθήκευση γίνεται σε PostgreSQL, σε αρχείο ή μέσω κάποιου εξωτερικού API. Χρειάζεται ένα συμβόλαιο που εκφράζει τις δικές του ανάγκες. The Clean Architecture — Robert C. Martin
Μια πιθανή οργάνωση είναι:
domain/ task.py application/ complete_task.py ports.py errors.py infrastructure/ postgres_task_repository.py email_completion_notifier.py tests/ unit/ integration/ Στο άρθρο κρατήσαμε τον κώδικα σε δύο αρχεία για να είναι εύκολος στην εκτέλεση. Δεν χρειάζονται πολλοί φάκελοι για να καταλάβουμε την αρχή.
Οι εξαρτήσεις του πηγαίου κώδικα μπορούν να φαίνονται ως εξής:
CompleteTask ------------> TaskRepository PostgresTaskRepository -> TaskRepository InMemoryTaskRepository -> TaskRepository Το CompleteTask χρησιμοποιεί το port.
Ο πραγματικός adapter και το fake υλοποιούν τη συμπεριφορά που περιγράφει το port. Στην Python αυτή η συμμόρφωση μπορεί να είναι δομική, χωρίς ρητή κληρονομικότητα.
Στην παραγωγή συνδέουμε το use case με τον πραγματικό adapter. Στα unit tests το συνδέουμε με το κατάλληλο double.
Αυτή η σύνδεση μέσω constructor είναι ...
