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