Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[media-library] Add unit tests to module (#9538)
# Why MediaLibrary has many utility methods, which behavior is sometimes unclear. Unit tests would describe that behavior. Also, contains some minor fixes. # How - Add native unit tests to `MediaLibrary` on Android. - Fixes bug, where `getAssetsAsync()` could return without promise rejection, when caught `IOException` - Adds a little input validation to module methods - Removes unused test dependencies from `expo-web-browser`. # Test plan `et android-native-unit-tests` --- WIP - [x] Add testing dependencies - [x] Add utility functions to mock `ContentResolver` - [x] Test `GetQueryInfo` - [x] Test `MediaLibraryUtils` where possible - [x] Test `GetAssets` - [x] Test `GetAssetInfo` - [x] Extract useful test utility functions to `unimodules-test-core` - [x] Input validation and minor refactors
- Loading branch information
Showing
16 changed files
with
765 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
121 changes: 121 additions & 0 deletions
121
...s/expo-media-library/android/src/test/java/expo/modules/medialibrary/GetAssetInfoTests.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package expo.modules.medialibrary | ||
|
||
import android.os.Bundle | ||
import android.provider.MediaStore | ||
import io.mockk.every | ||
import io.mockk.just | ||
import io.mockk.mockkStatic | ||
import io.mockk.runs | ||
import io.mockk.slot | ||
import org.junit.Assert.assertEquals | ||
import org.junit.Before | ||
import org.junit.Test | ||
import org.junit.runner.RunWith | ||
import org.robolectric.RobolectricTestRunner | ||
import org.unimodules.test.core.PromiseMock | ||
import org.unimodules.test.core.assertListsEqual | ||
import org.unimodules.test.core.assertRejected | ||
import org.unimodules.test.core.assertRejectedWithCode | ||
import org.unimodules.test.core.promiseResolvedWithType | ||
import java.io.IOException | ||
|
||
@RunWith(RobolectricTestRunner::class) | ||
internal class GetAssetInfoTests { | ||
|
||
private lateinit var promise: PromiseMock | ||
private lateinit var mockContext: MockContext | ||
|
||
@Before | ||
fun setUp() { | ||
promise = PromiseMock() | ||
mockContext = MockContext() | ||
} | ||
|
||
@Test | ||
fun `GetAssetInfo should call queryAssetInfo`() { | ||
//arrange | ||
val context = mockContext.get() | ||
val selectionSlot = slot<String>() | ||
val selectionArgsSlot = slot<Array<String>>() | ||
|
||
mockkStatic(MediaLibraryUtils::class) | ||
every { MediaLibraryUtils.queryAssetInfo( | ||
context, | ||
capture(selectionSlot), | ||
capture(selectionArgsSlot), | ||
true, | ||
promise | ||
) } just runs | ||
|
||
val expectedSelection = "${MediaStore.Images.Media._ID}=?" | ||
val assetId = "testAssetId" | ||
|
||
//act | ||
GetAssetInfo(context, assetId, promise).doInBackground() | ||
|
||
//assert | ||
assertEquals(expectedSelection, selectionSlot.captured) | ||
assertEquals(1, selectionArgsSlot.captured.size) | ||
assertEquals(assetId, selectionArgsSlot.captured[0]) | ||
} | ||
|
||
@Test | ||
fun `queryAssetInfo should resolve asset`() { | ||
//arrange | ||
val context = mockContext with mockContentResolverForResult(arrayOf( | ||
MockData.mockImage.toColumnArray() | ||
)) | ||
|
||
mockkStatic(MediaLibraryUtils::class) | ||
every { | ||
MediaLibraryUtils.putAssetsInfo(any(), any(), any(), any(), any(), any()) | ||
} just runs | ||
|
||
val selection = "${MediaStore.Images.Media._ID}=?" | ||
val selectionArgs = arrayOf(MockData.mockImage.id.toString()) | ||
|
||
//act | ||
MediaLibraryUtils.queryAssetInfo(context, selection, selectionArgs, false, promise) | ||
|
||
//assert | ||
promiseResolvedWithType<ArrayList<Bundle>>(promise) { | ||
assertListsEqual(emptyList<Bundle>(), it) | ||
} | ||
} | ||
|
||
@Test | ||
fun `queryAssetInfo should reject on null cursor`() { | ||
//arrange | ||
val context = mockContext with mockContentResolver(null) | ||
|
||
//act | ||
MediaLibraryUtils.queryAssetInfo(context, "", emptyArray(), false, promise) | ||
|
||
//assert | ||
assertRejected(promise) | ||
} | ||
|
||
@Test | ||
fun `queryAssetInfo should reject on SecurityException`() { | ||
//arrange | ||
val context = mockContext with throwableContentResolver(SecurityException()) | ||
|
||
//act | ||
MediaLibraryUtils.queryAssetInfo(context, "", emptyArray(), false, promise) | ||
|
||
//assert | ||
assertRejectedWithCode(promise, MediaLibraryConstants.ERROR_UNABLE_TO_LOAD_PERMISSION) | ||
} | ||
|
||
@Test | ||
fun `queryAssetInfo should reject on IOException`() { | ||
//arrange | ||
val context = mockContext with throwableContentResolver(IOException()) | ||
|
||
//act | ||
MediaLibraryUtils.queryAssetInfo(context, "", emptyArray(), false, promise) | ||
|
||
//assert | ||
assertRejectedWithCode(promise, MediaLibraryConstants.ERROR_IO_EXCEPTION) | ||
} | ||
} |
108 changes: 108 additions & 0 deletions
108
packages/expo-media-library/android/src/test/java/expo/modules/medialibrary/GetAssetsTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
package expo.modules.medialibrary | ||
|
||
import expo.modules.medialibrary.MediaLibraryConstants.ERROR_UNABLE_TO_LOAD | ||
import expo.modules.medialibrary.MediaLibraryConstants.ERROR_UNABLE_TO_LOAD_PERMISSION | ||
import io.mockk.every | ||
import io.mockk.just | ||
import io.mockk.mockk | ||
import io.mockk.mockkConstructor | ||
import io.mockk.mockkStatic | ||
import io.mockk.runs | ||
import io.mockk.unmockkConstructor | ||
import org.junit.After | ||
import org.junit.Assert.assertEquals | ||
import org.junit.Before | ||
import org.junit.Test | ||
import org.junit.runner.RunWith | ||
import org.robolectric.RobolectricTestRunner | ||
import org.unimodules.test.core.PromiseMock | ||
import org.unimodules.test.core.assertRejectedWithCode | ||
import org.unimodules.test.core.promiseResolved | ||
import java.io.IOException | ||
|
||
@RunWith(RobolectricTestRunner::class) | ||
internal class GetAssetsTest { | ||
|
||
private lateinit var promise: PromiseMock | ||
private lateinit var mockContext: MockContext | ||
|
||
@Before | ||
fun setUp() { | ||
promise = PromiseMock() | ||
mockContext = MockContext() | ||
|
||
mockGetQueryInfo(selection = "", order = "", limit = 10, offset = 0) | ||
|
||
mockkStatic(MediaLibraryUtils::class) | ||
every { MediaLibraryUtils.putAssetsInfo(any(), any(), any(), any(), any(), any()) } just runs | ||
} | ||
|
||
@After | ||
fun tearDown() { | ||
unmockkConstructor(GetQueryInfo::class) | ||
} | ||
|
||
@Test | ||
fun `getAssets should resolve with correct response`() { | ||
//arrange | ||
val context = mockContext with mockContentResolverForResult(arrayOf( | ||
MockData.mockImage.toColumnArray(), | ||
MockData.mockVideo.toColumnArray() | ||
)) | ||
|
||
//act | ||
GetAssets(context, mutableMapOf(), promise).doInBackground() | ||
|
||
//assert | ||
promiseResolved(promise) { | ||
assertEquals(2, it.getInt("totalCount")) | ||
} | ||
} | ||
|
||
@Test | ||
fun `GetAssets should reject on null cursor`() { | ||
//arrange | ||
val context = mockContext with mockContentResolver(null) | ||
|
||
//act | ||
GetAssets(context, mutableMapOf(), promise).doInBackground() | ||
|
||
//assert | ||
assertRejectedWithCode(promise, ERROR_UNABLE_TO_LOAD) | ||
} | ||
|
||
@Test | ||
fun `GetAssets should reject on SecurityException`() { | ||
//arrange | ||
val context = mockContext with throwableContentResolver(SecurityException()) | ||
|
||
//act | ||
GetAssets(context, mutableMapOf(), promise).doInBackground() | ||
|
||
//assert | ||
assertRejectedWithCode(promise, ERROR_UNABLE_TO_LOAD_PERMISSION) | ||
} | ||
|
||
@Test | ||
fun `GetAssets should reject on IOException`() { | ||
//arrange | ||
val context = mockContext with throwableContentResolver(IOException()) | ||
|
||
//act | ||
GetAssets(context, mutableMapOf(), promise).doInBackground() | ||
|
||
//assert | ||
assertRejectedWithCode(promise, ERROR_UNABLE_TO_LOAD) | ||
} | ||
|
||
private fun mockGetQueryInfo(selection: String, order: String, limit: Int, offset: Int) { | ||
val mockQueryInfo = mockk<GetQueryInfo>() | ||
every { mockQueryInfo.selection } returns selection | ||
every { mockQueryInfo.order } returns order | ||
every { mockQueryInfo.limit } returns limit | ||
every { mockQueryInfo.offset } returns offset | ||
|
||
mockkConstructor(GetQueryInfo::class) | ||
every { anyConstructed<GetQueryInfo>().invoke() } returns mockQueryInfo | ||
} | ||
} |
102 changes: 102 additions & 0 deletions
102
...s/expo-media-library/android/src/test/java/expo/modules/medialibrary/GetQueryInfoTests.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
package expo.modules.medialibrary | ||
|
||
import android.provider.MediaStore | ||
import org.junit.Assert.assertEquals | ||
import org.junit.Test | ||
import org.junit.runner.RunWith | ||
import org.robolectric.RobolectricTestRunner | ||
|
||
@RunWith(RobolectricTestRunner::class) | ||
internal class GetQueryInfoTests { | ||
|
||
@Test | ||
fun `test if proper values are handled properly`() { | ||
// arrange | ||
val limit = 21.0 | ||
val offset = "37" | ||
val album = "sampleAlbumId" | ||
val mediaTypes = listOf(MediaLibraryConstants.MEDIA_TYPE_PHOTO) | ||
val createdBefore = 6789.0 | ||
val createdAfter = 2345.0 | ||
val sortBy = listOf( | ||
ArrayList(listOf(MediaLibraryConstants.SORT_BY_DEFAULT, true)) | ||
) | ||
|
||
val inputMap = mapOf( | ||
"first" to limit, | ||
"after" to offset, | ||
"createdBefore" to createdBefore, | ||
"createdAfter" to createdAfter, | ||
"album" to album, | ||
"mediaType" to mediaTypes, | ||
"sortBy" to sortBy | ||
) | ||
|
||
val expectedSelection = "${MediaStore.Images.Media.BUCKET_ID} = $album" + | ||
" AND ${MediaStore.Files.FileColumns.MEDIA_TYPE} IN (${MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE})" + | ||
" AND ${MediaStore.Images.Media.DATE_TAKEN} > ${createdAfter.toLong()}" + | ||
" AND ${MediaStore.Images.Media.DATE_TAKEN} < ${createdBefore.toLong()}" | ||
|
||
val expectedOrder = MediaLibraryUtils.mapOrderDescriptor(sortBy) | ||
|
||
// act | ||
val queryInfo = GetQueryInfo(inputMap).invoke() | ||
|
||
// assert | ||
assertEquals(limit.toInt(), queryInfo.limit) | ||
assertEquals(offset.toInt(), queryInfo.offset) | ||
assertEquals(expectedSelection, queryInfo.selection) | ||
assertEquals(expectedOrder, queryInfo.order) | ||
} | ||
|
||
@Test | ||
fun `test if no input gives default values`() { | ||
// arrange | ||
val expectedSelection = "${MediaStore.Files.FileColumns.MEDIA_TYPE} != ${MediaStore.Files.FileColumns.MEDIA_TYPE_NONE}"; | ||
|
||
// act | ||
val queryInfo = GetQueryInfo(emptyMap()).invoke() | ||
|
||
// assert | ||
assertEquals(20, queryInfo.limit) | ||
assertEquals(0, queryInfo.offset) | ||
assertEquals(expectedSelection, queryInfo.selection) | ||
assertEquals(MediaStore.Images.Media.DEFAULT_SORT_ORDER, queryInfo.order) | ||
} | ||
|
||
@Test | ||
fun `test if invalid arguments fall back to defaults`() { | ||
// arrange | ||
val limitOutOfRange = -123.0 | ||
|
||
val inputMap = mapOf( | ||
"first" to limitOutOfRange, | ||
"after" to "invalidStringValue" | ||
) | ||
|
||
// act | ||
val queryInfo = GetQueryInfo(inputMap).invoke() | ||
|
||
// assert | ||
assertEquals(limitOutOfRange.toInt(), queryInfo.limit) | ||
assertEquals(0, queryInfo.offset) | ||
} | ||
|
||
@Test(expected = IllegalArgumentException::class) | ||
fun `test if invalid mediaType throws`() { | ||
val inputMap = mapOf( | ||
"mediaType" to listOf("someRandomString") | ||
) | ||
|
||
val queryInfo = GetQueryInfo(inputMap).invoke() | ||
} | ||
|
||
@Test(expected = IllegalArgumentException::class) | ||
fun `test if invalid order throws`() { | ||
val inputMap = mapOf( | ||
"sortBy" to listOf("invalid name") | ||
) | ||
|
||
val queryInfo = GetQueryInfo(inputMap).invoke() | ||
} | ||
} |
Oops, something went wrong.