Rust と歩んだ 7 年間:プロダクションコードのための実践的テスト (Rust.Tokyo2024)

24.2K Views

November 30, 24

スライド概要

profile-image

ユニークビジョン株式会社CTOです。

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

Rust と歩んだ 7 年間:プロダ クションコードのための実践的 テスト ユニークビジョン株式会社 CTO 青柳康平

2.

目次 目次 会社紹介と自己紹介 DBのカラム追加で壊れにくいテスト 外部APIをモックするテスト まとめ おしらせ Copyright ©Unique Vision Company, All Rights Reserved. 2

3.

ご紹介 ユニークビジョン株式会社 SNSマーケティングの領域でキャンペーンの実施や SNSアカウントを管理するツールなどを提供する会社 2024年10月時点で全社員 67人、エンジニア 33人 エンジニアは自社ツールの開発、キャンペーンの 案件開発を行っています。 取締役CTO X Marketing Partners 青柳康平 LINE販促・OMO部門の Technology Partner 主に開発をしながら、全社の技術方針や開 発方針を取りまとめている いわゆるプレイングマネージャー バックエンド開発とDB設計が得意 Copyright ©Unique Vision Company, All Rights Reserved. 3

4.

DBのカラム追加で壊れにくいテスト Copyright ©Unique Vision Company, All Rights Reserved. 4

5.

DBのカラム追加で壊れにくいテスト 壊れやすいテスト (Fragile Test)とは ● ● ● 機能追加やリファクタリングなどをすると、既存の多くの単体テストがエラーになっ てしまうことです コードを修正するたびに、大量のテストを修正することになり、時間がかかります これが日常的になると、メンテナンスがめんどくさくなり、単体テストをやらなくなった り、テストが通らなくても放置してしまいます Copyright ©Unique Vision Company, All Rights Reserved. 5

6.

DBのカラム追加で壊れにくいテスト DBでテーブルのカラム追加 ● ● 壊れやすいテストの原因はいくつかありますが、ここでは特にDBでテーブルのカラ ム追加について述べます DBを含んだ単体テストは以下のような手順になります ○ ○ ○ ● DBにレコードを追加して、事前条件を準備します コードを実行します 事後条件として処理結果と DBのレコードの状態を確認します 一度、単体テストを作った後にカラムが追加されると、特に事前状態の設定をして いる部分に影響が出やすく、単体テストがエラーになります。 Copyright ©Unique Vision Company, All Rights Reserved. 6

7.

DBのカラム追加で壊れにくいテスト 壊れやすいパターン (fixture) ● データの準備方法 ○ ○ ● 事前条件を満たすような SQLをファイルで用意します テストを行う時にその SQLを呼び出します カラムの追加が発生した場合 ○ ○ 作成したSQLファイルを修正します テストが多くなると修正が必要な SQLファイルも多くなります Copyright ©Unique Vision Company, All Rights Reserved. 7

8.
[beta]
DBのカラム追加で壊れにくいテスト
#[cfg(test)]
mod tests {
use postgresql::{prelude::*, setup_test, Pool, Error, execute_sql};
pub async fn fixtrue(pool: &Pool, key: &str) -> Result<(), Error> {
let path = format!("../../fixtures/{}.sql", key);
let content = tokio::fs::read_to_string(path).await?;
execute_sql(pool, &content).await?;
Ok(())
}
#[tokio::test]
async fn test_fragile_fixture() -> anyhow::Result<()> {
let pool = setup_test("postgres://user:pass@localhost/web", 5).await?;
fixtrue(&pool, "user_taro").await?;
fixtrue(&pool, "user_jiro").await?;
let result = Users::select_all(&pool).await?;
assert_eq!(result.len(), 2);
Ok(())
}
}
Copyright ©Unique Vision Company, All Rights Reserved.

8

9.

DBのカラム追加で壊れにくいテスト CREATE TABLE public.users ( uuid UUID NOT NULL PRIMARY KEY -- UUID ,user_name TEXT NOT NULL DEFAULT '' -- ユーザ名 ,user_mail TEXT NOT NULL DEFAULT '' -- ユーザメール ); INSERT INTO public.users ( INSERT INTO public.users ( uuid uuid ,user_name ,user_name ,user_mail ,user_mail ) VALUES ( ) VALUES ( ); gen_random_uuid() gen_random_uuid() ,'taro' ,'jiro' ,'[email protected]' ,'[email protected]' ); Copyright ©Unique Vision Company, All Rights Reserved. 9

10.

DBのカラム追加で壊れにくいテスト 壊れやすいパターン (関数) ● データの準備方法 ○ ○ ● レコードをINSERTする関数を準備する 単体テストでは事前条件として、関数を呼び出してデータを登録する カラムの追加が発生した場合 ○ ○ NotNullなカラムが追加されると、 INSERTの時にエラーになります 関数の修正は容易ですが、呼び出し側の修正が大変です Copyright ©Unique Vision Company, All Rights Reserved. 10

11.
[beta]
DBのカラム追加で壊れにくいテスト
#[cfg(test)]
mod tests {
use postgresql::{prelude::*, setup_test, Pool, sqlx};
const SQL: &str = r#"
INSERT INTO users (user_name, user_mail, created_at, updated_at)
VALUES ($1, $2, now(), now()) "#;
async fn add_user(pool: &Pool, user_name: &str, user_mail: &str) -> anyhow::Result<()> {
let _ = sqlx::query(SQL).bind(user_name).bind(user_mail).execute(pool).await?;
Ok(())
}
#[tokio::test]
async fn test_fragile_function() -> anyhow::Result<()> {
let pool = setup_test("postgres://user:pass@localhost/web", 5).await?;
add_user(&pool, "taro", "[email protected]").await?;
add_user(&pool, "jiro", "[email protected]").await?;
let users = Users::select_all(&pool).await?;
assert_eq!(users.len(), 2);
Ok(())
}
}
Copyright ©Unique Vision Company, All Rights Reserved.

11

12.

DBのカラム追加で壊れにくいテスト 壊れにくいパターン (Default) ● データの準備方法 ○ ○ ○ ● テーブルの同じ構造を持つ structを準備 Defaultを実装 利用側では好きな値に上書きできます カラムの追加が発生した場合 ○ ○ Defaultでエラーにならないような値を設定 利用する側の変更は必要無いです Copyright ©Unique Vision Company, All Rights Reserved. 12

13.
[beta]
DBのカラム追加で壊れにくいテスト
#[derive(Serialize, Deserialize, Debug, Clone, FromRow)]
pub struct Users {
pub uuid: Uuid,
pub user_name: String,
pub user_mail: String,
}

impl Users {
pub async fn insert(&self, pool: &Pool) -> Result<Self, sqlx::Error> {
sqlx::query_as(INSERT_SQL)
.bind(self.uuid)
.bind(&self.user_name)
.bind(&self.user_mail)
.fetch_one(pool)
.await
}
}

Copyright ©Unique Vision Company, All Rights Reserved.

13

14.
[beta]
DBのカラム追加で壊れにくいテスト
impl Default for Users {
fn default() -> Self {
Self {
uuid: Uuid::new_v4(),
user_name: "taro".to_string(),
user_mail: "[email protected]".to_string(),
}
}
}

Copyright ©Unique Vision Company, All Rights Reserved.

14

15.
[beta]
DBのカラム追加で壊れにくいテスト
#[cfg(test)]
mod tests {
use postgresql::{prelude::*, setup_test};
#[tokio::test]
async fn test_fragile_default() -> anyhow::Result<()> {
let pool = setup_test("postgres://user:pass@localhost/web", 5).await?;

Users::default().insert(&pool).await?;
let user = Users::default(); {
user_name: "jiro".to_string(),
user_mail: "[email protected]".to_string(),
..Default::default()
};
user.insert(&pool).await?;

let users = Users::select_all(&pool).await?;
assert_eq!(users.len(), 2);
Ok(())
}
}

Copyright ©Unique Vision Company, All Rights Reserved.

15

16.

DBのカラム追加で壊れにくいテスト Defaultの課題 ● ● Defaultでは値を1つして持つことができないです。 他の値を参照して値が設定できない。例えばnameからmailを作成するなど Copyright ©Unique Vision Company, All Rights Reserved. 16

17.

DBのカラム追加で壊れにくいテスト 壊れにくいパターン (Builder) ● データの準備方法 ○ ○ ○ ● crate derive_builderを使ってstructのbuilderを生成します builderを受け取って、 insertするためのmake関数を用意します その関数の中で適切の初期値を設定します カラムの追加が発生した場合 ○ ○ ○ structにカラムを追加します make関数に適切な初期値を設定します 利用する側の変更は必要無いです Copyright ©Unique Vision Company, All Rights Reserved. 17

18.
[beta]
DBのカラム追加で壊れにくいテスト
#[derive(Serialize, Deserialize, Debug, Clone, Builder, Default, FromRow)]
#[builder(setter(into), default, field(public))]
pub struct Users {
pub uuid: Uuid,
pub user_name: String,
pub user_mail: String,
}
impl Users {
pub async fn insert(&self, pool: &Pool) -> Result<Self, sqlx::Error> {
sqlx::query_as(INSERT_SQL)
.bind(self.uuid)
.bind(&self.user_name)
.bind(&self.user_mail)
.fetch_one(pool).await
}
}

Copyright ©Unique Vision Company, All Rights Reserved.

18

19.
[beta]
DBのカラム追加で壊れにくいテスト
pub async fn make_users(
pool: &Pool,
builder: &mut users::UsersBuilder,
) -> Result<Users, sqlx::Error> {
if builder.uuid.is_none() {
builder.uuid(Uuid::new_v4());
}

if builder.user_name.is_none() {
builder.user_name("taro");
}

if builder.user_mail.is_none() {
builder.user_mail(format!(
"{}@example.com",
builder.user_name.as_ref().unwrap()
));
}

builder.build().unwrap().insert(pool).await
}
Copyright ©Unique Vision Company, All Rights Reserved.

19

20.
[beta]
DBのカラム追加で壊れにくいテスト
async fn test_fragile_builder() -> anyhow::Result<()> {
let pool = setup_test("postgres://user:pass@postgresql/web", 5).await?;
let _ = make_users(&pool, UsersBuilder::default().user_name("taro")).await?;
let _ = make_users(&pool, UsersBuilder::default().user_name("jiro")).await?;
let users = Users::select_all(&pool).await?;
assert_eq!(users.len(), 2);
Ok(())
}

Copyright ©Unique Vision Company, All Rights Reserved.

20

21.

DBのカラム追加で壊れにくいテスト Builderのメリット ● ● ● make_users関数の中では未指定の値はデフォルト値を設定 mailの値をnameの値から作成するような柔軟なコードが書けます 目的ごとのmake関数を作ることができます ○ ○ ● make_users_admin(&pool, &mut user_builder) make_users_normal(&pool, &mut user_builder) 複数のテーブルをまとめるmake関数を作ることができます。作成の順番や参照す るカラムなどはmake関数内部で吸収できます。 Copyright ©Unique Vision Company, All Rights Reserved. 21

22.
[beta]
DBのカラム追加で壊れにくいテスト
pub async fn make_company_users(
pool: &Pool,
company_builder: &mut companies::CompaniesBuilder,
user_builder: &mut users::UsersBuilder,
) -> Result<(Companies, Users), sqlx::Error> {
let company = make_companies(pool, company_builder).await?;
user_builder.company_uuid(company.uuid);
let user = make_users(pool, user_builder).await?;
Ok((company, user))
}

Copyright ©Unique Vision Company, All Rights Reserved.

22

23.

DBのカラム追加で壊れにくいテスト DBのカラム追加で壊れにくいテストのまとめ ● ● ● テストの事前条件としてテーブルにレコードいれる時にfixtureや関数だとカラムを 変更したときに修正する範囲が大きくなります structにDefaultを用いることで緩和することができます。ただし値は一つしか選べ ません structにbuilderを用いれば柔軟に値を制御することができます Copyright ©Unique Vision Company, All Rights Reserved. 23

24.

外部APIをモックするテスト Copyright ©Unique Vision Company, All Rights Reserved. 24

25.

外部APIをモックするテスト SNSのAPIのcrateを自社で開発しています。 ● 𝕏 ○ ● Pinterest ○ ● twapi-v2 pinterest-api TikTok ○ ○ tiktok-business tiktokapi-v2 ユニークビジョンでは様々なSNSのキャンペーンを提供しています。そのためAPIを利用 するためのcrateが重要になります。 Copyright ©Unique Vision Company, All Rights Reserved. 25

26.

外部APIをモックするテスト キャンペーンでの外部 APIの利用方法 ● キャンペーンの参加を確認するための情報収集 ○ ○ ○ ○ ● キーワード検索してユーザーの投稿を回収 Webhookを用意してユーザーからのリアクションの回収 ユーザーを特定して投稿一覧の取得 ユーザーのフォローチェック ユーザーに対する返信 ○ ○ リプライによる返信 DM送信 Copyright ©Unique Vision Company, All Rights Reserved. 26

27.

外部APIをモックするテスト 外部APIをモックする ● 外部APIを単体テストでは呼べないです ○ ○ ○ ● 正常系と異常系 ○ ○ ○ ● 単体テストを動かしすぎてレートリミットになる可能性があります 外部APIの不調の影響を受けて、テストが失敗しやすくなります エラーが起きたときに、どちらの問題なのか確認するのが大変 正常系と同じくらい異常系もテストしたいです 異常系のテストは、例えばレートリミットが起きた場合に、呼び出しを制御するロジックが正しく動く か確認したいです 本番では起きにくいロジックなので、単体テストで確認したいです 単体テストではモックを利用します ○ ○ ローカルで動くのでテストの実行が早くなります 任意の状況が作り出せます Copyright ©Unique Vision Company, All Rights Reserved. 27

28.

外部APIをモックするテスト crate Mockitoを利用 「Rust の HTTP モック! Mockito は、Rust で HTTP モックを生成および配信するためのライブラリです。統合テ ストやオフライン作業に使用できます。 Mockito は、モックを作成、配信、削除する HTTP サーバーのローカル プールを実行します。」 (READMEより) Copyright ©Unique Vision Company, All Rights Reserved. 28

29.

外部APIをモックするテスト Mockitoの機能 ● ● ● ● ● ● ● ● ● HTTP1/2をサポート テストを並行して実行します 幅広いリクエストマッチャー(正規表現、JSON、クエリパラメータなど)が付属してい ます モックが呼び出されたことを確認します (スパイ) 複数のホストを同時にモックします 同期インターフェイスと非同期インターフェイスを公開します エラーが発生した場合に、最後に一致しないリクエストの色付きの差分を出力しま す シンプルで直感的な API 素晴らしいロゴ Copyright ©Unique Vision Company, All Rights Reserved. 29

30.

外部APIをモックするテスト これが素晴らしいロゴです。 社内でこれを紹介する時に「モックイトウ、モックイトウ」と発音していたら、「モキート」で すよと言われて恥ずかしかったです。 上記ロゴを見てモヒートだとわかりましたか? ちなみにモヒートのスペルは「Mojito」です Copyright ©Unique Vision Company, All Rights Reserved. 30

31.

外部APIをモックするテスト Mockitoの使い方 ● ● ● ● インスタンスを作成して、待ち受けるリクエストと返却するレスポンスを設定します。 モックのためのホストとポートが振り出されます。http://localhost:1111 指定したURLにアクセスすると、設定したレスポンスが返ります また、呼び出されたかどうかの確認もできます Copyright ©Unique Vision Company, All Rights Reserved. 31

32.
[beta]
外部APIをモックするテスト
#[test]
fn test_something() {
let mut server = mockito::Server::new();
let host = server.host_with_port();
let url = server.url();

let mock = server.mock("GET", "/hello")
.with_status(201)
.with_header("content-type", "text/plain")
.with_header("x-api-key", "1234")
.with_body("world")
.create();

let client = reqwest::Client::new();
let response = client.get(format!("{}/hello", url)).send().await?;
assert_eq!(response.status(), 201);
assert_eq!("world", response.text().await?);

mock.assert();
}

Copyright ©Unique Vision Company, All Rights Reserved.

32

33.

外部APIをモックするテスト Mockitoを使った外部 APIのテスト ● ● ● APIのライブラリはそれぞれアクセスすべきエンドポイントを持ちます モックするためには、そのエンドポイントを切り替える必要があります 利用するためにラッパーを用意する? ○ ○ モックでレスポンスの JSONを作成できるが、適切な型に変換する部分がライブラリの中にあったら ラッパー内部でそれを作る必要があります エンドポイントが沢山あるとラッパーを用意するのも大変です Copyright ©Unique Vision Company, All Rights Reserved. 33

34.

外部APIをモックするテスト ライブラリで直接エンドポイントを切り替える ● ライブラリではエンドポイントごとに向き先を変更するオプションが渡せます ○ ○ ● オプションには prefix_urlが設定できる APIを呼び出すための structがbuilderになっていて、オプションが渡せる このようなことが可能なのは自社でライブラリを開発しているから! Copyright ©Unique Vision Company, All Rights Reserved. 34

35.
[beta]
外部APIをモックするテスト
#[tokio::test]
async fn test_get_2_tweets_search_recent_oauth_mock_rate_limet() -> Result<()> {
let mut server = Server::new_async().await;

let body = json!({
"status": "error",
"title": "Too Many Requests"
});

let mock = server
.mock("GET", "/2/tweets/search/recent")
.match_query(mockito::Matcher::Any)
.with_header("content-type", "application/json")
.with_body(body.to_string())
.with_status(429)
.create_async()
.await;

Copyright ©Unique Vision Company, All Rights Reserved.

35

36.
[beta]
外部APIをモックするテスト
let twapi_options = api::TwapiOptions {
prefix_url: Some(server.url()),
..Default::default()
};

let builder = get_2_tweets_search_recent::Api::open("東京")
.max_results(10)
.twapi_options(twapi_options)
.build(&make_oauth());

match execute_twitter::<get_2_tweets_search_recent::Response>(builder).await {
Err(Error::Twitter(e, _value, _headers)) => {
assert_eq!(e.status_code.as_u16(), 429);
assert_eq!(e.title, "Too Many Requests");
}
_ => panic!("unexpected error"),
}

mock.assert();

Ok(())
}

Copyright ©Unique Vision Company, All Rights Reserved.

36

37.

外部APIをモックするテスト 環境変数による一括変更も用意 ● ● ● 環境変数により全てのエンドポイントの向き先を切り替える機能もあります node.jsなどで簡単に作ったMockサーバー向けを用意して、そちらでAPIをモック します。 ページング機能の確認などAPIを複数回呼び出した場合はMockitoで対応するよ りもMockサーバーの方が楽です Copyright ©Unique Vision Company, All Rights Reserved. 37

38.

外部APIをモックするテスト まとめ ● ● ● ● ユニークビジョンではSNSの外部APIを呼ぶことが多いです 異常系の単体テストがしたいが外部のAPIでは発生できないです Mockitoを使うことでURLのプレフィックスを変えてモックできます 外部APIのライブラリにURLのプレフィックスを変えるパラメーターを用意すること で、単体テストできるようになります Copyright ©Unique Vision Company, All Rights Reserved. 38

39.

まとめ 実践で使っているテストを2つ紹介しました ● DBのカラム追加で壊れにくいテスト カラム追加があっても一部を修正するだけで対応可能になりました ● 外部APIをモックするテスト モックを使うことで発生しにくい状況を作り出して、単体テストすることができるよう になりました。 Copyright ©Unique Vision Company, All Rights Reserved. 39

40.

おしらせ Copyright ©Unique Vision Company, All Rights Reserved. 40

41.

ご清聴ありがとうございました Copyright ©Unique Vision Company, All Rights Reserved. 41