Mock — τι είναι, αντικείμενα mock και βιβλιοθήκες για δοκιμές

Συγγραφέας: IT Sectr Δημοσιεύτηκε: 2026-04-10 Χρόνος ανάγνωσης: 8 λεπ

Mock — είναι ένα υποκατάστατο αντικείμενο που μιμείται τη συμπεριφορά ενός πραγματικού στοιχείου και επιτρέπει τον έλεγχο της αλληλεπίδρασης με αυτό. Σε αντίθεση με το Stub, το οποίο απλώς επιστρέφει μια καθορισμένη τιμή, το Mock καταγράφει το γεγονός της κλήσης της μεθόδου, τα ορίσματα που μεταδόθηκαν και τον αριθμό των κλήσεων. Σύμφωνα με στοιχεία του Mockito (2024), το Mock είναι ο πιο δημοφιλής τύπος Test Double σε έργα Java και Kotlin, που χρησιμοποιείται σε περισσότερο από το 70% των δοκιμών μονάδας εφαρμογών για κινητά.

Κύρια σημεία

  • Mock — αντικείμενο που ελέγχει την αλληλεπίδραση: ποιες μέθοδοι κλήθηκαν, με ποια ορίσματα και πόσες φορές
  • Mockito — η πιο δημοφιλής βιβλιοθήκη για τη δημιουργία Mock σε έργα Java και Android
  • MockK — εναλλακτική του Mockito για Kotlin με εγγενή υποστήριξη για coroutine και sealed class
  • Behavior verification — βασική διαφορά του Mock από το Stub: το Mock ελέγχει τη συμπεριφορά, όχι την κατάσταση
  • Over-mocking — κύριο Anti-Pattern: το mock πρέπει να είναι μόνο για εξωτερικές εξαρτήσεις

Τι είναι το Mock;

Mock — είναι ένα αντικείμενο που δημιουργείται από το πλαίσιο mocking (Mockito, MockK, EasyMock), το οποίο μιμείται μια διεπαφή ή κλάση και καταγράφει όλες τις κλήσεις των μεθόδων του. Ο προγραμματιστής ορίζει προσδοκίες: η μέθοδος X θα κληθεί με ορίσματα Y και θα επιστρέψει Z. Μετά την εκτέλεση της δοκιμής, το Mock ελέγχει αν οι προσδοκίες ταιριάζουν με τις πραγματικές κλήσεις.

Ο όρος προέρχεται από τη θεατρική μεταφορά των Test Doubles: το Mock είναι ένας “μιμητής” που δεν στέκεται απλώς στη σκηνή (όπως το Dummy), αλλά παίζει έναν ρόλο και ελέγχει αν η αλληλεπίδραση μαζί του ήταν σωστή. Αν ο ελεγχόμενος κώδικας δεν κάλεσε τη μέθοδο που περίμενε το Mock, ή την κάλεσε με λανθασμένα ορίσματα — η δοκιμή αποτυγχάνει με μήνυμα παραβιασμένης προσδοκίας.

Πώς λειτουργεί το Mock

Το Mock δημιουργείται μέσω του εργοστασίου του πλαισίου: mockk<MyInterface>() ή Mockito.mock(MyClass.java). Το πλαίσιο δημιουργεί ένα αντικείμενο proxy που παρεμποδίζει όλες τις κλήσεις μεθόδων. Κάθε κλήση συγκρίνεται με προκαθορισμένες προσδοκίες (expectations). Αν η κλήση αντιστοιχεί στην προσδοκία — επιστρέφεται η καθορισμένη τιμή. Αν όχι — το Mock επιστρέφει μια προεπιλεγμένη τιμή ή ρίχνει μια εξαίρεση, ανάλογα με τη διαμόρφωση.

Πότε είναι απαραίτητο το Mock

Mock είναι υποχρεωτικό όταν ο ελεγχόμενος κώδικας αλληλεπιδρά με στοιχεία που έχουν παρενέργειες: αποστολή δεδομένων στον διακομιστή, εγγραφή στη βάση δεδομένων, καταγραφή, αναλυτικά στοιχεία, πλοήγηση, εμφάνιση συστημικών διαλόγων. Χωρίς Mock, αυτές οι αλληλεπιδράσεις δεν μπορούν να επαληθευτούν χωρίς εκκίνηση της πραγματικής υποδομής. Σύμφωνα με το Google Testing Blog, το Mock είναι ο μόνος τρόπος για να ελεγχθεί ότι η εφαρμογή έστειλε πραγματικά ένα συμβάν αναλυτικής χωρίς ανύψωση δοκιμαστικού διακομιστή.

Mock και Stub: λεπτομερής σύγκριση

Η διαφορά μεταξύ Mock και Stub είναι ένα από τα πιο συζητημένα θέματα στις δοκιμές. Και οι δύο τύποι αντικαθιστούν την πραγματική εξάρτηση, αλλά με θεμελιωδώς διαφορετικούς τρόπους.

ΚριτήριοMockStub
Κύρια ερώτησηΚλήθηκε η μέθοδος;Ποιο αποτέλεσμα επιστράφηκε;
ΕπαλήθευσηΣυμπεριφοράς (verify)Κατάστασης (assert)
Επιστροφή δεδομένωνΠροαιρετικάΥποχρεωτικά
Παράδειγμαverify(analytics).logEvent("click")assertEquals(5, repository.getCount())
Πότε να χρησιμοποιείταιΠαρενέργειεςΕπιστροφή δεδομένων

Πρακτικός κανόνας: Mock ή όχι

Ένα απλό τεστ για επιλογή: αναρωτηθείτε — “αν διαγράψω αυτή τη γραμμή κώδικα, θα αποτύχει η δοκιμή;”. Αν η δοκιμή ελέγχει την επιστρεφόμενη τιμή — χρειάζεται Stub (έλεγχος μέσω assert). Αν η δοκιμή ελέγχει αν ο κώδικας κάλεσε τη μέθοδο με σωστά ορίσματα — χρειάζεται Mock (έλεγχος μέσω verify). Αυτή η διχοτομία προκύπτει από το πρότυπο Command-Query Separation: οι μέθοδοι που αλλάζουν κατάσταση (commands) χρειάζονται Mock; οι μέθοδοι που επιστρέφουν δεδομένα (queries) χρειάζονται Stub.

Mockito και MockK: σύγκριση βιβλιοθηκών

Η επιλογή μεταξύ Mockito και MockK είναι μία από τις πρώτες αποφάσεις κατά τη ρύθμιση της στοίβας δοκιμών ενός έργου Android σε Kotlin. Και οι δύο βιβλιοθήκες εκτελούν την ίδια εργασία, αλλά με διαφορετική προσέγγιση στα ειδικά χαρακτηριστικά του Kotlin.

Mockito: δοκιμασμένο κλασικό

Mockito — είναι το de facto πρότυπο για έργα Java. Η έκδοση 5.x υποστηρίζει αντικείμενα mock για τελικές κλάσεις, στατικές μεθόδους και κατασκευαστές χάρη στον ενσωματωμένο MockMaker. Για έργα Kotlin, το Mockito απαιτεί πρόσθετη διαμόρφωση: επεκτάσεις mockito-kotlin για βελτιωμένη σύνταξη, mockito-inline για τελικές κλάσεις. Το Mockito δεν υποστηρίζει coroutine Kotlin και συναρτήσεις suspend χωρίς πρόσθετους προσαρμογείς.

MockK: προσέγγιση Kotlin-first

MockK δημιουργήθηκε ειδικά για Kotlin. Υποστηρίζει εγγενώς coroutine (coEvery, coVerify), sealed class, data class, object-singleton και συναρτήσεις επέκτασης. Η σύνταξη του MockK χρησιμοποιεί DSL με μπλοκ λάμδα, που φαίνεται φυσική σε κώδικα Kotlin. Το MockK μπορεί επίσης να κάνει mock ιδιότητες (property mocking) χωρίς πρόσθετη διαμόρφωση — αυτό είναι σημαντικό για έργα Android που χρησιμοποιούν LiveData, StateFlow και Delegates.

kotlin
// 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 δευτερόλεπτα στην πλήρη εκτέλεση δοκιμών σε μεγάλα έργα.

Παραδείγματα δοκιμών Mock σε Kotlin

Θα εξετάσουμε τρία σενάρια: δοκιμή ViewModel με εξαρτήσεις Mock, δοκιμή UseCase με επαλήθευση κλήσης API και δοκιμή coroutine με coVerify.

Παράδειγμα 1: ViewModel με mock αναλυτικά

kotlin
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") }
    }
}

Παράδειγμα 2: UseCase με ασύγχρονη επαλήθευση

kotlin
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)
    }
}

Παράδειγμα 3: επαλήθευση ορισμάτων με ArgumentCaptor

kotlin
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 στην ανάπτυξη κινητών απαιτεί πειθαρχία. Η παραβίαση αυτών των κανόνων μετατρέπει τις δοκιμές σε εύθραυστα εμπόδια που σπάνε σε κάθε αναδιάρθρωση.

Mock μόνο εξωτερικά όρια της εφαρμογής

Αυστηρός κανόνας: Το Mock δημιουργείται μόνο για εξαρτήσεις που διασχίζουν το όριο της εφαρμογής: πελάτες API, βάσεις δεδομένων, σύστημα αρχείων, συστημικές υπηρεσίες (LocationManager, BluetoothAdapter, Camera). Οι εσωτερικές κλάσεις της εφαρμογής — οντότητες τομέα, Value Object, απλά βοηθητικά προγράμματα — δεν πρέπει να αντικαθίστανται από Mock. Η συμπεριφορά τους δοκιμάζεται μέσω πραγματικών αντικειμένων.

Ένα assert/verify ανά δοκιμή

Κάθε δοκιμή πρέπει να περιέχει ακριβώς έναν λογικό έλεγχο — είτε verify (για Mock), είτε assert (για Stub). Μην αναμιγνύετε τον έλεγχο κατάστασης και συμπεριφοράς σε μία δοκιμή. Αν πρέπει να ελεγχθούν τόσο η κλήση API όσο και το αποτέλεσμα — δημιουργήστε δύο ξεχωριστές δοκιμές με διαφορετικά ονόματα. Αυτός ο κανόνας, γνωστός ως “ένα assert ανά δοκιμή”, προέρχεται από τις συστάσεις του Kent Beck (2002).

  • Χρησιμοποιήστε relaxUnitFun = true στο MockK για μεθόδους που επιστρέφουν Unit — διαφορετικά το Mock θα ρίξει εξαίρεση σε μη περιγραφόμενη κλήση
  • Περιορίστε το verify μόνο σε κρίσιμες κλήσεις — μην επαληθεύετε κάθε getter και setter, αυτό καθιστά τις δοκιμές εύθραυστες
  • Εφαρμόστε τα ArgumentMatchers με νόημα — το any() κρύβει σημαντικές λεπτομέρειες αν το όρισμα είναι κρίσιμο για την επιχειρηματική λογική
  • Μην κάνετε κατάχρηση του verifyNoMoreInteractions — αυτή η μέθοδος καθιστά τη δοκιμή υπερβολικά άκαμπτη σε οποιεσδήποτε αλλαγές στον κώδικα παραγωγής
  • Χρησιμοποιήστε @MockkAnnotations για αυτόματη αρχικοποίηση αντικειμένων Mock — μειώνει το boilerplate και βελτιώνει την αναγνωσιμότητα

Προηγμένες τεχνικές δοκιμών Mock

Εκτός από τη βασική mocking, υπάρχουν προηγμένες τεχνικές που λύνουν συγκεκριμένες εργασίες στην ανάπτυξη κινητών: δοκιμή πολυνηματικότητας, έλεγχος κατάστασης Flow και μερική mocking πραγματικών αντικειμένων.

Μερικό Mock με spyK

Spy (ή μερικό mock) επιτρέπει τη δημιουργία ενός αντικειμένου που αναθέτει κλήσεις στην πραγματική υλοποίηση, αλλά επιτρέπει την παράκαμψη μεμονωμένων μεθόδων. Στο MockK, το spyk δημιουργείται βάσει μιας πραγματικής παρουσίας της κλάσης: val repo = spyk(InMemoryUserRepository()). Οι κλήσεις για τις οποίες έχουν οριστεί προσδοκίες μέσω every περνούν μέσω Mock; οι υπόλοιπες — μέσω του πραγματικού αντικειμένου. Το Spy είναι ιδιαίτερα χρήσιμο για δοκιμή παλαιού κώδικα όπου η έγχυση εξαρτήσεων δεν έχει ακόμη εφαρμοστεί και χρειάζεται να παρακαμφθεί μόνο μία μέθοδος.

Δοκιμή StateFlow με Turbine

Στα σύγχρονα έργα Android σε Jetpack Compose, το ViewModel εκθέτει την κατάσταση μέσω StateFlow. Το MockK επιτρέπει την mocking εξαρτήσεων Flow, και η βιβλιοθήκη Turbine απλοποιεί την επαλήθευση εκπομπής. Το κλασικό μοτίβο: MockK για UseCase που επιστρέφει Flow, Turbine για επαλήθευση εκπομπής ViewModel. Αυτή η στοίβα συνιστάται από την τεκμηρίωση Android Testing (Google, 2024) για έργα σε Kotlin Coroutines.

kotlin
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 από το Mockito;

Mock — είναι μια έννοια, ένας τύπος Test Double που ελέγχει τη συμπεριφορά. Mockito — είναι μια βιβλιοθήκη για τη δημιουργία αντικειμένων Mock σε Java και Android. Άλλες βιβλιοθήκες: MockK (Kotlin), EasyMock (Java), Cuckoo (iOS).

Πώς λειτουργεί το Mock με coroutine Kotlin;

Για δοκιμή συναρτήσεων suspend με Mock, χρησιμοποιήστε MockK (coEvery / coVerify) ή Mockito με mockito-kotlin. Το MockK υποστηρίζει coroutine εγγενώς: το coEvery ορίζει τη συμπεριφορά της συνάρτησης suspend, το coVerify επαληθεύει την κλήση της εντός του coroutine. Όλες οι κλήσεις suspend πρέπει να εκτελούνται εντός του runTest (kotlinx-coroutines-test).

Μπορεί το Mock να επιστρέφει διαφορετικές τιμές σε επαναλαμβανόμενες κλήσεις;

Ναι. Στο MockK για αυτό χρησιμοποιείται το returnsMany: every { api.getData() } returnsMany listOf(response1, response2). Στο Mockito — η αλυσίδα thenReturn(value1).thenReturn(value2). Αυτό είναι χρήσιμο για δοκιμή συμπεριφοράς σε διαδοχικές κλήσεις με διαφορετικές απαντήσεις.

Πώς να καθαρίσετε την κατάσταση Mock μεταξύ δοκιμών;

Στο MockK χρησιμοποιήστε τον σχολιασμό @MockK με πεδίο relaxed = true και καλέστε clearMocks(mock) στη μέθοδο @After. Στο MockitoMockito.reset(mock). Βέλτιστη πρακτική: δημιουργήστε νέο Mock για κάθε δοκιμή μέσω @Before για να εξαλειφθεί η επιρροή μεταξύ δοκιμών.

Πώς χειρίζεται το Mock την sealed class σε Kotlin;

Το MockK λειτουργεί σωστά με sealed class: every { useCase() } returns Result.Success(data). Το Mockito δεν υποστηρίζει άμεσα sealed class, απαιτώντας παρακάμψεις. Αυτός είναι ένας από τους λόγους για τους οποίους για έργα Kotlin συνιστάται το MockK αντί του Mockito.

Σύνοψη

  • Mock — τύπος Test Double που ελέγχει τη συμπεριφορά (verify), όχι την κατάσταση (assert) των εξαρτήσεων
  • Mockito — πρότυπο για Java/Android, MockK — επιλογή Kotlin-first με υποστήριξη coroutine και sealed class
  • Κύριος κανόνας: Mock για εξωτερικά όρια (δίκτυο, ΒΔ, συστημικές υπηρεσίες), πραγματικά αντικείμενα για εσωτερικές κλάσεις
  • Over-mocking — κύριο Anti-Pattern: η υπερβολική αντικατάσταση εξαρτήσεων καθιστά τις δοκιμές εύθραυστες και ελάχιστα χρήσιμες
  • Μία δοκιμή — ένας λογικός έλεγχος: verify για Mock ή assert για Stub, αλλά όχι και τα δύο σε μία δοκιμή
  • ArgumentCaptor / slot — ο σωστός τρόπος επαλήθευσης ορισμάτων κλήσης Mock αντί για τυφλό any()
  • Το MockK συνιστάται για έργα Kotlin: τα coEvery και coVerify λειτουργούν εγγενώς με coroutine χωρίς πρόσθετους προσαρμογείς

Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση

Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.

Συζήτηση έργου

Διαβάστε επίσης