Android架构:第五部分-Clean架构是如何测试的 (译)

你为何要关心测试? 像任何人同样,程序员犯错误。 咱们可能会忘记咱们上个月实现的边缘案例,或者咱们传递一个空字符串时某些方法的行为方式。html

在每次更改后均可以使用APP,并尝试每次可能的点击,点按,手势和方向更改,以确保一切正常。 在旋转设备时,您可能会忘记右上角的三次敲击,所以当用户执行此操做时,全部内容都会崩溃,并引起空指针异常。 用户作的很愚蠢,咱们须要确保每一个类都可以作到应有的功能,而且APP的每一个部分均可以处理咱们抛出的全部内容。java

这就是咱们编写自动化测试的缘由。react

1. 测试 Clean 架构

Clean架构彻底关于可维护性和可测试性。 架构的每一个部分都有一个目的。 咱们只须要指定它并检查它其实是否每次都作它的工做。android

如今,让咱们现实一点。 咱们能够测试什么? 一切。 诚然,若是你正确地构建你的代码,你能够测试一切。 这取决于你要测试什么。 不幸的是,一般没有时间来测试一切。程序员

可测性。 这是第一步。 第二步是测试正确的方法。 让咱们提醒一下FIRST的旧规则:架构

Fast – 测试应该很是快。若是须要几分钟或几小时来执行测试,写测试是没有意义的。 没有人会检查测试,若是是这样的话!app

Isolated – 一次测试APP的一个单元。 安排在该单位的一切行为彻底按照你想要的方式,而后执行测试单位而且断言它的行为是正确的。框架

Repeatable – 每次执行测试时都应该有相同的结果。 它不该该依赖于一些不肯定的数据。ide

Self-validating – 框架应该知道测试是否经过。 不该该有任何手动检查测试。 只要检查一切是不是绿色,就是这样:)函数

Timely – 测试应该和代码同样写,或者甚至在代码以前写!

因此,咱们制做了一个可测试的APP,咱们知道如何测试。 那如何命名单元测试的名字呢?

2. 命名测试

说实话,咱们如何命名测试很重要。它直接反映了你对测试的态度,以及你想要测试什么的方式。

让咱们认识咱们的受害者:

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!

3. 测试Domain

让咱们看看咱们如何测试用例。 咱们的Reedley应用程序中的用例结构以下所示:

Reedley App 3.1

问题是EnableBackgroundFeedUpdatesUseCase是最终的,若是它是一些其余用例测试所需的模拟,则没法完成。 Mockito不容许嘲笑最终课程。

用例被其实现引用,因此让咱们添加另外一层接口:

Reedley App 3.2

如今咱们能够模拟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 ,所以能够测试适当的回调。 它能够断言完成,发射值,数值等。

4. 测试Data

这里是很是简单的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);
}

在测试映射器(转换器)时,指定映射器的输入以及您指望从映射器获得的确切输出,而后声明它们是相等的。 为服务,解析器等作一样的事情

5. 测试 App module

在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

Part 2: The Clean Architecture

Part 1: every new beginning is hard

相关文章
相关标签/搜索