<lambda>null1 package expo.modules.test.core
2
3 import android.content.Context
4 import android.os.Bundle
5 import androidx.test.core.app.ApplicationProvider
6 import com.facebook.react.bridge.ReactApplicationContext
7 import expo.modules.core.interfaces.services.EventEmitter
8 import expo.modules.kotlin.AppContext
9 import expo.modules.kotlin.ModuleHolder
10 import expo.modules.kotlin.modules.Module
11 import io.mockk.MockK
12 import io.mockk.MockKGateway
13 import io.mockk.every
14 import io.mockk.mockk
15 import io.mockk.spyk
16 import java.lang.ref.WeakReference
17 import java.lang.reflect.Proxy
18 import kotlin.reflect.KClass
19
20 /**
21 * This class shouldn't be used directly. Instead use
22 * ```kotlin
23 * ModuleMock.createMock(MyModuleTestInterface::class, MyModule()) {
24 * // module test code here
25 * }
26 * ```
27 */
28 data class ModuleMock<TestInterfaceType : Any, ModuleType : Module>(
29 val testInterface: TestInterfaceType,
30 val appContext: AppContext,
31 val eventEmitter: EventEmitter,
32 val moduleSpy: ModuleType
33 ) {
34 companion object {
35 /**
36 * This overload shouldn't be used directly. Instead use
37 * the inline overload with `block` being the last argument:
38 * `ModuleMock.createMock(..., block: (...) -> Unit)` instead
39 */
40 fun <TestInterfaceType : Any, ModuleType : Module> createMock(
41 moduleTestInterface: KClass<TestInterfaceType>,
42 module: ModuleType,
43 customAppContext: AppContext? = null,
44 customEventEmitter: EventEmitter? = null
45 ): ModuleMock<TestInterfaceType, ModuleType> {
46 val appContext = prepareMockAppContext(customAppContext)
47 val eventEmitter: EventEmitter = customEventEmitter ?: mockk(relaxed = true)
48
49 // prepare module spy
50 val moduleSpy = convertToSpy(module, recordPrivateCalls = true)
51 every { moduleSpy getProperty "appContext" } returns appContext
52 every { moduleSpy.sendEvent(any(), any<Bundle>()) } answers { call ->
53 val (eventName, eventBody) = call.invocation.args
54 eventEmitter.emit(eventName as String, eventBody as? Bundle)
55 }
56
57 val holder = ModuleHolder(moduleSpy)
58 val moduleControllerImpl = ModuleControllerImpl(holder)
59
60 val invocationHandler = ModuleMockInvocationHandler(
61 moduleTestInterface,
62 moduleControllerImpl,
63 holder
64 )
65 @Suppress("UNCHECKED_CAST")
66 return ModuleMock(
67 Proxy
68 .newProxyInstance(
69 moduleTestInterface.java.classLoader,
70 arrayOf(moduleTestInterface.java, ModuleController::class.java),
71 invocationHandler
72 ) as TestInterfaceType,
73 appContext,
74 eventEmitter,
75 moduleSpy
76 )
77 }
78
79 /**
80 * Executes the given [block] in the mocked module scope.
81 * Example usage:
82 * ```kotlin
83 * ModuleMock.createMock(MyModuleTestInterface::class, MyModule()) {
84 * every { moduleSpy.someModulePrivateFn() } returns 5
85 * val result = module.someFunctionAsync()
86 * assertEquals(result, 5)
87 * }
88 * ```
89 */
90 inline fun <TestInterfaceType : Any, ModuleType : Module> createMock(
91 moduleTestInterface: KClass<TestInterfaceType>,
92 module: ModuleType,
93 autoOnCreate: Boolean = true,
94 customAppContext: AppContext? = null,
95 customEventEmitter: EventEmitter? = null,
96 block: ModuleMockHolder<TestInterfaceType, ModuleType>.() -> Unit
97 ) {
98 val (mock, appContext, eventEmitter, moduleSpy) = createMock(
99 moduleTestInterface,
100 module,
101 customAppContext,
102 customEventEmitter
103 )
104 val controller = mock as ModuleController
105 val holder = ModuleMockHolder<TestInterfaceType, ModuleType>(
106 mock, controller, appContext, eventEmitter, moduleSpy
107 )
108
109 if (autoOnCreate) {
110 controller.onCreate()
111 }
112
113 block.invoke(holder)
114 }
115 }
116 }
117
prepareMockAppContextnull118 private fun prepareMockAppContext(customAppContext: AppContext?): AppContext {
119 val reactContext = ReactApplicationContext(ApplicationProvider.getApplicationContext<Context>())
120 val appContext = customAppContext ?: AppContext(
121 modulesProvider = mockk(relaxed = true),
122 legacyModuleRegistry = mockk(relaxed = true),
123 reactContextHolder = WeakReference(reactContext)
124 )
125
126 // as AppContext holds only weak reference to Android Context which can be destroyed too early
127 // we need to override it to return actual strong reference (held by mockk internals)
128 val appContextSpy = convertToSpy(appContext)
129 every { appContextSpy getProperty "reactContext" } returns reactContext
130 every { appContextSpy getProperty "hasActiveReactInstance" } returns true
131 return appContextSpy
132 }
133
134 /**
135 * Creates a spy from a given object or returns it as-is if it's already a spy
136 */
convertToSpynull137 private fun <T : Any> convertToSpy(obj: T, recordPrivateCalls: Boolean = false): T =
138 MockK.useImpl {
139 return@useImpl if (MockKGateway.implementation().mockTypeChecker.isSpy(obj)) {
140 obj
141 } else {
142 // this is actually spyk<T>(obj) but without syntax sugar
143 // because we're already inside MockK.useImpl { } which is part of that sugar
144 MockKGateway.implementation().mockFactory.spyk(
145 mockType = null, // this should be null if objToCopy is provided
146 objToCopy = obj,
147 name = null,
148 moreInterfaces = emptyArray(),
149 recordPrivateCalls = recordPrivateCalls
150 )
151 }
152 }
153