【Rust】fakeでテスト用のフェイクデータを作成する

[fake](https://github.com/cksac/fake-rs)は主にテストの際利用する適当なデータを生成するためのクレートです．以下のように適当な名前や電話番号などを生成できます．
```rust
use fake::faker::name::raw::*;
use fake::locales::JA_JP;

use fake::faker::phone_number::raw::*;

let fake_name = LastName(JA_JP).fake::<String>();
println!("fake_name: {fake_name}");
// 例: 宮本

let cell_phone_number = CellNumber(JA_JP).fake::<String>();
println!("cell_phone_number: {cell_phone_number}");
// 例: 090-7604-5037
```

今回はこのfakeを用いて自分で定義した型からフェイクデータを作成してみます．コード全体は[こちら](https://github.com/deepgreenAN/rust-fake-example).
以下の`YoutubeUrl`はYoutubeのUrlを表す型で，簡単なパースを実装しています．

```rust
use std::{collections::HashMap, fmt::Display};
use url::Url;

#[derive(Clone, Debug)]
struct YoutubeUrl {
    video_id: String,
}

impl TryFrom<String> for YoutubeUrl {
    type Error = DomainError;
    fn try_from(value: String) -> Result<Self, Self::Error> {
        let url = Url::parse(&value)?;

        if "www.youtube.com" == url.host_str().ok_or(DomainError::InvalidYoutubeUrlError)? {
            let query_map = url
                .query_pairs()
                .into_owned()
                .collect::<HashMap<String, String>>();
            let video_id = query_map
                .get("v")
                .ok_or(DomainError::InvalidYoutubeUrlError)?;
            Ok(YoutubeUrl {
                video_id: video_id.clone(),
            })
        } else {
            Err(DomainError::InvalidYoutubeUrlError)
        }
    }
}

impl Display for YoutubeUrl {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "https://www.youtube.com/watch?v={}", self.video_id)
    }
}

```
この型からフェイクデータを作れるようにしてみます．

## Dummyトレイトを実装する
フェイクデータを作るためには`fake::Dummy`トレイトを実装します．実際に利用するときに使うのは`fake::Fake`トレイトですが，`From`トレイトに対する`Into`トレイトのように`Dummy`トレイトを実装することで自動で実装されます．トレイトの型パラメーターには任意の型を指定でき，例えば`std::ops::Range`などを指定して生成されるデータの設定などに利用できます．今回はシンプルに`fake::Faker`を指定することで，`Faker.fake`メソッドで`YoutubeUrl`のフェイクデータを作成できます．
`dummy_with_rng`は引数として与えた乱数生成器からフェイクデータを作ります．

```rust
impl Dummy<Faker> for YoutubeUrl {
    fn dummy_with_rng<R: rand::Rng + ?Sized>(_config: &Faker, rng: &mut R) -> Self {
        const ID_CHARS: &str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_";
        let video_id = StringFaker::with(Vec::from(ID_CHARS), 11).fake_with_rng::<String, R>(rng);
        format!("https://www.youtube.com/watch?v={video_id}")
            .try_into()
            .expect("Generate Fake Error")
    }
}
```
これで以下のようにフェイクデータが作成できます．
```rust
use fake::{Fake, Faker};

let youtube_url: YoutubeUrl = Faker.fake();
println!("youtube_url: {youtube_url}");
//例: youtube_url: https://www.youtube.com/watch?v=bGVAI5NO7B9
```

フィールドが全て`Dummy`トレイトを実装しておりそのデフォルトのフェイクデータを利用する場合はderiveマクロが使えます．例えば以下ではYoutubeのブックマークの型`YtBookMark`を定義し`Dummy`をderiveしています．

```rust
#[derive(Clone, Debug, Dummy)]
struct YtBookMark {
    url: YoutubeUrl,
    tags: Vec<String>,
}
```

## fakeが便利な場面
fakeを利用するメリットの一つはテストの際にテストモジュール側がその型のコンストラクタなどのインターフェースを知っている必要が無いことです．例えば以下のように`YtBookMark`のスライスに対してタグを追加する関数をテストすることを考えてみます．

```rust
fn add_tag(bookmarks: &mut [YtBookMark], tag: String) {
    bookmarks.iter_mut().for_each(|bookmark| {
        bookmark.tags.push(tag.clone());
    })
}
```

`YtBookMark`型は以下のコンストラクタを持つとします．

```rust
impl YtBookMark {
    fn new(
        url_str: String,
        tags: Vec<String>,
    ) -> Result<YtBookMark, DomainError> {
        Ok(Self {
            url: url_str.try_into()?,
            tags,
        })
    }
}
```

fakeを使わない場合，以下のように`add_tag`をテストできます．

```rust
#[test]
fn test_add_tag_v1() {
	let mut bookmarks = vec![
		YtBookMark::new(
			"https://www.youtube.com/watch?v=5gGha71avA5".to_string(),
			vec!["Rust".to_string()],
		)
		.unwrap(),
		YtBookMark::new(
			"https://www.youtube.com/watch?v=IN9jNrGAlJW".to_string(),
			vec!["Math".to_string()],
		)
		.unwrap(),
	];

	let added_tag = "Computer".to_string();

	add_tag(&mut bookmarks, added_tag.clone());

	bookmarks.into_iter().for_each(|bookmark| {
		assert!(bookmark.tags.into_iter().any(|tag| { tag == added_tag }));
	});
}
```

fakeを使う場合は以下のようにできます．

```rust
#[test]
fn test_add_tag_v2() {
    let mut bookmarks = (0..100)
        .map(|_| Faker.fake::<YtBookMark>())
        .collect::<Vec<_>>();

	let added_tag = "Computer".to_string();

	add_tag(&mut bookmarks, added_tag.clone());

	bookmarks.into_iter().for_each(|bookmark| {
		assert!(bookmark.tags.into_iter().any(|tag| { tag == added_tag }));
	});
}
```

なんらかの事情で`YtBookMark`を変更したくなったとします．例えば以下ではアカウント名をフィールドに持つように変更しています．

```rust

#[derive(Clone, Debug, Dummy)]
struct YtBookMark {
    url: YoutubeUrl,
    tags: Vec<String>,
+   account_name: String,
}

impl YtBookMark {
    fn new(
        url_str: String,
        tags: Vec<String>,
+       account_name: String,
    ) -> Result<YtBookMark, DomainError> {
        Ok(Self {
            url: url_str.try_into()?,
            tags,
+           account_name,
        })
    }
}
```

この変更に伴って，`test_add_tag_v1`は変更しなければなりません．一方で`test_add_tag_v2`を変更する必要はありません．

```rust
let mut bookmarks = vec![
    YtBookMark::new(
        "https://www.youtube.com/watch?v=5gGha71avA5".to_string(),
        vec!["Rust".to_string()],
+       "John".to_string(),
     )
     .unwrap(),
     YtBookMark::new(
         "https://www.youtube.com/watch?v=IN9jNrGAlJW".to_string(),
         vec!["Math".to_string()],
+        "Mark".to_string(),
     )
     .unwrap(),
];
```

`add_tag`関数は本来`YtBookMark`の作成ではなく`YtBookMark`の修正に関連した関数です．fakeを使うことで，テストの時においてもその型について知っているべきインターフェースを明確にできます．