🐕

Rustで𝕏(Twitter)APIをモックする

2024/03/11に公開

目的

本気で𝕏APIを使っていると、レートリミットが発生した時やユーザーのOAuthが外れたときなどのエラーハンドリングを書くことになってきます。
ですが、テストで状況を再現するのが大変だったり、めんどくさかったりします。
特にレートリミットはそれに達するほどAPIを叩いたりすると、利用しているアカウントがバンされたりするので、頭が痛いところです。
そこでAPIをモックしてほしい結果を得ることにします。

コード

mockito

本家にあるテストコードを引用します。テストケース内でサーバーをたてて、そのサーバーにアクセスするURLを取得して、任意の結果を返すように設定できます。この例では/helloにアクセスするとworldが戻ります。

#[cfg(test)]
mod tests {
  #[test]
  fn test_something() {
    // Request a new server from the pool
    let mut server = mockito::Server::new();

    // Use one of these addresses to configure your client
    let host = server.host_with_port();
    let url = server.url();

    // Create a mock
    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();

    // Any calls to GET /hello beyond this line will respond with 201, the
    // `content-type: text/plain` header and the body "world".

    // You can use `Mock::assert` to verify that your mock was called
    // mock.assert();
  }
}

twapi-v2

𝕏APIの接続には自作のライブラリtwapi-v2を使います。
ライブラリ側ではテストの時はmockitoのURLをプレフィックスとして受け取りたいので、そのように修正しました。

const ENV_KEY: &str = "TWAPI_V2_TWITTER_API_PREFIX_API";
const PREFIX_URL_TWITTER: &str = "https://api.twitter.com";

pub fn clear_prefix_url() {
    std::env::set_var(ENV_KEY, PREFIX_URL_TWITTER);
}

pub fn setup_prefix_url(url: &str) {
    std::env::set_var(ENV_KEY, url);
}

pub(crate) fn make_url<S: AsRef<str>>(post_url: S) -> String {
    let prefix_url = std::env::var(ENV_KEY).unwrap_or(PREFIX_URL_TWITTER.to_owned());
    format!("{}{}", prefix_url, post_url.as_ref())
}

この部分は悩みました。最初OnceLockを使おうかと思ったんですが、これだと複数のケースが合った場合それぞれがmockitoのサーバーを立ち上げると違うURLになるとうまくいきません。ただし実際に動かしたところ同じURLになっていたので気にしなくてもいいのかもです。しかしもう一つ問題があって本当のテストとまぜると動かなくなります。

苦肉の策で環境変数にしましたが、非同期のテストおかしくなりそうだし、環境変数汚染するし、環境変数の無い世界だとどうなるかわからないです。何かうまい方法があったら実装を変更するかもしれません。

テスト

レートリミットをモックしたテストは以下のようになります。
実際にエラーを起こせないので、推測でエラーを書いてますが、基本status_codeだけチェックすれば良いと思います。ちなみにモックなのでOAuth4つ組は適当でも動きます。

#[tokio::test]
async fn test_get_2_tweets_search_recent_oauth_mock_rate_limet() -> Result<()> {
    let mut server = Server::new_async().await;
    api::setup_prefix_url(&server.url());

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

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

    let auth = OAuthAuthentication::new(
        std::env::var("CONSUMER_KEY").unwrap_or_default(),
        std::env::var("CONSUMER_SECRET").unwrap_or_default(),
        std::env::var("ACCESS_KEY").unwrap_or_default(),
        std::env::var("ACCESS_SECRET").unwrap_or_default(),
    );
    let builder = get_2_tweets_search_recent::Api::open("東京")
        .max_results(10)
        .build(&auth);
    let res = execute_twitter::<get_2_tweets_search_recent::Response>(builder).await;
    match res {
        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(())
}

まとめ

ということで、実装に納得いっていないところもありますが、モックが動くようになりました。
実は𝕏APIのモックを書くのは長い間の悲願でした。ようやくこういうのが書けるくらにRustに慣れてきたなと感じます。

Discussion