🦔

DartのMockitoでカスタムMacherを作る

2023/08/26に公開

Mockitoは、Dartでモック、スタブ、およびスパイを簡単に作成できるライブラリです。Mockitoには、テストに使用される様々なMatcherが用意されていますが、既存のMatcherだとうまく行かないことがあったので、自作する方法を調べて実際に実装してみました。

Matcherとは何にか?

Matcherは、テスト中に値を比較するために使用されるオブジェクトです。Matcherを使用すると、テストの読みやすさと保守性が向上します。Matcherは、単純な値と複雑なオブジェクトを比較するために使用できます。

Matcherの使用方法

説明のためにPersonRepositoryクラスとPersonRepositoryクラスを定義します。

class PersonRepository {
  void save(String name, int age) {
    // DBなどの永続化する処理
  };
}

class PersonService {
  final PersonRepository personRepository;

  RegistrationViewModel(this.personRepository);
 
  void register(String name, int age) {
    // 名前や年齢のバリデーション

    personRepository.save(name, age);  
  }
}

PersonRepositoryはDBやローカルに保存する役割を持ち、saveメソッドはDBやローカルストレージなどに名前と年齢の情報を保存する役割を持ちます。PersonServiceregisterメソッドは何らかのボタンなどをタップされた時に使われるイメージで、正しい値が入力されたことの確認をし、保存の処理を呼び出す役割です。

ここで「指定された名前と年齢で保存処理を呼び出すこと」という検証のテストを考えます。

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

import 'person_service_test.mocks.dart';

([PersonRepository])
void main() {
  late PersonService sut;
  late MockPersonRepository personRepository;

  setUp(() {
    costRepository = MockCostRepository();
    sut = RegistrationViewModel(costRepository);
  });

  test('registerは指定された名前と年齢で保存処理を呼び出すこと', () {
		var name = 'Taro';
		var age = 21;
		
		sut.register(name, age);

		verify(personRepository.save(argThat('Taro', 21)).called(1);
  });
}

上記のようにargThatを用いて指定された名前と年齢で保存処理を呼び出すことを検証できます。

Matcherを自作する

同姓同名で同じ年齢の人がいた時に区別できるよう、Personクラスを作成し、ユニークなIDを持つようにします。それに従いPersonRepositoryの引数とPersonServiceの実装を変更します。

import 'package:uuid/uuid.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'person.freezed.dart';


class Person {
  factory Person._({
    required String id,
    required String name,
    required int age
  }) = _Person;

  factory Person.of(String name, int age) {
    return Person._(
      id: const Uuid().v4(),
      name: name,
      age: age
    );
  }
}

class PersonRepository {
  void save(Person person) {
    // DBなどの永続化する処理
  };
}

class PersonService {
  final PersonRepository personRepository;

  RegistrationViewModel(this.personRepository);
 
  void register(String name, int age) {
    personRepository.save(Person.of(name, age));  
  }
}

先ほどと同様に「指定された名前と年齢で保存処理を呼び出すこと」という検証のテストを考えます。IDは生成のたびに変化してしまうので以下のように書いても失敗してしまいます。

test('registerは指定された名前と年齢で保存処理を呼び出すこと', () {
  var name = 'Taro';
	var age = 21;
		
	sut.register(name, age);

  var expected = Person.of(name, age); 
	verify(personRepository.save(argThat(expected)).called(1);
 });

そこで、名前と年齢が等しい時にマッチするようなカスタムMatcherを作ります。


class PersonMatcher extends Matcher {
  final Person expectedPerson;

  PersonMatcher(this.expectedCost);

  
  Description describe(Description description) {
    return description.add('Person: ${expectedCost.toString()}');
  }

  
  bool matches(item, Map matchState) {
    return item is Person &&
        item.name == expectedPerson.name &&
        item.age == expectedPerson.age;
  }
}

カスタムMatcherを作成する際の基本的な考え方は、Matcherクラスを継承し、matchesメソッドをオーバーライドすることです。このカスタムMacherを使うことで、以下のようにテストを書くことができます。

test('registerは指定された名前と年齢で保存処理を呼び出すこと', () {
  var name = 'Taro';
	var age = 21;
		
	sut.register(name, age);

  var expected = Person.of(name, age); 
	verify(personRepository.save(argThat(PersonMatcher(expected))).called(1);
});

まとめ

カスタムMatcherを作成することで、Mockitoを使用してテストをより効果的に実行できます。カスタムMatcherは、テストの可読性と保守性を向上させ、より複雑なオブジェクトの比較も可能にします。

DartのMockitoは元々JavaのMockitoに影響されて作られているので、Java側でできることはある程度できそうな気がします。

最後に

初めて友人と二人でスマホアプリをリリースしました。Spenderというアプリです。

https://play.google.com/store/apps/details?id=com.torichu.spender&pli=1
https://apps.apple.com/app/spender/id6461213140

このアプリ開発過程で今回のような学びがありました。また何か学びがあれば書いていきます。
アプリ開発の経緯などはこちらから

https://note.com/nugget_okapi/n/n7c52a591b16d

Discussion