iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 20
1

今天是testing的最後一天,把剩下的Movie Bloc和Movie Api的測試寫完吧。

Movie Bloc Test

在test/bloc資料夾下新增「movie_bloc_test.dart」

引用所需的檔案和套件,並建立一個mock MovieRepository 的實體來模擬MovieRepository。

import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'package:fluttube/movie/movie.dart';

class MockMovieRepository extends Mock implements MovieRepository {}

使用setUp()在每次測試開始前初始化movieRepository和movieBloc。

void main() {
  MockMovieRepository movieRepository;
  MovieBloc movieBloc;

  setUp(() {
    movieRepository = MockMovieRepository();
    movieBloc = MovieBloc(movieRepository: movieRepository);
  });

老樣子,測試初始State是否正確以及dispose後還有沒有轉換State

test('initial state is correct', () {
    expect(movieBloc.initialState, InitMovieState());
  });

test('dispose does not emit new states', () {
    expectLater(
      movieBloc.state,
      emitsInOrder([]),
    );
    movieBloc.dispose();
  });

接下來是測試三個FetchMovieList的Event是否有正確運作,由於三個的程式碼基本一樣(差在網址而已),所以就以NowPlayingMovie當範例。

group('Test NowPlayingMovieState Success and Fail condition', () {
    test('emits [InitMovieState, LoadingMovie, NowPlayingMovieState] for success FetchNowPlaying', () {
      var fakeData = {
        'page': 1,
        'total_pages': 10,
        'total_results': 100,
        'results': []
      };

      final expectedResponse = [
        InitMovieState().toString(),
        LoadingMovie().toString(),
        NowPlayingMovieState(movieList: MovieList.fromJson(fakeData)).toString()
      ];

      when(movieRepository.fetchNowPlayingMovieList(region: "TW"))
          .thenAnswer((_) => Future.value(MovieList.fromJson(fakeData)));

      expectLater(
        movieBloc.state.map((state) => state.toString()),
        emitsInOrder(expectedResponse),
      );

      movieBloc.dispatch(FetchNowPlaying(region: 'TW'));
    });

    test('emits [InitMovieState, LoadingMovie, FailedFetchData] for fail FetchNowPlaying', (){

      final expectedResponse = [
        InitMovieState(),
        LoadingMovie(),
        FailedFetchData(),
      ];

      when(movieRepository.fetchNowPlayingMovieList(region: "TW"))
          .thenThrow(Exception);

      expectLater(
        movieBloc.state,
        emitsInOrder(expectedResponse),
      );

      movieBloc.dispatch(FetchNowPlaying(region: 'TW'));
    });
  });

這邊需要使用到toString()的原因是雖然MovieState本身有繼承Equatable可以協助比較兩個實體是否相同,但傳入到NowPlayingMovieState的參數MovieList並沒有,導致expect會判定為不符合。

另外要注意的是movieRepository.fetchNowPlayingMovieList需要型態為MovieList的回傳值,所以要建立一個假的資料給他。

另外兩個Event的測試就單純的更改method名就可以囉。

Movie Api Test

在test/api資料夾下新增「movie_api_test.dart」

引用所需的檔案和套件,並建立一個mock Client 的實體來模擬http的Client。

import 'dart:io';

import 'package:http/http.dart';
import 'package:fluttube/movie/movie_api_provider.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'package:fluttube/movie/model/movie_list.dart';
import 'dart:convert';

class MockClient extends Mock implements Client {}

使用setUp()在每次測試開始前初始化client和movieApiProvider。

特別解釋一下stubGetmethod的用途。它接收兩個參數,第一個參數放要發送Request的網址,這裡使用argThat(startsWith(...))這個matcher,只要client.get()內的參數開頭和url相同就會觸發when;第二個參數可以放成功或失敗的response。

void main() {
  MockClient client;
  MovieApiProvider movieApiProvider;
  final baseUrl = 'http://api.themoviedb.org/3/movie';
  var rawData = {"results": [{"popularity": 21.532, "vote_count": 0, "video": false, "poster_path": r"/relIJqmRexUeUXPq7pNfnb178qg.jpg", "id": 614017, "adult": false, "backdrop_path": r"/xtCjSzqGLhb0oEiclJUyDApQHR2.jpg", "original_language": "zh", "original_title": "返校", "genre_ids": [27], "title": "Detention", "vote_average": 0, "overview": "Set in Taiwan during the 'White Terror' period of martial law, a high school girl who awakens in an empty school, only to find that her entire community has been abandoned except for one other student. Soon they realize that they have entered a realm filled with vengeful spirits and hungry ghosts.", "release_date": "2019-09-20"},], "page": 1, "total_results": 12, "dates": {"maximum": "2019-09-25", "minimum": "2019-08-08"}, "total_pages": 1};

  setUp(() {
    client = MockClient();
    movieApiProvider = MovieApiProvider(client: client);
  });
  
   void stubGet(String url, Response response) {
    when(client.get(argThat(startsWith(url))))
        .thenAnswer((_) async => response);
  }
}

由於三個API測試的程式碼基本上都相同,就只挑一個解釋囉。

相較於測試Bloc要考慮State的順序,測試API只要確認結果正確就可以了。
第一個是在測試Api正確回傳資料情形。使用假的資料產生response測試回傳資料是否有被正確處理,需要加上HttpHeaders.contentTypeHeader: 'application/json; charset=utf-8'是因為測試資料裡有中文,如果不加這段程式會報錯。

第二個是在測試API回傳的statusCode是200以外的情況,期待的結果就是引發例外throwsException

group('Test call fetch nowPlayingMovieList API', () {
    test('Success fetch nowPlayingMovieList', () async {
      final expectResult = MovieList.fromJson(rawData);
      stubGet(
          "$baseUrl/now_playing?",
          Response(json.encode(rawData), 200, headers: {
            HttpHeaders.contentTypeHeader: 'application/json; charset=utf-8'
          }));

      final result = await movieApiProvider.fetchNowPlayingMovieList();
      expect(result.toString(), expectResult.toString());
    });

    test('Fail to fetch nowPlayingMovieList API', () async {
      final expectResult = throwsException;

      stubGet("$baseUrl/now_playing?", Response("BAD DATA", 404));

      expect(movieApiProvider.fetchNowPlayingMovieList(), expectResult);
    });
  });

今日總結

今天的內容比較少,不過終於幫目前為止的功能都寫了測試。

若想要一次測試test資料夾下的所有檔案,可以在command line輸入flutter test

接下來可以繼續開發專案啦,明後兩天就來改造現在很醜的首頁,看看Flutter提供的widget能創造出怎麼樣的介面吧。

完整程式碼在這裡-> FlutTube Github


上一篇
【Flutter基礎概念與實作】 Day19–如何用Mockito測試BloC
下一篇
【Flutter基礎概念與實作】 Day21–美化首頁(1) 滑動吧!電影卡片
系列文
Flutter---Google推出的跨平台框架,Android、iOS一起搞定30

尚未有邦友留言

立即登入留言