你为什么要关心测试? 像任何人一样,程序员犯错误。 我们可能会忘记我们上个月实现的边缘案例,或者我们传递一个空字符串时某些方法的行为方式。
在每次更改后都可以使用APP,并尝试每次可能的点击,点按,手势和方向更改,以确保一切正常。 在旋转设备时,您可能会忘记右上角的三次敲击,因此当用户执行此操作时,所有内容都会崩溃,并引发空指针异常。 用户做的很愚蠢,我们需要确保每个类都能够做到应有的功能,并且APP的每个部分都可以处理我们抛出的所有内容。
这就是我们编写自动化测试的原因。
1. 测试 Clean 架构
Clean架构完全关于可维护性和可测试性。 架构的每个部分都有一个目的。 我们只需要指定它并检查它实际上是否每次都做它的工作。
现在,让我们现实一点。 我们可以测试什么? 一切。 诚然,如果你正确地构建你的代码,你可以测试一切。 这取决于你要测试什么。 不幸的是,通常没有时间来测试一切。
可测性。 这是第一步。 第二步是测试正确的方法。 让我们提醒一下FIRST的旧规则:
Fast – 测试应该非常快。如果需要几分钟或几小时来执行测试,写测试是没有意义的。 没有人会检查测试,如果是这样的话!
Isolated – 一次测试APP的一个单元。 安排在该单位的一切行为完全按照你想要的方式,然后执行测试单位并且断言它的行为是正确的。
Repeatable – 每次执行测试时都应该有相同的结果。 它不应该依赖于一些不确定的数据。
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应用程序中的用例结构如下所示:
问题是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 ,因此可以测试适当的回调。 它可以断言完成,发射值,数值等。
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
本文探讨了自动化测试的重要性和最佳实践,包括Clean架构下的测试方法、不同组件的测试策略、以及如何编写有意义的测试名称。




被折叠的 条评论
为什么被折叠?



