diff --git a/packages/react-native/ReactAndroid/build.gradle.kts b/packages/react-native/ReactAndroid/build.gradle.kts index fa579dab4ed7e3..2f31180ade13f1 100644 --- a/packages/react-native/ReactAndroid/build.gradle.kts +++ b/packages/react-native/ReactAndroid/build.gradle.kts @@ -664,6 +664,7 @@ dependencies { testImplementation(libs.junit) testImplementation(libs.assertj) testImplementation(libs.mockito) + testImplementation(libs.mockito.kotlin) testImplementation(libs.robolectric) testImplementation(libs.thoughtworks) } diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.kt index 1dc90c0b1221d8..6c3898125bde95 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.kt @@ -14,7 +14,6 @@ import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.JavaOnlyArray import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap import com.facebook.react.common.network.OkHttpCallUtil @@ -34,89 +33,68 @@ import org.junit.Before import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatchers.any -import org.mockito.ArgumentMatchers.eq -import org.mockito.Captor import org.mockito.MockedStatic -import org.mockito.Mockito.mock +import org.mockito.Mockito.RETURNS_MOCKS +import org.mockito.Mockito.mockConstruction import org.mockito.Mockito.mockStatic -import org.mockito.Mockito.times -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever +import org.mockito.kotlin.KArgumentCaptor +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.kotlin.withSettings import org.robolectric.RobolectricTestRunner -/** - * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when null is - * returned. - */ -private fun anyOrNull(type: Class): T = any(type) - -private val FORM = MediaType.get("multipart/form-data") - -/** - * Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException when - * null is returned. - */ -fun capture(argumentCaptor: ArgumentCaptor): T = argumentCaptor.capture() - +/** Tests [NetworkingModule] */ @RunWith(RobolectricTestRunner::class) class NetworkingModuleTest { + private lateinit var networkingModule: NetworkingModule private lateinit var httpClient: OkHttpClient private lateinit var context: ReactApplicationContext private lateinit var arguments: MockedStatic + private lateinit var okHttpCallUtil: MockedStatic private lateinit var requestBodyUtil: MockedStatic - - @Captor private lateinit var requestArgumentCaptor: ArgumentCaptor + private lateinit var requestArgumentCaptor: KArgumentCaptor @Before - fun prepareModules() { - httpClient = mock(OkHttpClient::class.java) - whenever(httpClient.cookieJar()).thenReturn(mock(CookieJarContainer::class.java)) - whenever(httpClient.newCall(anyOrNull(Request::class.java))).thenAnswer { - val callMock = mock(Call::class.java) - callMock - } + fun setUp() { + httpClient = mock() + whenever(httpClient.cookieJar()).thenReturn(mock()) + whenever(httpClient.newCall(any())).thenReturn(mock()) - val clientBuilder = mock(OkHttpClient.Builder::class.java) + val clientBuilder = mock() whenever(clientBuilder.build()).thenReturn(httpClient) whenever(httpClient.newBuilder()).thenReturn(clientBuilder) - context = mock(ReactApplicationContext::class.java) + context = mock() whenever(context.hasActiveReactInstance()).thenReturn(true) networkingModule = NetworkingModule(context, "", httpClient) arguments = mockStatic(Arguments::class.java) - arguments.`when`(Arguments::createArray).thenAnswer { JavaOnlyArray() } - arguments.`when`(Arguments::createMap).thenAnswer { JavaOnlyMap() } + arguments.`when` { Arguments.createArray() }.thenAnswer { JavaOnlyArray() } + arguments.`when` { Arguments.createMap() }.thenAnswer { JavaOnlyMap() } - requestBodyUtil = mockStatic(RequestBodyUtil::class.java) - requestBodyUtil - .`when` { - RequestBodyUtil.getFileInputStream(any(ReactContext::class.java), any(String::class.java)) - } - .thenReturn(mock(InputStream::class.java)) - requestBodyUtil - .`when` { - RequestBodyUtil.create(any(MediaType::class.java), any(InputStream::class.java)) - } - .thenReturn(mock(RequestBody::class.java)) + okHttpCallUtil = mockStatic(OkHttpCallUtil::class.java) + requestArgumentCaptor = argumentCaptor() + } + + private fun setupRequestBodyUtil() { + requestBodyUtil = + mockStatic(RequestBodyUtil::class.java, withSettings().defaultAnswer(RETURNS_MOCKS)) requestBodyUtil - .`when` { - RequestBodyUtil.createProgressRequest( - any(RequestBody::class.java), any(ProgressListener::class.java)) - } + .`when` { RequestBodyUtil.createProgressRequest(any(), any()) } .thenCallRealMethod() - - requestArgumentCaptor = ArgumentCaptor.forClass(Request::class.java) } @After fun tearDown() { arguments.close() - requestBodyUtil.close() + okHttpCallUtil.close() } @Test @@ -132,11 +110,14 @@ class NetworkingModuleTest { 0.0, /* timeout */ false /* withCredentials */) - verify(httpClient).newCall(capture(requestArgumentCaptor)) - assertThat(requestArgumentCaptor.value.url().toString()).isEqualTo("http://somedomain/foo") - // We set the User-Agent header by default - assertThat(requestArgumentCaptor.value.headers().size()).isEqualTo(1) - assertThat(requestArgumentCaptor.value.method()).isEqualTo("GET") + with(requestArgumentCaptor) { + verify(httpClient).newCall(capture()) + + assertThat(firstValue.url().toString()).isEqualTo("http://somedomain/foo") + // We set the User-Agent header by default + assertThat(firstValue.headers().size()).isEqualTo(1) + assertThat(firstValue.method()).isEqualTo("GET") + } } @Test @@ -193,12 +174,12 @@ class NetworkingModuleTest { } private fun verifyErrorEmit(context: ReactApplicationContext, requestId: Int) { - val captor = ArgumentCaptor.forClass(WritableArray::class.java) + val captor = argumentCaptor() verify(context).emitDeviceEvent(eq("didCompleteNetworkResponse"), captor.capture()) - val array = captor.value + val array = captor.firstValue assertThat(array.getInt(0)).isEqualTo(requestId) - assertThat(array.getString(1)).isNotNull + assertThat(array.getString(1)).isNotBlank } @Test @@ -217,15 +198,17 @@ class NetworkingModuleTest { 0.0, /* timeout */ false /* withCredentials */) - verify(httpClient).newCall(capture(requestArgumentCaptor)) - assertThat(requestArgumentCaptor.value.url().toString()).isEqualTo("http://somedomain/bar") - assertThat(requestArgumentCaptor.value.headers().size()).isEqualTo(2) - assertThat(requestArgumentCaptor.value.method()).isEqualTo("POST") - assertThat(requestArgumentCaptor.value.body()?.contentType()?.type()).isEqualTo("text") - assertThat(requestArgumentCaptor.value.body()?.contentType()?.subtype()).isEqualTo("plain") - val contentBuffer = Buffer() - requestArgumentCaptor.value.body()?.writeTo(contentBuffer) - assertThat(contentBuffer.readUtf8()).isEqualTo("This is request body") + with(requestArgumentCaptor) { + verify(httpClient).newCall(capture()) + assertThat(firstValue.url().toString()).isEqualTo("http://somedomain/bar") + assertThat(firstValue.headers().size()).isEqualTo(2) + assertThat(firstValue.method()).isEqualTo("POST") + assertThat(firstValue.body()?.contentType()?.type()).isEqualTo("text") + assertThat(firstValue.body()?.contentType()?.subtype()).isEqualTo("plain") + val contentBuffer = Buffer() + firstValue.body()?.writeTo(contentBuffer) + assertThat(contentBuffer.readUtf8()).isEqualTo("This is request body") + } } @Test @@ -246,11 +229,13 @@ class NetworkingModuleTest { 0.0, /* timeout */ false /* withCredentials */) - verify(httpClient).newCall(capture(requestArgumentCaptor)) - val requestHeaders = requestArgumentCaptor.value.headers() - assertThat(requestHeaders.size()).isEqualTo(2) - assertThat(requestHeaders["Accept"]).isEqualTo("text/plain") - assertThat(requestHeaders["User-Agent"]).isEqualTo("React test agent/1.0") + with(requestArgumentCaptor) { + verify(httpClient).newCall(capture()) + val requestHeaders = firstValue.headers() + assertThat(requestHeaders.size()).isEqualTo(2) + assertThat(requestHeaders["Accept"]).isEqualTo("text/plain") + assertThat(requestHeaders["User-Agent"]).isEqualTo("React test agent/1.0") + } } @Test @@ -269,10 +254,10 @@ class NetworkingModuleTest { 0.0, /* timeout */ false /* withCredentials */) - verify(httpClient).newCall(capture(requestArgumentCaptor)) + verify(httpClient).newCall(requestArgumentCaptor.capture()) // Verify okhttp does not append "charset=utf-8" - assertThat(requestArgumentCaptor.value.body()?.contentType().toString()) + assertThat(requestArgumentCaptor.firstValue.body()?.contentType().toString()) .isEqualTo("application/json") } @@ -294,10 +279,10 @@ class NetworkingModuleTest { 0.0, /* timeout */ false /* withCredentials */) - verify(httpClient).newCall(capture(requestArgumentCaptor)) + verify(httpClient).newCall(requestArgumentCaptor.capture()) val contentBuffer = Buffer() - requestArgumentCaptor.value.body()?.writeTo(contentBuffer) + requestArgumentCaptor.firstValue.body()?.writeTo(contentBuffer) assertThat(contentBuffer.readString(StandardCharsets.UTF_16)).isEqualTo(testString) } @@ -317,17 +302,18 @@ class NetworkingModuleTest { 0.0, /* timeout */ false /* withCredentials */) - verify(httpClient).newCall(capture(requestArgumentCaptor)) + verify(httpClient).newCall(requestArgumentCaptor.capture()) val contentBuffer = Buffer() - requestArgumentCaptor.value.body()?.writeTo(contentBuffer) + requestArgumentCaptor.firstValue.body()?.writeTo(contentBuffer) assertThat(contentBuffer.readString(StandardCharsets.UTF_8)).isEqualTo("test") - assertThat(requestArgumentCaptor.value.header("Content-Type")).isEqualTo("invalid") + assertThat(requestArgumentCaptor.firstValue.header("Content-Type")).isEqualTo("invalid") } @Test fun testMultipartPostRequestSimple() { + setupRequestBodyUtil() val body = JavaOnlyMap() val formData = JavaOnlyArray() val bodyPart = JavaOnlyMap() @@ -349,18 +335,22 @@ class NetworkingModuleTest { false /* withCredentials */) // verify url, method, headers - verify(httpClient).newCall(capture(requestArgumentCaptor)) - assertThat(requestArgumentCaptor.value.url().toString()).isEqualTo("http://someurl/uploadFoo") - assertThat(requestArgumentCaptor.value.method()).isEqualTo("POST") - assertThat(requestArgumentCaptor.value.body()?.contentType()?.type()).isEqualTo(FORM.type()) - assertThat(requestArgumentCaptor.value.body()?.contentType()?.subtype()) - .isEqualTo(FORM.subtype()) - val requestHeaders = requestArgumentCaptor.value.headers() - assertThat(requestHeaders.size()).isEqualTo(1) + with(requestArgumentCaptor) { + verify(httpClient).newCall(capture()) + assertThat(firstValue.url().toString()).isEqualTo("http://someurl/uploadFoo") + assertThat(firstValue.method()).isEqualTo("POST") + assertThat(firstValue.body()?.contentType()?.type()).isEqualTo(FORM.type()) + assertThat(firstValue.body()?.contentType()?.subtype()).isEqualTo(FORM.subtype()) + val requestHeaders = firstValue.headers() + assertThat(requestHeaders.size()).isEqualTo(1) + } + + requestBodyUtil.close() } @Test fun testMultipartPostRequestHeaders() { + setupRequestBodyUtil() val headers = listOf( JavaOnlyArray.of("Accept", "text/plain"), @@ -388,37 +378,37 @@ class NetworkingModuleTest { false /* withCredentials */) // verify url, method, headers - verify(httpClient).newCall(capture(requestArgumentCaptor)) - assertThat(requestArgumentCaptor.value.url().toString()).isEqualTo("http://someurl/uploadFoo") - assertThat(requestArgumentCaptor.value.method()).isEqualTo("POST") - assertThat(requestArgumentCaptor.value.body()?.contentType()?.type()).isEqualTo(FORM.type()) - assertThat(requestArgumentCaptor.value.body()?.contentType()?.subtype()) - .isEqualTo(FORM.subtype()) - val requestHeaders = requestArgumentCaptor.value.headers() - assertThat(requestHeaders.size()).isEqualTo(3) - assertThat(requestHeaders["Accept"]).isEqualTo("text/plain") - assertThat(requestHeaders["User-Agent"]).isEqualTo("React test agent/1.0") - assertThat(requestHeaders["content-type"]).isEqualTo("multipart/form-data") + with(requestArgumentCaptor) { + verify(httpClient).newCall(capture()) + assertThat(firstValue.url().toString()).isEqualTo("http://someurl/uploadFoo") + assertThat(firstValue.method()).isEqualTo("POST") + assertThat(firstValue.body()?.contentType()?.type()).isEqualTo(FORM.type()) + assertThat(firstValue.body()?.contentType()?.subtype()).isEqualTo(FORM.subtype()) + val requestHeaders = firstValue.headers() + assertThat(requestHeaders.size()).isEqualTo(3) + assertThat(requestHeaders["Accept"]).isEqualTo("text/plain") + assertThat(requestHeaders["User-Agent"]).isEqualTo("React test agent/1.0") + assertThat(requestHeaders["content-type"]).isEqualTo("multipart/form-data") + } + requestBodyUtil.close() } @Test - @Ignore("TODO: Fix me (T171890419)") fun testMultipartPostRequestBody() { - val inputStream = mock(InputStream::class.java) + val inputStream = mock() whenever(inputStream.available()).thenReturn("imageUri".length) - - val multipartBuilder = mock(MultipartBody.Builder::class.java) - // TODO This PowerMock statement should be migrated to an equivalent for Mockito - // once this test is unsuppressed. - // whenNew(MultipartBody.Builder.class) - // .withNoArguments() - // .thenReturn(multipartBuilder); - whenever(multipartBuilder.setType(anyOrNull(MediaType::class.java))).thenAnswer { - multipartBuilder + setupRequestBodyUtil() + with(requestBodyUtil) { + `when` { RequestBodyUtil.getFileInputStream(any(), any()) } + .thenReturn(inputStream) + `when` { RequestBodyUtil.create(any(), any()) }.thenCallRealMethod() } - whenever(multipartBuilder.addPart(any(Headers::class.java), anyOrNull(RequestBody::class.java))) - .thenAnswer { multipartBuilder } - whenever(multipartBuilder.build()).thenAnswer { mock(MultipartBody::class.java) } + val multipartBodyBuilderMock = + mockConstruction(MultipartBody.Builder::class.java) { mock, _ -> + whenever(mock.setType(any())).thenReturn(mock) + whenever(mock.addPart(any(), any())).thenReturn(mock) + whenever(mock.build()).thenReturn(mock()) + } val headers = listOf(JavaOnlyArray.of("content-type", "multipart/form-data")) @@ -457,23 +447,17 @@ class NetworkingModuleTest { false /* withCredentials */) // verify RequestBodyPart for image - - // TODO This should be migrated to requestBodyUtil.verify(); - // PowerMockito.verifyStatic(RequestBodyUtil.class, times(1)); - RequestBodyUtil.getFileInputStream(any(ReactContext::class.java), eq("imageUri")) - // TODO This should be migrated to requestBodyUtil.verify(); - // PowerMockito.verifyStatic(RequestBodyUtil.class, times(1)); - RequestBodyUtil.create(MediaType.parse("image/jpg"), inputStream) + requestBodyUtil.verify { RequestBodyUtil.getFileInputStream(any(), eq("imageUri")) } + requestBodyUtil.verify { RequestBodyUtil.create(eq(MediaType.parse("image/jpg")), any()) } // verify body - // TODO fix it (now mock is not called) + val multipartBuilder = multipartBodyBuilderMock.constructed()[0] verify(multipartBuilder).build() verify(multipartBuilder).setType(FORM) - // TODO fix it (Captors are nulls) - val headersArgumentCaptor = ArgumentCaptor.forClass(Headers::class.java) - val bodyArgumentCaptor = ArgumentCaptor.forClass(RequestBody::class.java) + val headersArgumentCaptor = argumentCaptor() + val bodyArgumentCaptor = argumentCaptor() verify(multipartBuilder, times(2)) - .addPart(capture(headersArgumentCaptor), capture(bodyArgumentCaptor)) + .addPart(headersArgumentCaptor.capture(), bodyArgumentCaptor.capture()) val bodyHeaders = headersArgumentCaptor.allValues assertThat(bodyHeaders.size).isEqualTo(2) @@ -487,6 +471,9 @@ class NetworkingModuleTest { .isEqualTo("filename=\"测试photo.jpg\"; filename*=utf-8''%E6%B5%8B%E8%AF%95photo.jpg") assertThat(bodyRequestBody[1].contentType()).isEqualTo(MediaType.parse("image/jpg")) assertThat(bodyRequestBody[1].contentLength()).isEqualTo("imageUri".toByteArray().size.toLong()) + + multipartBodyBuilderMock.close() + requestBodyUtil.close() } @Test @@ -495,12 +482,12 @@ class NetworkingModuleTest { val requests = 3 val calls = arrayOfNulls(requests) for (idx in 0 until requests) { - calls[idx] = mock(Call::class.java) + calls[idx] = mock() } - whenever(httpClient.newCall(anyOrNull(Request::class.java))).thenAnswer { invocation -> + whenever(httpClient.newCall(any())).thenAnswer { invocation -> val request = invocation.arguments[0] as Request - calls[(request.tag() as Int?)!! - 1] + calls[(request.tag() as Int) - 1] } networkingModule.initialize() @@ -517,15 +504,14 @@ class NetworkingModuleTest { false /* withCredentials */) } - verify(httpClient, times(3)).newCall(anyOrNull(Request::class.java)) + verify(httpClient, times(3)).newCall(any()) networkingModule.invalidate() - // TODO This should be migrated to okHttpCallUtil.verify(); - // PowerMockito.verifyStatic(OkHttpCallUtil.class, times(3)); - val clientArguments = ArgumentCaptor.forClass(OkHttpClient::class.java) - val requestIdArguments = ArgumentCaptor.forClass(Int::class.java) - - OkHttpCallUtil.cancelTag(capture(clientArguments), capture(requestIdArguments)) + val clientArguments = argumentCaptor() + val requestIdArguments = argumentCaptor() + okHttpCallUtil.verify( + { OkHttpCallUtil.cancelTag(clientArguments.capture(), requestIdArguments.capture()) }, + times(3)) assertThat(requestIdArguments.allValues.size).isEqualTo(requests) for (idx in 0 until requests) { @@ -539,13 +525,12 @@ class NetworkingModuleTest { val requests = 3 val calls = arrayOfNulls(requests) for (idx in 0 until requests) { - calls[idx] = mock(Call::class.java) + calls[idx] = mock() } - whenever(httpClient.newCall(anyOrNull(Request::class.java))).thenAnswer { invocation -> + whenever(httpClient.newCall(any())).thenAnswer { invocation -> val request = invocation.arguments[0] as Request - calls[(request.tag() as Int?)!! - 1] + calls[(request.tag() as Int) - 1] } - for (idx in 0 until requests) { networkingModule.sendRequest( "GET", @@ -558,14 +543,14 @@ class NetworkingModuleTest { 0.0, /* timeout */ false /* withCredentials */) } - verify(httpClient, times(3)).newCall(anyOrNull(Request::class.java)) + verify(httpClient, times(3)).newCall(any()) networkingModule.abortRequest(requests.toDouble()) - // TODO This should be migrated to okHttpCallUtil.verify(); - // PowerMockito.verifyStatic(OkHttpCallUtil.class, times(1)); - var clientArguments = ArgumentCaptor.forClass(OkHttpClient::class.java) - var requestIdArguments = ArgumentCaptor.forClass(Int::class.java) - OkHttpCallUtil.cancelTag(clientArguments.capture(), requestIdArguments.capture()) + var clientArguments = argumentCaptor() + var requestIdArguments = argumentCaptor() + okHttpCallUtil.verify { + OkHttpCallUtil.cancelTag(clientArguments.capture(), requestIdArguments.capture()) + } println(requestIdArguments.allValues) assertThat(requestIdArguments.allValues.size).isEqualTo(1) assertThat(requestIdArguments.allValues[0]).isEqualTo(requests) @@ -575,14 +560,16 @@ class NetworkingModuleTest { // If `cancelTag` would've been called again for the aborted call, we would have had // `requests + 1` calls. networkingModule.invalidate() - // TODO This should be migrated to okHttpCallUtil.verify(); - // PowerMockito.verifyStatic(OkHttpCallUtil.class, times(requests)); - clientArguments = ArgumentCaptor.forClass(OkHttpClient::class.java) - requestIdArguments = ArgumentCaptor.forClass(Int::class.java) - OkHttpCallUtil.cancelTag(clientArguments.capture(), requestIdArguments.capture()) + clientArguments = argumentCaptor() + requestIdArguments = argumentCaptor() + okHttpCallUtil.verify( + { OkHttpCallUtil.cancelTag(clientArguments.capture(), requestIdArguments.capture()) }, + times(requests)) assertThat(requestIdArguments.allValues.size).isEqualTo(requests) for (idx in 0 until requests) { assertThat(requestIdArguments.allValues.contains(idx + 1)).isTrue } } } + +private val FORM = MediaType.get("multipart/form-data") diff --git a/packages/react-native/gradle/libs.versions.toml b/packages/react-native/gradle/libs.versions.toml index 03351e484a26bd..3d03c4f1f22d57 100644 --- a/packages/react-native/gradle/libs.versions.toml +++ b/packages/react-native/gradle/libs.versions.toml @@ -26,6 +26,7 @@ jsr305 = "3.0.2" junit = "4.13.2" kotlin = "2.0.21" mockito = "3.12.4" +mockito-kotlin = "3.2.0" nexus-publish = "1.3.0" okhttp = "4.9.2" okio = "2.9.0" @@ -72,6 +73,7 @@ jsc-android = { module = "io.github.react-native-community:jsc-android", version junit = {module = "junit:junit", version.ref = "junit" } assertj = {module = "org.assertj:assertj-core", version.ref = "assertj" } mockito = {module = "org.mockito:mockito-inline", version.ref = "mockito" } +mockito-kotlin = {module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockito-kotlin" } robolectric = {module = "org.robolectric:robolectric", version.ref = "robolectric" } thoughtworks = {module = "com.thoughtworks.xstream:xstream", version.ref = "xstream" }