无架构设计
无架构设计时期,直接基于Activity/Fragment组件开发,分层和特点:
- View层:layout布局
- 基于XML的有状态View
- 无法承载所有View,如RecyclerView
- Activity/Fragment:大管家
- 初始化View,如Adapter
- 直接耦合各种数据源
- 与Android平台深度依赖
单元测试分类及意义
上述是常规的黑盒测试,从中可以发现,通过黑盒测试,很难模拟各种边界条件。
好的架构除了可扩展和易维护的特点外,还有一个重大的优点是:方便写单元测试
单元测试的分类:
- Local tests:JVM上运行,不依赖真机或模拟器,执行时间最快
- Instrumented tests:真机或模拟器上运行,部署麻烦,执行较慢
单元测试的意义:
- 模拟黑盒测试难以模拟的边界条件
- 代码重构的保障
- 最好的文档
单元测试的范围:
- 正常和异常的Case,如发生过的Bug,Null处理
- 性能测试,如方法耗时
MVC框架
在 MVC框架里,重点是对 M层的分细,实现低耦合。
MVC:
- View层:XML 布局文件
- C层:Activity/Fragment
- 初始化 View,即与View层耦合很大
- 依据 UseCase 的过程和结果,更新View,如Loading、Error、Success
- M层:UseCase + Repository
- UseCase 不依赖 Android 平台
- 通过 RxJava 组装各种数据源
- Repository 的实现依赖 Android 平台,功能单一
MVP框架
MVP:
- View层:XML + Activity/Fragment
- P层:Presenter
- 与 View 层解藕,接口依赖
- 依据 UseCase 的过程和结果,通过View Contract更新View,如Loading、Error、Success
使 Presenter 支持单元测试
MVVM+RxJava框架
MVVM:
- View层:XML + Activity/Fragment
- VM层:ViewModel
- 与 View 层解藕,数据监听,生命周期监听
- Fragment之间共享数据
- 屏幕旋转时,界面重建,数据保留
- 依据 UseCase 的过程和结果,更新LiveData
使用 RxJava 函数式编译,不仅仅是为了方便线程切换,RxJava 带来最大的好处在于在空间和时间两个维度上,对结果进行重排。
通过 MediatorLiveData 也能实现对结果进行重排:
MVVM+LiveData框架
LiveData 本身支持多线程,通过 MediatorLiveData 也能实现对结果的重排,即可以把 M层进行纯 LiveData 改造。
改造成本不大,主要是两个核心点:
- LiveDataTask:run(),LiveData实现线程同步
- UseCase:
- 线程池:io, net, main
- MediatorLiveData 组合各种 LiveData
MVVM+LiveData的代码实现
M层
LiveDataTask:
1 2 3 4 5 6
| public abstract class LiveDataTask<T> implements Runnable { protected final MutableLiveData<DataResult<T>> _liveData = new MutableLiveData<>(); public LiveData<DataResult<T>> getLiveData(){ return _liveData; } }
|
ContentDataRepository:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public interface ContentDataRepository {
LiveDataTask<ContentBean> contentDataFromNet(String cityDir);
LiveDataTask<ContentBean> contentDataFromCache(String cityDir);
LiveDataTask<ContentBean> contentDataFromAssets(String cityDir);
LiveDataTask<Unit> saveContentDataToCache(ContentBean pairData); }
|
GetContentDataUseCase:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
| public class GetContentDataUseCase extends UseCase<GetContentDataUseCase.RequestValues, ContentBean> {
private final ContentDataRepository mContentDataRepository;
public GetContentDataUseCase( AppExecutors appExecutors, ContentDataRepository contentDataRepository ) { super(appExecutors); this.mContentDataRepository = contentDataRepository; }
@Override public LiveData<DataResult<ContentBean>> execute(RequestValues requestValues) { LiveDataTask<ContentBean> assetsLiveDataTask, cacheLiveDataTask, networkLiveDataTask;
assetsLiveDataTask = mContentDataRepository.contentDataFromAssets(requestValues.cityDir); cacheLiveDataTask = mContentDataRepository.contentDataFromCache(requestValues.cityDir); networkLiveDataTask = mContentDataRepository.contentDataFromNet(requestValues.cityDir);
MediatorLiveData<DataResult<ContentBean>> result = new MediatorLiveData<>();
result.setValue(DataResult.loading(null));
MediatorLiveData<DataResult<ContentBean>> cacheAndAssetsLiveData = new MediatorLiveData<>(); appExecutors.diskIO().execute(cacheLiveDataTask); cacheAndAssetsLiveData.addSource(cacheLiveDataTask.getLiveData(), cacheDataResult -> { switch (cacheDataResult.status) { case SUCCESS: case LOADING: cacheAndAssetsLiveData.setValue(cacheDataResult); break; case ERROR: cacheAndAssetsLiveData.removeSource(cacheLiveDataTask.getLiveData()); appExecutors.diskIO().execute(assetsLiveDataTask); cacheAndAssetsLiveData.addSource(assetsLiveDataTask.getLiveData(), new Observer<DataResult<ContentBean>>() { @Override public void onChanged(DataResult<ContentBean> assetsDataResult) { cacheAndAssetsLiveData.setValue(assetsDataResult); } }); break; } });
result.addSource(cacheAndAssetsLiveData, dataResult -> result.setValue(DataResult.loading(dataResult.data)));
appExecutors.networkIO().execute(networkLiveDataTask); result.addSource(networkLiveDataTask.getLiveData(), networkDataResult -> { switch (networkDataResult.status) { case SUCCESS: result.removeSource(cacheAndAssetsLiveData); result.removeSource(networkLiveDataTask.getLiveData());
appExecutors.diskIO().execute(mContentDataRepository.saveContentDataToCache(networkDataResult.data)); result.setValue(DataResult.success(networkDataResult.data)); break; case ERROR: result.removeSource(cacheAndAssetsLiveData); result.removeSource(networkLiveDataTask.getLiveData());
result.addSource(cacheAndAssetsLiveData, dataResult -> result.setValue(DataResult.error(networkDataResult.message, dataResult.data))); break; case LOADING: break; } });
return result; }
public static class RequestValues implements UseCase.RequestValues { private final String cityDir;
public RequestValues(String cityDir) { this.cityDir = cityDir; }
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; RequestValues that = (RequestValues) o; return Objects.equals(cityDir, that.cityDir); }
@Override public int hashCode() { return Objects.hash(cityDir); }
@Override public String toString() { return "RequestValues{" + "cityDir='" + cityDir + '\'' + '}'; } } }
|
VM层
HomePageViewModel:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| public class HomePageViewModel extends ViewModel {
private final GetContentDataUseCase getContentDataUseCase;
private final MutableLiveData<String> cityDirLiveData = new MutableLiveData<>(); public LiveData<String> getCityDirLiveData(){ return cityDirLiveData; }
public void retryCityDir(){ String cityDir = cityDirLiveData.getValue(); if(TextUtils.isEmpty(cityDir)) { return ; } cityDirLiveData.setValue(cityDir); }
public void setCityDir(String cityDir) { if (TextUtils.isEmpty(cityDir) || cityDir.equals(cityDirLiveData.getValue())) { return; }
cityDirLiveData.setValue(cityDir); }
public final LiveData<DataResult<ContentBean>> contentData = Transformations.switchMap(cityDirLiveData, new Function<String, LiveData<DataResult<ContentBean>>>() { MutableLiveData<DataResult<ContentBean>> mSource = new MutableLiveData<>(); @Override public LiveData<DataResult<ContentBean>> apply(String cityDir) { return getContentDataUseCase.execute(new GetContentDataUseCase.RequestValues(cityDir)); } });
public HomePageViewModel(GetContentDataUseCase getContentDataUseCase) { this.getContentDataUseCase = getContentDataUseCase; } }
|
单元测试
对 ViewModel 的单元测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| @RunWith(JUnit4.class) public class HomePageViewModelTest {
@Rule @JvmField public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule();
private final GetContentDataUseCase getContentDataUseCase = mock(GetContentDataUseCase.class); private final HomePageViewModel homePageViewModel = new HomePageViewModel(getContentDataUseCase);
@Test public void testNull(){ assertThat(homePageViewModel.contentData, notNullValue()); verify(getContentDataUseCase, never()).execute(any(GetContentDataUseCase.RequestValues.class)); }
@Test public void dontFetchWithoutObservers(){ homePageViewModel.setCityDir("bj"); verify(getContentDataUseCase, never()).execute(any(GetContentDataUseCase.RequestValues.class)); }
@Test public void fetchWhenObserved(){ homePageViewModel.setCityDir("bj"); homePageViewModel.contentData.observeForever(mock(Observer.class)); verify(getContentDataUseCase).execute(new GetContentDataUseCase.RequestValues("bj")); }
@Test public void changeWhileObserved(){ homePageViewModel.contentData.observeForever(mock(Observer.class));
homePageViewModel.setCityDir("bj"); homePageViewModel.setCityDir("shanghai");
verify(getContentDataUseCase).execute(new GetContentDataUseCase.RequestValues("bj")); verify(getContentDataUseCase).execute(new GetContentDataUseCase.RequestValues("shanghai")); }
@Test public void retry(){ homePageViewModel.retryCityDir(); verifyNoMoreInteractions(getContentDataUseCase);
homePageViewModel.setCityDir("bj"); verifyNoMoreInteractions(getContentDataUseCase);
homePageViewModel.contentData.observeForever(mock(Observer.class)); verify(getContentDataUseCase).execute(new GetContentDataUseCase.RequestValues("bj"));
reset(getContentDataUseCase); homePageViewModel.retryCityDir(); verify(getContentDataUseCase).execute(new GetContentDataUseCase.RequestValues("bj")); }
@Test public void blankCityDir(){ homePageViewModel.setCityDir(""); homePageViewModel.contentData.observeForever(mock(Observer.class)); verifyNoMoreInteractions(getContentDataUseCase); } }
|
对 UseCase 的单元测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
| @RunWith(JUnit4.class) public class GetContentDataUseCaseTest {
@Rule @JvmField public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule();
private MutableLiveData<DataResult<ContentBean>> assetsLiveData = new MutableLiveData<>(); private MutableLiveData<DataResult<ContentBean>> cacheLiveData = new MutableLiveData<>(); private MutableLiveData<DataResult<ContentBean>> netLiveData = new MutableLiveData<>();
private LiveDataTask<ContentBean> assetsTask = Mockito.mock(LiveDataTask.class); private LiveDataTask<ContentBean> cacheTask = Mockito.mock(LiveDataTask.class); private LiveDataTask<ContentBean> netTask = Mockito.mock(LiveDataTask.class);
private ContentDataRepository contentDataRepository;
private GetContentDataUseCase getContentDataUseCase;
@Before public void init() { contentDataRepository = Mockito.mock(ContentDataRepository.class);
Mockito.when(assetsTask.getLiveData()).thenReturn(assetsLiveData); Mockito.when(contentDataRepository.contentDataFromAssets(Mockito.anyString())).thenReturn(assetsTask);
Mockito.when(cacheTask.getLiveData()).thenReturn(cacheLiveData); Mockito.when(contentDataRepository.contentDataFromCache(Mockito.anyString())).thenReturn(cacheTask);
Mockito.when(netTask.getLiveData()).thenReturn(netLiveData); Mockito.when(contentDataRepository.contentDataFromNet(Mockito.anyString())).thenReturn(netTask);
getContentDataUseCase = new GetContentDataUseCase( new InstantAppExecutors(), contentDataRepository ); }
@Test public void testBegin(){ getContentDataUseCase.execute(new GetContentDataUseCase.RequestValues("bj")); verify(cacheTask).run(); verify(netTask).run(); verifyNoMoreInteractions(assetsTask); }
@Test public void dontHaveCache(){ Observer<DataResult<ContentBean>> observer = mock(Observer.class);
getContentDataUseCase .execute(new GetContentDataUseCase.RequestValues("bj")) .observeForever(observer);
verify(observer).onChanged(DataResult.loading(null));
cacheLiveData.postValue(DataResult.error("not have data", null)); verify(assetsTask).run();
ContentBean assetsContentBean = mock(ContentBean.class); assetsLiveData.postValue(DataResult.success(assetsContentBean)); verify(observer).onChanged(DataResult.loading(assetsContentBean));
ContentBean netContentBean = mock(ContentBean.class); netLiveData.postValue(DataResult.success(netContentBean)); verify(observer).onChanged(DataResult.success(netContentBean)); }
@Test public void haveCache(){ Observer<DataResult<ContentBean>> observer = mock(Observer.class);
getContentDataUseCase .execute(new GetContentDataUseCase.RequestValues("bj")) .observeForever(observer);
verify(observer).onChanged(DataResult.loading(null));
ContentBean cacheContentBean = mock(ContentBean.class); cacheLiveData.postValue(DataResult.success(cacheContentBean)); verify(observer).onChanged(DataResult.loading(cacheContentBean)); verify(assetsTask, never()).run();
ContentBean netContentBean = mock(ContentBean.class); netLiveData.postValue(DataResult.success(netContentBean)); verify(contentDataRepository).saveContentDataToCache(netContentBean); verify(observer).onChanged(DataResult.success(netContentBean)); }
@Test public void netError(){ Observer<DataResult<ContentBean>> observer = mock(Observer.class);
getContentDataUseCase .execute(new GetContentDataUseCase.RequestValues("bj")) .observeForever(observer);
verify(observer).onChanged(DataResult.loading(null));
ContentBean cacheContentBean = mock(ContentBean.class); cacheLiveData.postValue(DataResult.success(cacheContentBean)); verify(observer).onChanged(DataResult.loading(cacheContentBean)); verify(assetsTask, never()).run();
netLiveData.postValue(DataResult.error("net error", null)); verify(observer).onChanged(DataResult.error("net error", cacheContentBean)); }
@Test public void netErrorButQuick(){ Observer<DataResult<ContentBean>> observer = mock(Observer.class);
getContentDataUseCase .execute(new GetContentDataUseCase.RequestValues("bj")) .observeForever(observer);
verify(observer).onChanged(DataResult.loading(null));
netLiveData.postValue(DataResult.error("net error", null)); verify(observer, never()).onChanged(DataResult.error("net error", any(ContentBean.class)));
ContentBean cacheContentBean = mock(ContentBean.class); cacheLiveData.postValue(DataResult.success(cacheContentBean)); verify(observer).onChanged(DataResult.error("net error", cacheContentBean)); }
@Test public void netSuccessButQuick(){ Observer<DataResult<ContentBean>> observer = mock(Observer.class);
getContentDataUseCase .execute(new GetContentDataUseCase.RequestValues("bj")) .observeForever(observer);
verify(observer).onChanged(DataResult.loading(null));
ContentBean netContentBean = mock(ContentBean.class); netLiveData.postValue(DataResult.success(netContentBean)); verify(observer).onChanged(DataResult.success(netContentBean));
ContentBean cacheContentBean = mock(ContentBean.class); cacheLiveData.postValue(DataResult.success(cacheContentBean)); verify(observer, never()).onChanged(DataResult.loading(cacheContentBean)); }
}
|
参考
- 架构设计的历史·MVC·MVP·MVVM
- https://academy.realm.io/posts/eric-maxwell-mvc-mvp-and-mvvm-on-android/
- NetworkBoundResourceTest.kt
- Repository图
- 各种框架代码