Mock — είναι ένα υποκατάστατο αντικείμενο που μιμείται τη συμπεριφορά ενός πραγματικού στοιχείου και επιτρέπει τον έλεγχο της αλληλεπίδρασης με αυτό. Σε αντίθεση με το Stub, το οποίο απλώς επιστρέφει μια καθορισμένη τιμή, το Mock καταγράφει το γεγονός της κλήσης της μεθόδου, τα ορίσματα που μεταδόθηκαν και τον αριθμό των κλήσεων. Σύμφωνα με στοιχεία του Mockito (2024), το Mock είναι ο πιο δημοφιλής τύπος Test Double σε έργα Java και Kotlin, που χρησιμοποιείται σε περισσότερο από το 70% των δοκιμών μονάδας εφαρμογών για κινητά.
Κύρια σημεία
Mock — είναι ένα αντικείμενο που δημιουργείται από το πλαίσιο mocking (Mockito, MockK, EasyMock), το οποίο μιμείται μια διεπαφή ή κλάση και καταγράφει όλες τις κλήσεις των μεθόδων του. Ο προγραμματιστής ορίζει προσδοκίες: η μέθοδος X θα κληθεί με ορίσματα Y και θα επιστρέψει Z. Μετά την εκτέλεση της δοκιμής, το Mock ελέγχει αν οι προσδοκίες ταιριάζουν με τις πραγματικές κλήσεις.
Ο όρος προέρχεται από τη θεατρική μεταφορά των Test Doubles: το Mock είναι ένας “μιμητής” που δεν στέκεται απλώς στη σκηνή (όπως το Dummy), αλλά παίζει έναν ρόλο και ελέγχει αν η αλληλεπίδραση μαζί του ήταν σωστή. Αν ο ελεγχόμενος κώδικας δεν κάλεσε τη μέθοδο που περίμενε το Mock, ή την κάλεσε με λανθασμένα ορίσματα — η δοκιμή αποτυγχάνει με μήνυμα παραβιασμένης προσδοκίας.
Το Mock δημιουργείται μέσω του εργοστασίου του πλαισίου: mockk<MyInterface>() ή Mockito.mock(MyClass.java). Το πλαίσιο δημιουργεί ένα αντικείμενο proxy που παρεμποδίζει όλες τις κλήσεις μεθόδων. Κάθε κλήση συγκρίνεται με προκαθορισμένες προσδοκίες (expectations). Αν η κλήση αντιστοιχεί στην προσδοκία — επιστρέφεται η καθορισμένη τιμή. Αν όχι — το Mock επιστρέφει μια προεπιλεγμένη τιμή ή ρίχνει μια εξαίρεση, ανάλογα με τη διαμόρφωση.
Mock είναι υποχρεωτικό όταν ο ελεγχόμενος κώδικας αλληλεπιδρά με στοιχεία που έχουν παρενέργειες: αποστολή δεδομένων στον διακομιστή, εγγραφή στη βάση δεδομένων, καταγραφή, αναλυτικά στοιχεία, πλοήγηση, εμφάνιση συστημικών διαλόγων. Χωρίς Mock, αυτές οι αλληλεπιδράσεις δεν μπορούν να επαληθευτούν χωρίς εκκίνηση της πραγματικής υποδομής. Σύμφωνα με το Google Testing Blog, το Mock είναι ο μόνος τρόπος για να ελεγχθεί ότι η εφαρμογή έστειλε πραγματικά ένα συμβάν αναλυτικής χωρίς ανύψωση δοκιμαστικού διακομιστή.
Η διαφορά μεταξύ Mock και Stub είναι ένα από τα πιο συζητημένα θέματα στις δοκιμές. Και οι δύο τύποι αντικαθιστούν την πραγματική εξάρτηση, αλλά με θεμελιωδώς διαφορετικούς τρόπους.
| Κριτήριο | Mock | Stub |
|---|---|---|
| Κύρια ερώτηση | Κλήθηκε η μέθοδος; | Ποιο αποτέλεσμα επιστράφηκε; |
| Επαλήθευση | Συμπεριφοράς (verify) | Κατάστασης (assert) |
| Επιστροφή δεδομένων | Προαιρετικά | Υποχρεωτικά |
| Παράδειγμα | verify(analytics).logEvent("click") | assertEquals(5, repository.getCount()) |
| Πότε να χρησιμοποιείται | Παρενέργειες | Επιστροφή δεδομένων |
Ένα απλό τεστ για επιλογή: αναρωτηθείτε — “αν διαγράψω αυτή τη γραμμή κώδικα, θα αποτύχει η δοκιμή;”. Αν η δοκιμή ελέγχει την επιστρεφόμενη τιμή — χρειάζεται Stub (έλεγχος μέσω assert). Αν η δοκιμή ελέγχει αν ο κώδικας κάλεσε τη μέθοδο με σωστά ορίσματα — χρειάζεται Mock (έλεγχος μέσω verify). Αυτή η διχοτομία προκύπτει από το πρότυπο Command-Query Separation: οι μέθοδοι που αλλάζουν κατάσταση (commands) χρειάζονται Mock; οι μέθοδοι που επιστρέφουν δεδομένα (queries) χρειάζονται Stub.
Η επιλογή μεταξύ Mockito και MockK είναι μία από τις πρώτες αποφάσεις κατά τη ρύθμιση της στοίβας δοκιμών ενός έργου Android σε Kotlin. Και οι δύο βιβλιοθήκες εκτελούν την ίδια εργασία, αλλά με διαφορετική προσέγγιση στα ειδικά χαρακτηριστικά του Kotlin.
Mockito — είναι το de facto πρότυπο για έργα Java. Η έκδοση 5.x υποστηρίζει αντικείμενα mock για τελικές κλάσεις, στατικές μεθόδους και κατασκευαστές χάρη στον ενσωματωμένο MockMaker. Για έργα Kotlin, το Mockito απαιτεί πρόσθετη διαμόρφωση: επεκτάσεις mockito-kotlin για βελτιωμένη σύνταξη, mockito-inline για τελικές κλάσεις. Το Mockito δεν υποστηρίζει coroutine Kotlin και συναρτήσεις suspend χωρίς πρόσθετους προσαρμογείς.
MockK δημιουργήθηκε ειδικά για Kotlin. Υποστηρίζει εγγενώς coroutine (coEvery, coVerify), sealed class, data class, object-singleton και συναρτήσεις επέκτασης. Η σύνταξη του MockK χρησιμοποιεί DSL με μπλοκ λάμδα, που φαίνεται φυσική σε κώδικα Kotlin. Το MockK μπορεί επίσης να κάνει mock ιδιότητες (property mocking) χωρίς πρόσθετη διαμόρφωση — αυτό είναι σημαντικό για έργα Android που χρησιμοποιούν LiveData, StateFlow και Delegates.
// Mockito + mockito-kotlin
val repository = mock<UserRepository>()
whenever(repository.getUser(1)).thenReturn(user)
// MockK
val repository = mockk<UserRepository>()
every { repository.getUser(1) } returns user
Οι συγκρίσεις επιδόσεων (JVM Benchmark, 2024) δείχνουν ότι το MockK δημιουργεί αντικείμενα mock 15–20% ταχύτερα από το Mockito για έργα Kotlin χάρη στην άμεση εργασία με bytecode Kotlin, όχι Java Reflections. Για έργα με χιλιάδες δοκιμές μονάδας, η διαφορά στον χρόνο δημιουργίας μπορεί να είναι αισθητή: το MockK εξοικονομεί 30–60 δευτερόλεπτα στην πλήρη εκτέλεση δοκιμών σε μεγάλα έργα.
Θα εξετάσουμε τρία σενάρια: δοκιμή ViewModel με εξαρτήσεις Mock, δοκιμή UseCase με επαλήθευση κλήσης API και δοκιμή coroutine με coVerify.
class ProfileViewModelTest {
private val analytics = mockk<AnalyticsService>()
private val repo = mockk<UserRepository>()
private val vm = ProfileViewModel(repo, analytics)
fun `profile opened logs analytics event`() {
every { analytics.logEvent("profile_opened") } returns Unit
vm.onViewCreated()
verify { analytics.logEvent("profile_opened") }
}
}
class SendMessageUseCaseTest {
private val api = mockk<MessagingApi>()
private val useCase = SendMessageUseCase(api)
fun `send message with correct payload`() = runTest {
val message = Message(text = "Hello", userId = 42)
coEvery { api.sendMessage(any()) } returns MessageResult.Sent("msg_1")
val result = useCase.execute(message)
coVerify {
api.sendMessage(match {
it.text == "Hello" && it.userId == 42
})
}
assertTrue(result is MessageResult.Sent)
}
}
class OrderUseCaseTest {
private val api = mockk<OrderApi>()
private val useCase = OrderUseCase(api)
private val slot = slot<OrderRequest>()
fun `order request contains correct items`() = runTest {
coEvery { api.placeOrder(capture(slot)) } returns OrderResult.Placed("order_1")
useCase.execute(listOf("item_a", "item_b"))
assertEquals(2, slot.captured.items.size)
assertEquals("item_a", slot.captured.items[0])
}
}
Η αποτελεσματική χρήση του Mock στην ανάπτυξη κινητών απαιτεί πειθαρχία. Η παραβίαση αυτών των κανόνων μετατρέπει τις δοκιμές σε εύθραυστα εμπόδια που σπάνε σε κάθε αναδιάρθρωση.
Αυστηρός κανόνας: Το Mock δημιουργείται μόνο για εξαρτήσεις που διασχίζουν το όριο της εφαρμογής: πελάτες API, βάσεις δεδομένων, σύστημα αρχείων, συστημικές υπηρεσίες (LocationManager, BluetoothAdapter, Camera). Οι εσωτερικές κλάσεις της εφαρμογής — οντότητες τομέα, Value Object, απλά βοηθητικά προγράμματα — δεν πρέπει να αντικαθίστανται από Mock. Η συμπεριφορά τους δοκιμάζεται μέσω πραγματικών αντικειμένων.
Κάθε δοκιμή πρέπει να περιέχει ακριβώς έναν λογικό έλεγχο — είτε verify (για Mock), είτε assert (για Stub). Μην αναμιγνύετε τον έλεγχο κατάστασης και συμπεριφοράς σε μία δοκιμή. Αν πρέπει να ελεγχθούν τόσο η κλήση API όσο και το αποτέλεσμα — δημιουργήστε δύο ξεχωριστές δοκιμές με διαφορετικά ονόματα. Αυτός ο κανόνας, γνωστός ως “ένα assert ανά δοκιμή”, προέρχεται από τις συστάσεις του Kent Beck (2002).
Εκτός από τη βασική mocking, υπάρχουν προηγμένες τεχνικές που λύνουν συγκεκριμένες εργασίες στην ανάπτυξη κινητών: δοκιμή πολυνηματικότητας, έλεγχος κατάστασης Flow και μερική mocking πραγματικών αντικειμένων.
Spy (ή μερικό mock) επιτρέπει τη δημιουργία ενός αντικειμένου που αναθέτει κλήσεις στην πραγματική υλοποίηση, αλλά επιτρέπει την παράκαμψη μεμονωμένων μεθόδων. Στο MockK, το spyk δημιουργείται βάσει μιας πραγματικής παρουσίας της κλάσης: val repo = spyk(InMemoryUserRepository()). Οι κλήσεις για τις οποίες έχουν οριστεί προσδοκίες μέσω every περνούν μέσω Mock; οι υπόλοιπες — μέσω του πραγματικού αντικειμένου. Το Spy είναι ιδιαίτερα χρήσιμο για δοκιμή παλαιού κώδικα όπου η έγχυση εξαρτήσεων δεν έχει ακόμη εφαρμοστεί και χρειάζεται να παρακαμφθεί μόνο μία μέθοδος.
Στα σύγχρονα έργα Android σε Jetpack Compose, το ViewModel εκθέτει την κατάσταση μέσω StateFlow. Το MockK επιτρέπει την mocking εξαρτήσεων Flow, και η βιβλιοθήκη Turbine απλοποιεί την επαλήθευση εκπομπής. Το κλασικό μοτίβο: MockK για UseCase που επιστρέφει Flow, Turbine για επαλήθευση εκπομπής ViewModel. Αυτή η στοίβα συνιστάται από την τεκμηρίωση Android Testing (Google, 2024) για έργα σε Kotlin Coroutines.
class SearchViewModelTest {
private val searchUseCase = mockk<SearchUseCase>()
private val vm = SearchViewModel(searchUseCase)
fun `search emits results`() = runTest {
coEvery { searchUseCase.search("android") } returns
flowOf(SearchResult.Success(listOf(Item("Android TDD"))))
vm.search("android")
vm.state.test {
val state = awaitItem()
assertTrue(state.items.isNotEmpty())
cancelAndIgnoreRemainingEvents()
}
}
}
Συχνές ερωτήσεις
Mock — είναι μια έννοια, ένας τύπος Test Double που ελέγχει τη συμπεριφορά. Mockito — είναι μια βιβλιοθήκη για τη δημιουργία αντικειμένων Mock σε Java και Android. Άλλες βιβλιοθήκες: MockK (Kotlin), EasyMock (Java), Cuckoo (iOS).
Για δοκιμή συναρτήσεων suspend με Mock, χρησιμοποιήστε MockK (coEvery / coVerify) ή Mockito με mockito-kotlin. Το MockK υποστηρίζει coroutine εγγενώς: το coEvery ορίζει τη συμπεριφορά της συνάρτησης suspend, το coVerify επαληθεύει την κλήση της εντός του coroutine. Όλες οι κλήσεις suspend πρέπει να εκτελούνται εντός του runTest (kotlinx-coroutines-test).
Ναι. Στο MockK για αυτό χρησιμοποιείται το returnsMany: every { api.getData() } returnsMany listOf(response1, response2). Στο Mockito — η αλυσίδα thenReturn(value1).thenReturn(value2). Αυτό είναι χρήσιμο για δοκιμή συμπεριφοράς σε διαδοχικές κλήσεις με διαφορετικές απαντήσεις.
Στο MockK χρησιμοποιήστε τον σχολιασμό @MockK με πεδίο relaxed = true και καλέστε clearMocks(mock) στη μέθοδο @After. Στο Mockito — Mockito.reset(mock). Βέλτιστη πρακτική: δημιουργήστε νέο Mock για κάθε δοκιμή μέσω @Before για να εξαλειφθεί η επιρροή μεταξύ δοκιμών.
Το MockK λειτουργεί σωστά με sealed class: every { useCase() } returns Result.Success(data). Το Mockito δεν υποστηρίζει άμεσα sealed class, απαιτώντας παρακάμψεις. Αυτός είναι ένας από τους λόγους για τους οποίους για έργα Kotlin συνιστάται το MockK αντί του Mockito.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης