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

相關文章
相關標籤/搜索