LoginSignup
2
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-02-17

fakeは主にテストの際利用する適当なデータを生成するためのクレートです.以下のように適当な名前や電話番号などを生成できます.

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を用いて自分で定義した型からフェイクデータを作成してみます.コード全体はこちら.
以下のYoutubeUrlはYoutubeのUrlを表す型で,簡単なパースを実装しています.

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は引数として与えた乱数生成器からフェイクデータを作ります.

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")
    }
}

これで以下のようにフェイクデータが作成できます.

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しています.

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

fakeが便利な場面

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

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

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

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

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

#[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を使う場合は以下のようにできます.

#[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を変更したくなったとします.例えば以下ではアカウント名をフィールドに持つように変更しています.


#[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を変更する必要はありません.

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を使うことで,テストの時においてもその型について知っているべきインターフェースを明確にできます.

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2