你为何要关心测试? 像任何人同样,程序员犯错误。 咱们可能会忘记咱们上个月实现的边缘案例,或者咱们传递一个空字符串时某些方法的行为方式。html
在每次更改后均可以使用APP,并尝试每次可能的点击,点按,手势和方向更改,以确保一切正常。 在旋转设备时,您可能会忘记右上角的三次敲击,所以当用户执行此操做时,全部内容都会崩溃,并引起空指针异常。 用户作的很愚蠢,咱们须要确保每一个类都可以作到应有的功能,而且APP的每一个部分均可以处理咱们抛出的全部内容。java
这就是咱们编写自动化测试的缘由。react
Clean架构彻底关于可维护性和可测试性。 架构的每一个部分都有一个目的。 咱们只须要指定它并检查它其实是否每次都作它的工做。android
如今,让咱们现实一点。 咱们能够测试什么? 一切。 诚然,若是你正确地构建你的代码,你能够测试一切。 这取决于你要测试什么。 不幸的是,一般没有时间来测试一切。程序员
可测性。 这是第一步。 第二步是测试正确的方法。 让咱们提醒一下FIRST的旧规则:架构
Fast – 测试应该很是快。若是须要几分钟或几小时来执行测试,写测试是没有意义的。 没有人会检查测试,若是是这样的话!app
Isolated – 一次测试APP的一个单元。 安排在该单位的一切行为彻底按照你想要的方式,而后执行测试单位而且断言它的行为是正确的。框架
Repeatable – 每次执行测试时都应该有相同的结果。 它不该该依赖于一些不肯定的数据。ide
Self-validating – 框架应该知道测试是否经过。 不该该有任何手动检查测试。 只要检查一切是不是绿色,就是这样:)函数
Timely – 测试应该和代码同样写,或者甚至在代码以前写!
因此,咱们制做了一个可测试的APP,咱们知道如何测试。 那如何命名单元测试的名字呢?
说实话,咱们如何命名测试很重要。它直接反映了你对测试的态度,以及你想要测试什么的方式。
让咱们认识咱们的受害者:
1 2 3 4 5 6 |
public final class DeleteFeedUseCase implements CompletableUseCaseWithParameter { @Override public Completable execute(final Integer feedId) { //implementation } } |
首先,幼稚的方法是编写像这样的测试:
1 2 3 4 5 6 7 8 9 |
@Test public void executeWhenDatabaseReturnsTrue() throws Exception { } @Test public void executeWithErrorInDatabase() throws Exception { } |
这被称为实现式命名。 它与类实现紧密结合。 当咱们改变实施时,咱们须要改变咱们对类的指望。 这些一般是在代码以后编写的,关于它们惟一的好处是它们能够很快写入。
第二种方式是示例式命名:
1 2 3 4 5 6 7 8 9 |
@Test public void doSomethingWithIdsSmallerThanZero() throws Exception { } @Test public void ignoreWhenNullIsPassed() throws Exception { } |
示例式测试是系统使用的示例。 它们在测试边缘案例时很好,但不要将它们用于全部事情,它们应该与实现相关联。
如今,让咱们尝试抽象咱们对这个类的见解,并从实现中移开。 那这个呢:
1 2 3 4 5 6 7 8 9 |
@Test public void shouldDeleteExistingFeed() throws Exception { } @Test public void shouldIgnoreDeletingNonExistingFeed() throws Exception { } |
咱们确切地知道咱们对这个类的指望。 这个测试类能够用做类的规范,所以可使用名称规范式的命名。 名称没有说明实现的任何内容,而且从测试的名称 - 规范 - 咱们能够编写实际的具体类。 规范样式的名称一般是最好的选择,但若是您认为您没法测试某些特定于实现的边缘案例,则能够随时抛出几个示例样式的测试。
理论到此为止,咱们准备好让咱们的手变dirty!
让咱们看看咱们如何测试用例。 咱们的Reedley应用程序中的用例结构以下所示:
问题是EnableBackgroundFeedUpdatesUseCase是最终的,若是它是一些其余用例测试所需的模拟,则没法完成。 Mockito不容许嘲笑最终课程。
用例被其实现引用,因此让咱们添加另外一层接口:
如今咱们能够模拟EnableBackgroundFeedUpdatesUseCase接口。 但在咱们的平常实践中,咱们得出结论,这在开发时很是混乱,中间层接口是空的,用例实际上并不须要接口。 用例只作一项工做,它在名称中说得很对 - “启用后台供稿更新用例”,没有什么能够抽象的!
好的,让咱们试试这个 - 咱们不须要作最终用例。
咱们尽量作最后的决定,它使得更多结构化和更优化的代码。 咱们能够忍受用例不是最终的,但必须有更好的方法。
咱们找到了使用mockito-inline的解决方案。 它使得unmockable,mockable。 随着Mockito的新版本,能够启用最终classes的模拟。
如下是用例实现的示例:
1 2 3 4 5 6 7 8 9 10 11 12 |
public final class EnableBackgroundFeedUpdatesUseCase implements CompletableUseCase { private final SetShouldUpdateFeedsInBackgroundUseCase setShouldUpdateFeedsInBackgroundUseCase; private final FeedsUpdateScheduler feedsUpdateScheduler; //constructor @Override public Completable execute() { return setShouldUpdateFeedsInBackgroundUseCase.execute(true) .concatWith(Completable.fromAction(feedsUpdateScheduler::scheduleBackgroundFeedUpdates)); } } |
在测试用例时,咱们应该测试该用例调用Repositories中的正确方法或执行其余用例。 咱们还应该测试该用例返回适当的回调:
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 |
private EnableBackgroundFeedUpdatesUseCase enableBackgroundFeedUpdatesUseCase; private SetShouldUpdateFeedsInBackgroundUseCase setShouldUpdateFeedsInBackgroundUseCase; private FeedsUpdateScheduler feedUpdateScheduler; private TestSubscriber testSubscriber; @Before public void setUp() throws Exception { setShouldUpdateFeedsInBackgroundUseCase = Mockito.mock(SetShouldUpdateFeedsInBackgroundUseCase.class); feedUpdateScheduler = Mockito.mock(FeedsUpdateScheduler.class); testSubscriber = new TestSubscriber(); enableBackgroundFeedUpdatesUseCase = new EnableBackgroundFeedUpdatesUseCase(setShouldUpdateFeedsInBackgroundUseCase, feedUpdateScheduler); } @Test public void shouldEnableBackgroundFeedUpdates() throws Exception { Mockito.when(setShouldUpdateFeedsInBackgroundUseCase.execute(true)).thenReturn(Completable.complete()); enableBackgroundFeedUpdatesUseCase.execute().subscribe(testSubscriber); Mockito.verify(setShouldUpdateFeedsInBackgroundUseCase, Mockito.times(1)).execute(true); Mockito.verifyNoMoreInteractions(setShouldUpdateFeedsInBackgroundUseCase); Mockito.verify(feedUpdateScheduler, Mockito.times(1)).scheduleBackgroundFeedUpdates(); Mockito.verifyNoMoreInteractions(feedUpdateScheduler); testSubscriber.assertCompleted(); } |
这里使用了来自Rx的 TestSubscriber ,所以能够测试适当的回调。 它能够断言完成,发射值,数值等。
这里是很是简单的Repository方法,它只使用一个DAO方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public final class FeedRepositoryImpl implements FeedRepository { private final FeedDao feedDao; private final Scheduler backgroundScheduler; //constructor @Override public Single feedExists(final String feedUrl) { return Single.defer(() -> feedDao.doesFeedExist(feedUrl)) .subscribeOn(backgroundScheduler); } //more methods } |
测试Repository时,应该安排DAO - 使它们返回或接收一些虚拟数据,并检查Repository是否以正确的方式处理数据:
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 |
private FeedService feedService; private FeedDao feedDao; private PreferenceUtils preferenceUtils; private Scheduler scheduler; private FeedRepositoryImpl feedRepositoryImpl; @Before public void setUp() throws Exception { feedService = Mockito.mock(FeedService.class); feedDao = Mockito.mock(FeedDao.class); preferenceUtils = Mockito.mock(PreferenceUtils.class); scheduler = Schedulers.immediate(); feedRepositoryImpl = new FeedRepositoryImpl(feedService, feedDao, preferenceUtils, scheduler);} @Test public void shouldReturnInfoAboutFeedExistingIfFeedExists() throws Exception { Mockito.when(feedDao.doesFeedExist(DataTestData.TEST_COMPLEX_URL_STRING_1)).thenReturn(Single.just(true)); final TestSubscriber testSubscriber = new TestSubscriber<>(); feedRepositoryImpl.feedExists(DataTestData.TEST_COMPLEX_URL_STRING_1).subscribe(testSubscriber); Mockito.verify(feedDao, Mockito.times(1)).doesFeedExist(DataTestData.TEST_COMPLEX_URL_STRING_1); Mockito.verifyNoMoreInteractions(feedDao); testSubscriber.assertCompleted(); testSubscriber.assertValue(true); } |
在测试映射器(转换器)时,指定映射器的输入以及您指望从映射器获得的确切输出,而后声明它们是相等的。 为服务,解析器等作一样的事情
在Clean架构之上,咱们喜欢使用MVP。 Presenter只是普通的Java对象,不与Android链接,因此测试它们没有什么特别之处。 让咱们看看咱们能够测试什么:
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 |
public final class ArticlesPresenterTest { @Test public void shouldFetchArticlesAndPassThemToView() throws Exception { } @Test public void shouldFetchFavouriteArticlesAndPassThemToView() throws Exception { } @Test public void shouldShowArticleDetails() throws Exception { } @Test public void shouldMarkArticleAsRead() throws Exception { } @Test public void shouldMakeArticleFavourite() throws Exception { } @Test public void shouldMakeArticleNotFavorite() throws Exception { } } |
Presenter一般有不少依赖关系。 咱们经过@Inject注释将依赖关系注入Presenter,而不是经过构造函数。 因此在下面的测试中,咱们须要使用@Mock和@Spy注释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public final class ArticlesPresenter extends BasePresenter implements ArticlesContract.Presenter { @Inject GetArticlesUseCase getArticlesUseCase; @Inject FeedViewModeMapper feedViewModeMapper; // (...) more fields public ArticlesPresenter(final ArticlesContract.View view) { super(view); } @Override public void fetchArticles(final int feedId) { viewActionQueue.subscribeTo(getArticlesUseCase.execute(feedId) .map(feedViewModeMapper::mapArticlesToViewModels) .map(this::toViewAction),Throwable::printStackTrace); } // (...) more methods } |
@Mock只是简单地模拟出Class。 @Spy让你使用现有的全部方法均可以工做的实例,可是你能够methods一些方法,而且“spy”调用哪些方法。 Mocks经过@InjectMocks注释注入Presenter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@Mock GetArticlesUseCase getArticlesUseCase; @Mock FeedViewModeMapper feedViewModeMapper; @Mock ConnectivityReceiver connectivityReceiver; @Mock ViewActionQueueProvider viewActionQueueProvider; @Spy Scheduler mainThreadScheduler = Schedulers.immediate(); @Spy MockViewActionQueue mockViewActionHandler; @InjectMocks ArticlesPresenter articlesPresenter; |
而后一些设置是必需的。 视图是手动模拟的,由于它是经过构造函数注入的,咱们调用presenter.start()和presenter.activate(),所以演示程序已准备好并启动:
1 2 3 4 5 6 7 8 9 10 11 12 |
@Before public void setUp() throws Exception { view = Mockito.mock(ArticlesContract.View.class); articlesPresenter = new ArticlesPresenter(view); MockitoAnnotations.initMocks(this); Mockito.when(connectivityReceiver.getConnectivityStatus()).thenReturn(Observable.just(true)); Mockito.when(viewActionQueueProvider.queueFor(Mockito.any())).thenReturn(new MockViewActionQueue ()); articlesPresenter.start(); articlesPresenter.activate(); } |
一切准备就绪后,咱们能够开始编写测试。 准备好全部内容并确保Presenter在须要时调用视图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@Test public void shouldFetchArticlesAndPassThemToView() throws Exception { final int feedId = AppTestData.TEST_FEED_ID; final List<article> articles = new ArrayList<>(); final Article = new Article (AppTestData.TEST_ARTICLE_ID, feedId, AppTestData.TEST_STRING, AppTestData.TEST_LINK, AppTestData.TEST_LONG_DATE, false, false); articles.add(article); final List<ArticleViewModel articleViewModels = new ArrayList <>(); final ArticleViewModel articleViweModel = new ArticleViewModel(AppTestData.TEST_ARTICLE_ID, AppTestData.TEST_STRING, AppTestData.TEST_LINK, AppTestDAta.TEST_STRING, false, false); articleViewModels.add(articleViewModel); Mockito.when(getArticlesUseCase.execute(feedID)).thenReturn(Single.just(articles)); Mockito.when(feedViewModeMapper.mapArticlesToViewModels(Mockito.anyList())).thenReturn(articleViewModels); articlesPresenter.fetchArticles(feedId); Mockito.verify(getArticlesUseCase, Mockito.times(1)).execute(feedId); Moclito.verify(view, Mockito.times(1)).showArticles(articleViewModels); } |
在编码以前和期间考虑测试,这样你就能够编写可测试和解耦的代码。 使用你的测试做为类的规范,若是可能的话在代码以前写下它们。 不要让你的自我妨碍,咱们都会犯错误。 所以,咱们须要有一个流程来保护咱们本身的应用程序!
这是Android Architecture系列的一部分。 想查看咱们的其余部分能够:
Part 4: Applying Clean Architecture on Android (Hands-on)
Part 3: Applying Clean Architecture on Android