Local feeds: Unit tests for LocalFeedUpdater (#4551)

This commit is contained in:
Herbert Reiter 2020-10-25 17:22:36 +01:00 committed by GitHub
parent 41580b57cc
commit 28ebbedbdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 373 additions and 4 deletions

View File

@ -52,6 +52,12 @@ android {
dimension "market"
}
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
dependencies {
@ -94,7 +100,9 @@ dependencies {
testImplementation "org.awaitility:awaitility:$awaitilityVersion"
testImplementation 'junit:junit:4.13'
testImplementation 'org.mockito:mockito-core:1.10.19'
testImplementation 'org.mockito:mockito-inline:3.5.13'
testImplementation 'org.robolectric:robolectric:4.3.1'
testImplementation 'javax.inject:javax.inject:1'
androidTestImplementation "com.jayway.android.robotium:robotium-solo:$robotiumSoloVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
androidTestImplementation "androidx.test:runner:$runnerVersion"

View File

@ -358,6 +358,21 @@ public class PodDBAdapter {
// do nothing
}
/**
* <p>Resets all database connections to ensure new database connections for
* the next test case. Call method only for unit tests.</p>
*
* <p>That's a workaround for a Robolectric issue in ShadowSQLiteConnection
* that leads to an error <tt>IllegalStateException: Illegal connection
* pointer</tt> if several threads try to use the same database connection.
* For more information see
* <a href="https://github.com/robolectric/robolectric/issues/1890">robolectric/robolectric#1890</a>.</p>
*/
public static void tearDownTests() {
db = null;
SingletonHolder.dbHelper.close();
}
public static boolean deleteDatabase() {
PodDBAdapter adapter = getInstance();
adapter.open();

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,138 @@
package androidx.documentfile.provider;
import android.content.res.AssetManager;
import android.net.Uri;
import android.webkit.MimeTypeMap;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.IOException;
/**
* <p>Wraps an Android assets file or folder as a DocumentFile object.</p>
*
* <p>This is used to emulate access to the external storage.</p>
*/
public class AssetsDocumentFile extends DocumentFile {
/**
* Absolute file path in the assets folder.
*/
@NonNull
private final String fileName;
@NonNull
private final AssetManager assetManager;
public AssetsDocumentFile(@NonNull String fileName, @NonNull AssetManager assetManager) {
super(null);
this.fileName = fileName;
this.assetManager = assetManager;
}
@Nullable
@Override
public DocumentFile createFile(@NonNull String mimeType, @NonNull String displayName) {
return null;
}
@Nullable
@Override
public DocumentFile createDirectory(@NonNull String displayName) {
return null;
}
@NonNull
@Override
public Uri getUri() {
return Uri.parse(fileName);
}
@Nullable
@Override
public String getName() {
int pos = fileName.indexOf('/');
if (pos >= 0) {
return fileName.substring(pos + 1);
} else {
return fileName;
}
}
@Nullable
@Override
public String getType() {
String extension = MimeTypeMap.getFileExtensionFromUrl(fileName);
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
@Override
public boolean isDirectory() {
return false;
}
@Override
public boolean isFile() {
return true;
}
@Override
public boolean isVirtual() {
return false;
}
@Override
public long lastModified() {
return 0;
}
@Override
public long length() {
return 0;
}
@Override
public boolean canRead() {
return true;
}
@Override
public boolean canWrite() {
return false;
}
@Override
public boolean delete() {
return false;
}
@Override
public boolean exists() {
return true;
}
@NonNull
@Override
public DocumentFile[] listFiles() {
try {
String[] files = assetManager.list(fileName);
if (files == null) {
return new DocumentFile[0];
}
DocumentFile[] result = new DocumentFile[files.length];
for (int i = 0; i < files.length; i++) {
String subFileName = fileName + '/' + files[i];
result[i] = new AssetsDocumentFile(subFileName, assetManager);
}
return result;
} catch (IOException e) {
return new DocumentFile[0];
}
}
@Override
public boolean renameTo(@NonNull String displayName) {
return false;
}
}

View File

@ -0,0 +1,208 @@
package de.danoeh.antennapod.core.feed;
import android.app.Application;
import android.content.Context;
import android.media.MediaMetadataRetriever;
import android.webkit.MimeTypeMap;
import androidx.annotation.NonNull;
import androidx.documentfile.provider.AssetsDocumentFile;
import androidx.documentfile.provider.DocumentFile;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.shadows.ShadowMediaMetadataRetriever;
import java.io.IOException;
import java.util.List;
import de.danoeh.antennapod.core.ApplicationCallbacks;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
/**
* Test local feeds handling in class LocalFeedUpdater.
*/
@RunWith(RobolectricTestRunner.class)
public class LocalFeedUpdaterTest {
/**
* URL to locate the local feed media files on the external storage (SD card).
* The exact URL doesn't matter here as access to external storage is mocked
* (seems not to be supported by Robolectric).
*/
private static final String FEED_URL =
"content://com.android.externalstorage.documents/tree/primary%3ADownload%2Flocal-feed";
private static final String LOCAL_FEED_DIR1 = "local-feed1";
private static final String LOCAL_FEED_DIR2 = "local-feed2";
private Context context;
@Before
public void setUp() throws Exception {
// Initialize environment
context = InstrumentationRegistry.getInstrumentation().getContext();
UserPreferences.init(context);
Application app = (Application) context;
ClientConfig.applicationCallbacks = mock(ApplicationCallbacks.class);
when(ClientConfig.applicationCallbacks.getApplicationInstance()).thenReturn(app);
// Initialize database
PodDBAdapter.init(context);
PodDBAdapter.deleteDatabase();
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
adapter.close();
mapDummyMetadata(LOCAL_FEED_DIR1);
mapDummyMetadata(LOCAL_FEED_DIR2);
shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping("mp3", "audio/mp3");
}
@After
public void tearDown() {
PodDBAdapter.tearDownTests();
}
/**
* Test adding a new local feed.
*/
@Test
public void testUpdateFeed_AddNewFeed() {
// check for empty database
List<Feed> feedListBefore = DBReader.getFeedList();
assertTrue(feedListBefore.isEmpty());
callUpdateFeed(LOCAL_FEED_DIR2);
// verify new feed in database
verifySingleFeedInDatabaseAndItemCount(2);
Feed feedAfter = verifySingleFeedInDatabase();
assertEquals(FEED_URL, feedAfter.getDownload_url());
}
/**
* Test adding further items to an existing local feed.
*/
@Test
public void testUpdateFeed_AddMoreItems() {
// add local feed with 1 item (localFeedDir1)
callUpdateFeed(LOCAL_FEED_DIR1);
// now add another item (by changing to local feed folder localFeedDir2)
callUpdateFeed(LOCAL_FEED_DIR2);
verifySingleFeedInDatabaseAndItemCount(2);
}
/**
* Test removing items from an existing local feed without a corresponding media file.
*/
@Test
public void testUpdateFeed_RemoveItems() {
// add local feed with 2 items (localFeedDir1)
callUpdateFeed(LOCAL_FEED_DIR2);
// now remove an item (by changing to local feed folder localFeedDir1)
callUpdateFeed(LOCAL_FEED_DIR1);
verifySingleFeedInDatabaseAndItemCount(1);
}
/**
* Test feed icon defined in the local feed media folder.
*/
@Test
public void testUpdateFeed_FeedIconFromFolder() {
callUpdateFeed(LOCAL_FEED_DIR2);
Feed feedAfter = verifySingleFeedInDatabase();
assertTrue(feedAfter.getImageUrl().contains("local-feed2/folder.png"));
}
/**
* Test default feed icon if there is no matching file in the local feed media folder.
*/
@Test
public void testUpdateFeed_FeedIconDefault() {
callUpdateFeed(LOCAL_FEED_DIR1);
Feed feedAfter = verifySingleFeedInDatabase();
String resourceEntryName = context.getResources().getResourceEntryName(R.raw.local_feed_default_icon);
assertTrue(feedAfter.getImageUrl().contains(resourceEntryName));
}
/**
* Fill ShadowMediaMetadataRetriever with dummy duration and title.
*
* @param localFeedDir assets local feed folder with media files
*/
private void mapDummyMetadata(@NonNull String localFeedDir) throws IOException {
String[] fileNames = context.getAssets().list(localFeedDir);
for (String fileName : fileNames) {
String path = localFeedDir + '/' + fileName;
ShadowMediaMetadataRetriever.addMetadata(path,
MediaMetadataRetriever.METADATA_KEY_DURATION, "10");
ShadowMediaMetadataRetriever.addMetadata(path,
MediaMetadataRetriever.METADATA_KEY_TITLE, fileName);
}
}
/**
* Calls the method {@link LocalFeedUpdater#updateFeed(Feed, Context)} with
* the given local feed folder.
*
* @param localFeedDir assets local feed folder with media files
*/
private void callUpdateFeed(@NonNull String localFeedDir) {
DocumentFile documentFile = new AssetsDocumentFile(localFeedDir, context.getAssets());
try (MockedStatic<DocumentFile> dfMock = Mockito.mockStatic(DocumentFile.class)) {
// mock external storage
dfMock.when(() -> DocumentFile.fromTreeUri(any(), any())).thenReturn(documentFile);
// call method to test
Feed feed = new Feed(FEED_URL, null);
LocalFeedUpdater.updateFeed(feed, context);
}
}
/**
* Verify that the database contains exactly one feed and return that feed.
*/
@NonNull
private static Feed verifySingleFeedInDatabase() {
List<Feed> feedListAfter = DBReader.getFeedList();
assertEquals(1, feedListAfter.size());
return feedListAfter.get(0);
}
/**
* Verify that the database contains exactly one feed and the number of
* items in the feed.
*
* @param expectedItemCount expected number of items in the feed
*/
private static void verifySingleFeedInDatabaseAndItemCount(int expectedItemCount) {
Feed feed = verifySingleFeedInDatabase();
List<FeedItem> feedItems = DBReader.getFeedItemList(feed);
assertEquals(expectedItemCount, feedItems.size());
}
}

View File

@ -32,7 +32,7 @@ import static java.util.Collections.emptyList;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.stub;
import static org.mockito.Mockito.when;
public class ItemEnqueuePositionCalculatorTest {
@ -189,7 +189,7 @@ public class ItemEnqueuePositionCalculatorTest {
//
ItemEnqueuePositionCalculator calculator = new ItemEnqueuePositionCalculator(options);
DownloadStateProvider stubDownloadStateProvider = mock(DownloadStateProvider.class);
stub(stubDownloadStateProvider.isDownloadingFile(any(FeedMedia.class))).toReturn(false);
when(stubDownloadStateProvider.isDownloadingFile(any(FeedMedia.class))).thenReturn(false);
calculator.downloadStateProvider = stubDownloadStateProvider;
// Setup initial data
@ -232,7 +232,7 @@ public class ItemEnqueuePositionCalculatorTest {
private static FeedItem setAsDownloading(FeedItem item, DownloadStateProvider stubDownloadStateProvider,
boolean isDownloading) {
stub(stubDownloadStateProvider.isDownloadingFile(item.getMedia())).toReturn(isDownloading);
when(stubDownloadStateProvider.isDownloadingFile(item.getMedia())).thenReturn(isDownloading);
return item;
}