你爲何要關心測試? 像任何人同樣,程序員犯錯誤。 咱們可能會忘記咱們上個月實現的邊緣案例,或者咱們傳遞一個空字符串時某些方法的行爲方式。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