Add beginning of a ViewModel test

mostly to demonstrate how those could look like
This commit is contained in:
Torsten Grote
2020-12-18 14:42:33 -03:00
parent 015ecb1d99
commit 205b4f77b2
12 changed files with 317 additions and 16 deletions

View File

@@ -126,10 +126,11 @@ dependencies {
testImplementation 'androidx.test:runner:1.3.0'
testImplementation 'androidx.test.ext:junit:1.1.2'
testImplementation 'androidx.fragment:fragment-testing:1.2.5'
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
testImplementation 'org.robolectric:robolectric:4.3.1'
testImplementation 'org.mockito:mockito-core:3.1.0'
testImplementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.13.1'
testImplementation "org.jmock:jmock:$jmockVersion"
testImplementation "org.jmock:jmock-junit4:$jmockVersion"
testImplementation "org.jmock:jmock-legacy:$jmockVersion"

View File

@@ -12,6 +12,7 @@ import org.briarproject.bramble.api.db.NoSuchPendingContactException;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.LiveResult;
@@ -54,8 +55,9 @@ public class AddContactViewModel extends DbViewModel {
ContactManager contactManager,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db) {
super(application, dbExecutor, lifecycleManager, db);
TransactionManager db,
AndroidExecutor androidExecutor) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
this.contactManager = contactManager;
}

View File

@@ -19,6 +19,7 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.rendezvous.RendezvousPoller;
import org.briarproject.bramble.api.rendezvous.event.RendezvousPollEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import java.util.ArrayList;
@@ -58,10 +59,11 @@ public class PendingContactListViewModel extends DbViewModel
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
ContactManager contactManager,
RendezvousPoller rendezvousPoller,
EventBus eventBus) {
super(application, dbExecutor, lifecycleManager, db);
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
this.contactManager = contactManager;
this.rendezvousPoller = rendezvousPoller;
this.eventBus = eventBus;

View File

@@ -22,6 +22,7 @@ import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.attachment.AttachmentCreator;
import org.briarproject.briar.android.attachment.AttachmentManager;
import org.briarproject.briar.android.attachment.AttachmentResult;
@@ -106,6 +107,7 @@ public class ConversationViewModel extends DbViewModel
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
EventBus eventBus,
MessagingManager messagingManager,
ContactManager contactManager,
@@ -113,7 +115,7 @@ public class ConversationViewModel extends DbViewModel
PrivateMessageFactory privateMessageFactory,
AttachmentRetriever attachmentRetriever,
AttachmentCreator attachmentCreator) {
super(application, dbExecutor, lifecycleManager, db);
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
this.db = db;
this.eventBus = eventBus;
this.messagingManager = messagingManager;

View File

@@ -15,6 +15,7 @@ import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveEvent;
@@ -80,8 +81,9 @@ public class ImageViewModel extends DbViewModel implements EventListener {
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
@IoExecutor Executor ioExecutor) {
super(application, dbExecutor, lifecycleManager, db);
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
this.messagingManager = messagingManager;
this.eventBus = eventBus;
this.ioExecutor = ioExecutor;

View File

@@ -9,6 +9,7 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import java.util.concurrent.Executor;
@@ -53,8 +54,9 @@ public class NavDrawerViewModel extends DbViewModel {
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
SettingsManager settingsManager) {
super(app, dbExecutor, lifecycleManager, db);
super(app, dbExecutor, lifecycleManager, db, androidExecutor);
this.settingsManager = settingsManager;
}

View File

@@ -29,6 +29,7 @@ import org.briarproject.bramble.api.plugin.event.TransportStateEvent;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import java.util.concurrent.Executor;
@@ -87,9 +88,10 @@ public class PluginViewModel extends DbViewModel implements EventListener {
@Inject
PluginViewModel(Application app, @DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, TransactionManager db,
SettingsManager settingsManager, PluginManager pluginManager,
EventBus eventBus, NetworkManager networkManager) {
super(app, dbExecutor, lifecycleManager, db);
AndroidExecutor androidExecutor, SettingsManager settingsManager,
PluginManager pluginManager, EventBus eventBus,
NetworkManager networkManager) {
super(app, dbExecutor, lifecycleManager, db, androidExecutor);
this.app = app;
this.settingsManager = settingsManager;
this.pluginManager = pluginManager;

View File

@@ -20,6 +20,7 @@ import org.briarproject.bramble.api.sync.ClientId;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.event.GroupAddedEvent;
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveResult;
import org.briarproject.briar.api.android.AndroidNotificationManager;
@@ -78,11 +79,12 @@ class GroupListViewModel extends DbViewModel implements EventListener {
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
PrivateGroupManager groupManager,
GroupInvitationManager groupInvitationManager,
ContactManager contactManager,
AndroidNotificationManager notificationManager, EventBus eventBus) {
super(application, dbExecutor, lifecycleManager, db);
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
this.groupManager = groupManager;
this.groupInvitationManager = groupInvitationManager;
this.contactManager = contactManager;

View File

@@ -1,7 +1,6 @@
package org.briarproject.briar.android.viewmodel;
import android.app.Application;
import android.os.Handler;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbCallable;
@@ -10,6 +9,7 @@ import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.AndroidExecutor;
import java.util.ArrayList;
import java.util.List;
@@ -27,7 +27,6 @@ import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.recyclerview.widget.RecyclerView;
import static android.os.Looper.getMainLooper;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@@ -42,16 +41,19 @@ public abstract class DbViewModel extends AndroidViewModel {
private final Executor dbExecutor;
private final LifecycleManager lifecycleManager;
private final TransactionManager db;
private final AndroidExecutor androidExecutor;
public DbViewModel(
@NonNull Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db) {
TransactionManager db,
AndroidExecutor androidExecutor) {
super(application);
this.dbExecutor = dbExecutor;
this.lifecycleManager = lifecycleManager;
this.db = db;
this.androidExecutor = androidExecutor;
}
/**
@@ -103,8 +105,8 @@ public abstract class DbViewModel extends AndroidViewModel {
Thread.currentThread().interrupt();
} catch (DbException e) {
logException(LOG, WARNING, e);
new Handler(getMainLooper())
.post(() -> uiUpdate.call(new LiveResult<>(e)));
androidExecutor.runOnUiThread(
() -> uiUpdate.call(new LiveResult<>(e)));
}
});
}

View File

@@ -0,0 +1,36 @@
package org.briarproject.briar.android;
import org.briarproject.bramble.api.system.AndroidExecutor;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
public class AndroidExecutorTestImpl implements AndroidExecutor {
private final Executor executor;
public AndroidExecutorTestImpl(Executor executor) {
this.executor = executor;
}
@Override
public <V> Future<V> runOnBackgroundThread(Callable<V> c) {
throw new IllegalStateException("not implemented");
}
@Override
public void runOnBackgroundThread(Runnable r) {
executor.execute(r);
}
@Override
public <V> Future<V> runOnUiThread(Callable<V> c) {
throw new IllegalStateException("not implemented");
}
@Override
public void runOnUiThread(Runnable r) {
executor.execute(r);
}
}

View File

@@ -0,0 +1,212 @@
package org.briarproject.briar.android.privategroup.list;
import android.app.Application;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.identity.AuthorInfo;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.sync.Group;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.DbExpectations;
import org.briarproject.bramble.test.ImmediateExecutor;
import org.briarproject.briar.android.AndroidExecutorTestImpl;
import org.briarproject.briar.android.viewmodel.LiveResult;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.privategroup.PrivateGroup;
import org.briarproject.briar.api.privategroup.PrivateGroupManager;
import org.briarproject.briar.api.privategroup.event.GroupDissolvedEvent;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationItem;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager;
import org.jmock.Expectations;
import org.jmock.lib.legacy.ClassImposteriser;
import org.junit.Rule;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executor;
import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
import static edu.emory.mathcs.backport.java.util.Collections.emptyList;
import static edu.emory.mathcs.backport.java.util.Collections.singletonList;
import static org.briarproject.bramble.test.TestUtils.getAuthor;
import static org.briarproject.bramble.test.TestUtils.getContact;
import static org.briarproject.bramble.test.TestUtils.getGroup;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.briar.android.viewmodel.LiveDataTestUtil.getOrAwaitValue;
import static org.briarproject.briar.api.client.MessageTracker.GroupCount;
import static org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager.CLIENT_ID;
import static org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager.MAJOR_VERSION;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class GroupListViewModelTest extends BrambleMockTestCase {
@Rule
public final InstantTaskExecutorRule testRule =
new InstantTaskExecutorRule();
private final LifecycleManager lifecycleManager =
context.mock(LifecycleManager.class);
private final TransactionManager db =
context.mock(TransactionManager.class);
private final PrivateGroupManager groupManager =
context.mock(PrivateGroupManager.class);
private final GroupInvitationManager groupInvitationManager =
context.mock(GroupInvitationManager.class);
private final ContactManager contactManager =
context.mock(ContactManager.class);
private final AndroidNotificationManager notificationManager =
context.mock(AndroidNotificationManager.class);
private final EventBus eventBus = context.mock(EventBus.class);
private final GroupListViewModel viewModel;
private final Group g1 = getGroup(CLIENT_ID, MAJOR_VERSION);
private final Group g2 = getGroup(CLIENT_ID, MAJOR_VERSION);
private final PrivateGroup privateGroup1 =
new PrivateGroup(g1, "foo", getAuthor(), getRandomBytes(2));
private final PrivateGroup privateGroup2 =
new PrivateGroup(g2, "bar", getAuthor(), getRandomBytes(2));
private final AuthorInfo authorInfo1 =
new AuthorInfo(AuthorInfo.Status.UNVERIFIED);
private final AuthorInfo authorInfo2 =
new AuthorInfo(AuthorInfo.Status.VERIFIED);
private final GroupCount groupCount1 = new GroupCount(2, 1, 23L);
private final GroupCount groupCount2 = new GroupCount(5, 3, 42L);
private final GroupItem item1 =
new GroupItem(privateGroup1, authorInfo1, groupCount1, false);
private final GroupItem item2 =
new GroupItem(privateGroup2, authorInfo2, groupCount2, false);
public GroupListViewModelTest() {
context.setImposteriser(ClassImposteriser.INSTANCE);
Application app = context.mock(Application.class);
context.checking(new Expectations() {{
oneOf(eventBus).addListener(with(any(EventListener.class)));
}});
Executor dbExecutor = new ImmediateExecutor();
AndroidExecutor androidExecutor =
new AndroidExecutorTestImpl(dbExecutor);
viewModel = new GroupListViewModel(app, dbExecutor, lifecycleManager,
db, androidExecutor, groupManager, groupInvitationManager,
contactManager, notificationManager, eventBus);
}
@Test
public void testLoadGroupsException() throws Exception {
DbException dbException = new DbException();
Transaction txn = new Transaction(null, true);
context.checking(new DbExpectations() {{
oneOf(lifecycleManager).waitForDatabase();
oneOf(db).transaction(with(true), withDbRunnable(txn));
oneOf(groupManager).getPrivateGroups(txn);
will(throwException(dbException));
}});
viewModel.loadGroups();
LiveResult<List<GroupItem>> result =
getOrAwaitValue(viewModel.getGroupItems());
assertTrue(result.hasError());
assertEquals(dbException, result.getException());
assertNull(result.getResultOrNull());
}
@Test
public void testLoadGroups() throws Exception {
Transaction txn = new Transaction(null, true);
context.checking(new DbExpectations() {{
oneOf(lifecycleManager).waitForDatabase();
oneOf(db).transaction(with(true), withDbRunnable(txn));
oneOf(groupManager).getPrivateGroups(txn);
will(returnValue(Arrays.asList(privateGroup1, privateGroup2)));
}});
expectLoadGroup(txn, privateGroup1, authorInfo1, groupCount1, false);
expectLoadGroup(txn, privateGroup2, authorInfo2, groupCount2, false);
viewModel.loadGroups();
// unpack updated live data
LiveResult<List<GroupItem>> result =
getOrAwaitValue(viewModel.getGroupItems());
assertFalse(result.hasError());
List<GroupItem> liveList = result.getResultOrNull();
assertNotNull(liveList);
// list is sorted by last message timestamp
assertEquals(Arrays.asList(item2, item1), liveList);
// group 1 gets dissolved by creator
Event dissolvedEvent = new GroupDissolvedEvent(privateGroup1.getId());
viewModel.eventOccurred(dissolvedEvent);
result = getOrAwaitValue(viewModel.getGroupItems());
liveList = result.getResultOrNull();
assertNotNull(liveList);
assertEquals(2, liveList.size());
// assert that list update includes dissolved group item
for (GroupItem item : liveList) {
if (item.getId().equals(privateGroup1.getId())) {
assertTrue(item.isDissolved());
} else if (item.getId().equals(privateGroup2.getId())) {
assertFalse(item.isDissolved());
} else fail();
}
}
@Test
public void testLoadNumInvitations() throws Exception {
context.checking(new Expectations() {{
oneOf(lifecycleManager).waitForDatabase();
oneOf(groupInvitationManager).getInvitations();
will(returnValue(emptyList()));
}});
viewModel.loadNumInvitations();
int num = getOrAwaitValue(viewModel.getNumInvitations());
assertEquals(0, num);
PrivateGroup pg = context.mock(PrivateGroup.class);
Contact c = getContact();
GroupInvitationItem item = new GroupInvitationItem(pg, c);
context.checking(new Expectations() {{
oneOf(lifecycleManager).waitForDatabase();
oneOf(groupInvitationManager).getInvitations();
will(returnValue(singletonList(item)));
}});
viewModel.loadNumInvitations();
num = getOrAwaitValue(viewModel.getNumInvitations());
assertEquals(1, num);
}
private void expectLoadGroup(Transaction txn, PrivateGroup privateGroup,
AuthorInfo authorInfo, GroupCount groupCount, boolean dissolved)
throws DbException {
context.checking(new DbExpectations() {{
oneOf(contactManager)
.getAuthorInfo(txn, privateGroup.getCreator().getId());
will(returnValue(authorInfo));
oneOf(groupManager).getGroupCount(txn, privateGroup.getId());
will(returnValue(groupCount));
oneOf(groupManager).isDissolved(txn, privateGroup.getId());
will(returnValue(dissolved));
}});
}
}

View File

@@ -0,0 +1,36 @@
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0
https://gist.github.com/JoseAlcerreca/1e9ee05dcdd6a6a6fa1cbfc125559bba
*/
package org.briarproject.briar.android.viewmodel;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
public class LiveDataTestUtil {
public static <T> T getOrAwaitValue(final LiveData<T> liveData)
throws InterruptedException {
final Object[] data = new Object[1];
final CountDownLatch latch = new CountDownLatch(1);
Observer<T> observer = new Observer<T>() {
@Override
public void onChanged(@Nullable T o) {
data[0] = o;
latch.countDown();
liveData.removeObserver(this);
}
};
liveData.observeForever(observer);
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(2, TimeUnit.SECONDS)) {
throw new RuntimeException("LiveData value was never set.");
}
//noinspection unchecked
return (T) data[0];
}
}