2021/3/20 Mock dependencies using Mockitoの訳

 

<<前のページへ

単体テストは、ライブWebサービスまたはデータベースからデータをフェッチするクラスに依存する場合があります。 これはいくつかの理由で不便です:

  • ライブサービスまたはデータベースを呼び出すと、テストの実行が遅くなります。
  • Webサービスまたはデータベースが予期しない結果を返した場合、合格すべきテストの結果が「失敗(不合格)」になる可能性があります。 これは「フレークテスト」として知られています。
  • ライブWebサービスまたはデータベースを使用して、考えられるすべての成功および失敗のシナリオをテストすることは困難です。

したがって、ライブWebサービスまたはデータベースに依存するのではなく、これらの依存関係を「モック」することができます。

モックを使用すると、ライブWebサービスまたはデータベースをエミュレートし、状況に応じて特定の結果を返すことができます。

一般的に言えば、クラスの代替実装を作成することで依存関係をモックできます。 これらの代替実装を手作業で作成するか、ショートカットとしてMockitoパッケージを利用してください。

このレシピは、次の手順を使用してMockitoパッケージを使用したモックの基本を示しています。

1.Mockitoパッケージを追加します。

2.テストするための関数を定義します。

3.http.Clientのモックを使用したテストファイルを作ります。

4.それぞれの条件でのテストを記述します。

5.テストを実行します。

さらなる情報は Mockito packageをご覧ください。


1. Add the package dependencies

1.Mockitoパッケージを追加します。

mockitoパッケージを使用するために、下記のように、pubspec.yamlファイルのdev_dependenciesセクションに

mockito

flutter_test

パッケージを追加します。

今回のサンプルでもhttpパッケージを使いますので、pubspc.yamlファイルのdependenciesセクションに追加します。

mockito : 5.0.0 はコード自動生成によりDartのnull-safetyをサポートしています。必要なコード自動生成を実行するために、build_runnerパッケージをdev_dependenciesセクションに追加します。

dependencies:
  http: <newest_version>
dev_dependencies:
  flutter_test:
    sdk: flutter
  mockito: <newest_version>
  build_runner: <newest_version>

 

 


2. Create a function to test

2.テストするための関数を定義します。

この例では、“the Fetch data from the internet recipe”のfetchAlbum関数を単体テストします。 この機能をテストするには、次の2つの変更を行います。

1.関数にhttp.Clientを提供します。 これにより、状況に応じて正しいhttp.Clientを提供できます。 Flutterおよびサーバー側プロジェクトの場合は、http.IOClientを提供します。 ブラウザアプリの場合は、http.BrowserClientを提供します。 テストには、モックhttp.Clientを提供します。

2.モックが難しい静的なhttp.get()メソッドではなく、http.Clientを使用してインターネットからデータをフェッチします。

変更後の関数は次のようになります。

Future<Album> fetchAlbum(http.Client client) async {
  final response =
      await client.get(Uri.https('jsonplaceholder.typicode.com', 'albums/1'));

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON.
    return Album.fromJson(jsonDecode(response.body));
  } else {
    // If the server did not return a 200 OK response,
    // then throw an exception.
    throw Exception('Failed to load album');
  }
}

アプリ側のコード(fetchAlbum呼び出し側)では、

fetchAlbum(http.Client())

のようにして、http.ClientをfetchAlbumに直接渡します。


3. Create a test file with a mock http.Client

Next, create a test file.

次に、テストファイルを作ります。

 

Following the advice in the Introduction to unit testing recipe, create a file called fetch_album_test.dart in the root test folder.

Introduction to unit testingレシピに従い、fetch_album_test.dartファイルをtestディレクトリ直下に新規作成します。

 

Add the annotation @GenerateMocks([http.Client]) to the main function to generate a MockClient class with mockito.

アノテーション

@GenerateMocks([http.Client])

をmain関数につけて、mockitoのMockClientクラスを自動生成で作ります。

 

The generated MockClient class implements the http.Client class. This allows you to pass the MockClient to the fetchAlbum function, and return different http responses in each test.

自動生成されたMockClientクラスの定義を見ると、http.Clientクラスを実装(impliments)していることがわかります。これにより、MockClientインスタンスをfetchAlbum関数に渡すことができるようになります。

そして各テストごとに異なるhttpレスポンスを返すことが可能になります。

 

The generated mocks will be located in fetch_album_test.mocks.dart. Import this file to use them.

自動生成されたモッククラス(MockClientクラス)はfetch_album_test.mocks.dartで定義されています。

このクラスを使うために下記のようにインポートします。

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';

import '../lib/main.dart';
import 'fetch_album_test.mocks.dart';

// Generate a MockClient using the Mockito package.
// Create new instances of this class in each test.
//MockitoパッケージのMockClientを自動生成します。
//各テストでこのクラスのインスタンスを生成します。
@GenerateMocks([http.Client])
void main() {

次に、下記のコマンドを実行してモッククラス(MockClientクラス)を自動生成します。

flutter pub run build_runner build


4. Write a test for each condition

The fetchAlbum() function does one of two things:

fetchAlbum()関数について下記の二つのことが言えます。

 

  1. Returns an Album if the http call succeeds
  2. Throws an Exception if the http call fails

1.httpリクエストの結果成功した時、Albumインスタンスを返す。

2.httpリクエストの結果失敗した時、例外をスローする。

 

Therefore, you want to test these two conditions. Use the MockClient class to return an “Ok” response for the success test, and an error response for the unsuccessful test. Test these conditions using the when() function provided by Mockito:

ですからこれから上記二つの条件が正しいかをテストします。MockClientクラスを使って成功時のテストの際”Ok”レスポンスを返すようにします。また、失敗時のテストの際エラーレスポンスを返すようにします。

Mockitoパッケージのwhen()関数を使ってこれらの条件をテストしていきます。

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';

import '../lib/main.dart';
import 'fetch_album_test.mocks.dart';

// Generate a MockClient using the Mockito package.
// Create new instances of this class in each test.
@GenerateMocks([http.Client])
void main() {
  group('fetchAlbum', () {
    test('returns an Album if the http call completes successfully', () async {
      final client = MockClient();

      // Use Mockito to return a successful response when it calls the
      // provided http.Client.
      when(client.get(Uri.https('jsonplaceholder.typicode.com', 'albums/1')))
          .thenAnswer((_) async => http.Response('{"userId": 1, "id": 2, "title": "mock"}', 200));

      expect(await fetchAlbum(client), isA<Album>());
    });

    test('throws an exception if the http call completes with an error', () {
      final client = MockClient();

      // Use Mockito to return an unsuccessful response when it calls the
      // provided http.Client.
      when(client.get(Uri.https('jsonplaceholder.typicode.com', 'albums/1')))
          .thenAnswer((_) async => http.Response('Not Found', 404));

      expect(fetchAlbum(client), throwsException);
    });
  });
}

 


5. Run the tests

Now that you have a fetchAlbum() function with tests in place, run the tests.

fetchAlbum()関数のテストコードができましたので、テストしていきます。

flutter test test/fetch_album_test.dart

Introduction to unit testing recipeにあるように、エディターでテストを行うこともできます。


Complete example

lib/main.dart

import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

Future<Album> fetchAlbum(http.Client client) async {
  final response =
      await client.get(Uri.https('jsonplaceholder.typicode.com', 'albums/1'));

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON.
    return Album.fromJson(jsonDecode(response.body));
  } else {
    // If the server did not return a 200 OK response,
    // then throw an exception.
    throw Exception('Failed to load album');
  }
}

class Album {
  final int userId;
  final int id;
  final String title;

  Album({required this.userId, required this.id, required this.title});

  factory Album.fromJson(Map<String, dynamic> json) {
    return Album(
      userId: json['userId'],
      id: json['id'],
      title: json['title'],
    );
  }
}

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  MyApp({Key? key}) : super(key: key);

  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late final Future<Album> futureAlbum;

  @override
  void initState() {
    super.initState();
    futureAlbum = fetchAlbum(http.Client());
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fetch Data Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Fetch Data Example'),
        ),
        body: Center(
          child: FutureBuilder<Album>(
            future: futureAlbum,
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                return Text(snapshot.data!.title);
              } else if (snapshot.hasError) {
                return Text("${snapshot.error}");
              }

              // By default, show a loading spinner.
              return CircularProgressIndicator();
            },
          ),
        ),
      ),
    );
  }
}

 


test/fetch_album_test.dart

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';

import '../lib/main.dart';
import 'fetch_album_test.mocks.dart';

// Generate a MockClient using the Mockito package.
// Create new instances of this class in each test.
@GenerateMocks([http.Client])
void main() {
  group('fetchAlbum', () {
    test('returns an Album if the http call completes successfully', () async {
      final client = MockClient();

      // Use Mockito to return a successful response when it calls the
      // provided http.Client.
      when(client.get(Uri.https('jsonplaceholder.typicode.com', 'albums/1')))
          .thenAnswer((_) async => http.Response('{"userId": 1, "id": 2, "title": "mock"}', 200));

      expect(await fetchAlbum(client), isA<Album>());
    });

    test('throws an exception if the http call completes with an error', () {
      final client = MockClient();

      // Use Mockito to return an unsuccessful response when it calls the
      // provided http.Client.
      when(client.get(Uri.https('jsonplaceholder.typicode.com', 'albums/1')))
          .thenAnswer((_) async => http.Response('Not Found', 404));

      expect(fetchAlbum(client), throwsException);
    });
  });
}

 


Summary

この例では、Mockitoを使用して、Webサービスまたはデータベースに依存する関数またはクラスをテストする方法を学習しました。これは、Mockitoライブラリとモックの概念の簡単な紹介にすぎません。詳細については、Mockitoパッケージが提供するドキュメントを参照してください。

参考

https://flutter.dev/docs/cookbook/testing/unit/mocking

コメントを残す

メールアドレスが公開されることはありません。