1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TauriAdvent Calendar 2024

Day 15

rust知らない人のためのtauri

Posted at

この記事は、Tauriアドベントカレンダー 2024 15日目の記事です。

はじめに

Rustの学習としてTauriを使ってみたのですが、やはりRust、難しかったので、詰まった部分や最初から知っておきたかったライブラリなどを紹介していきます。

詰まった部分やTips

デバッグ

Debugトレイトがついた型なら以下の形でデバッグできます。

println!("{:#?}", hoge);
/* 
=> Hoge {
    foo: 1,
    bar: false,
}
*/

Commandのテスト

私が使っているtauri: 2.0.6では tauri::test::mock_builder の実行でエラーが出て tauri::Statetauri::AppHandle が作成できなかったので以下のようにエラーが出ないよう回避しました。

tauri::Stateを使ったCommandのテスト

stateの中身だけを受ける関数を作ってそのテストをしました。

中身だけを受ける関数を作成することでテストでtauri::Stateを作成する必要がなくなります。

async fn get_game_state_exec(my_state: &MyState) -> Result<(),String> {
    my_state.call_function()
}

#[tauri::command]
#[specta::specta]
pub async fn get_game_state(state: tauri::State<'_, MyState>) -> Result<(), String> {
    get_game_state_exec(state.inner()).await
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::commands::dto::GameState;
    // 非同期処理があるなら#[tokio:test]が必要
    #[tokio::test]
**    async fn should_return_game_state() {
        assert_eq!(get_game_state_exec(&container).await, Ok(()));
    }
}

tauri::AppHandleを使ったCommandのテスト

こちらはtauri::Apphandleをラップすることでtauri::AppHandleの作成を回避しています。

ラップしてあげることでテストでtauri::AppHandleを作成する必要がなくなります。

mockallを使ってますが、これについては後述します。

#[mockall::automock]
pub trait CommandAppHandle {
    fn version(&self) -> Result<String, String>;
}

pub struct CommandAppHandleImpl {
    app: tauri::AppHandle,
}

impl CommandAppHandleImpl {
    pub fn new(app: tauri::AppHandle) -> Self {
        Self { app }
    }
}
impl CommandAppHandle for CommandAppHandleImpl {
    fn version(&self) -> Result<String, String> {
        self.app
            .config()
            .version
            .clone()
            .ok_or("No version found".to_string())
    }
}

async fn hello_exec(state: impl CommandAppHandle) -> Result<(), String> {
    println!("result: {:?}", state.version());
    Ok(())
}

#[tauri::command]
#[specta::specta]
pub async fn hello(state: tauri::AppHandle) -> Result<(), String> {
    let command_handle = CommandAppHandleImpl::new(state);
    hello_exec(command_handle).await
}

#[cfg(test)]
mod tests {
    use crate::commands::app_handle::MockCommandAppHandle;

    use super::*;
    #[tokio::test]
    async fn should_return_ok() {
        let mut app_handle = MockCommandAppHandle::new();
        app_handle
            .expect_version()
            .returning(|| Ok("test_version".to_string()));
        assert_eq!(hello_exec(app_handle).await, Ok(()));
    }
}

便利なライブラリ

mockall

簡単にモックが作れるライブラリです。

テストに便利

use mockall::automock;

#[automock]
trait MyTrait {
    fn foo(&self, x: u32) -> u32;
}

fn call_with_four(x: &dyn MyTrait) -> u32 {
    x.foo(4)
}

#[cfg(test)]
mod tests {
    use mockall::predicate;
    #[test]
    fn should_return_5() {
        let mut mock = super::MockMyTrait::new();
        mock.expect_foo()
            .with(predicate::eq(4))
            .times(1)
            .returning(|x| x + 1);
        assert_eq!(5, super::call_with_four(&mock));
    }
}

dotenvyenvy

dotenvyは .envファイルを環境変数に読み込むクレート

envyは環境変数を構造体で取得するクレート

です。

use serde::Deserialize;

#[derive(Deserialize, Debug)]
pub struct Config {
    pub foo: u16,
    pub bar: bool,
    pub baz: String,
    pub boom: Option<u64>,
}

fn main(){
    dotenvy::dotenv().expect(".env file not found");
    let config = envy::from_env::<env::Config>().expect("environment variables not found");
    println!("{:#?}", config);
    /*
	    => Config {
			    foo: 1,
			    bar: false,
			    baz: "",
			    boom: None,
			}
    */
}

shaku

DIコンテナ

mockallと組み合わせてテストが書きやすくなる

いろいろ調べましたが、shakuを使ったDIが一番シンプルで手っ取り早いです。

pub struct Player {
    pub is_main_player: bool,
}

#[mockall::automock]
pub trait PlayerRepository: shaku::Interface {
    fn get_party_of_main_player(&self) -> Result<Vec<Player>, DomainError>;
    fn get_party(&self, party_id: i32) -> Result<Vec<Player>, DomainError>;
}

#[derive(shaku::Component)]
#[shaku(interface = PlayerRepository)]
pub struct PlayerRepositoryImpl;

impl player::PlayerRepository for PlayerRepositoryImpl {
    fn get_party_of_main_player(
        &self,
    ) -> Result<Vec<player::Player>, crate::domain::error::DomainError> {
        todo!()
    }

    fn get_party(
        &self,
        party_id: i32,
    ) -> Result<Vec<player::Player>, crate::domain::error::DomainError> {
        todo!()
    }
}

shaku::module! {
    pub AppModule {
        components = [crate::domain::player::PlayerRepositoryImpl,],
        providers = []
    }
}

#[cfg(test)]
pub mod test {
    use shaku::ModuleBuilder;

    use super::*;
    pub fn create_mock_module_builder() -> ModuleBuilder<AppModule> {
		    // テスト時にいちいち置き換えるコードを書かなくていいように、ここでモックを入れちゃうといいです。
        AppModule::builder()
            .with_component_override::<dyn crate::domain::player::PlayerRepository>(Box::new(
                crate::domain::player::MockPlayerRepository::new(),
            ))
    }
}

async_trait

非同期関数を持つtraitに対してshakuを使うとobject safe系のエラーが出るので対処する必要がるのですが、async_traitを使うとそういったRust特有のよくわかんないエラーを無視することができます。

次のコードはエラーが出ます。


#[mockall::automock]
pub trait GameStateRepository: shaku::Interface {
    async fn get_game_state(&self) -> GameState;
}

// ↓で「for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>」が出る
#[derive(shaku::Component)]
#[shaku(interface = game_state::GameStateRepository)]
pub struct GameStateRepositoryImpl {
    #[shaku(inject)]
    valorant_api: Arc<dyn crate::infra::valorant::ValorantAPI>,
}

impl game_state::GameStateRepository for GameStateRepositoryImpl {
    async fn get_game_state(&self) -> game_state::GameState {
        match self.valorant_api.get_game_state().await {
            Ok(_) => game_state::GameState::Login,
            Err(_) => game_state::GameState::None,
        }
    }
}

以下の修正をするだけでエラーが消えます。

+ #[async_trait::async_trait]
#[mockall::automock]
pub trait GameStateRepository: shaku::Interface {
    async fn get_game_state(&self) -> GameState;
}

#[derive(shaku::Component)]
#[shaku(interface = game_state::GameStateRepository)]
pub struct GameStateRepositoryImpl {
    #[shaku(inject)]
    valorant_api: Arc<dyn crate::infra::valorant::ValorantAPI>,
}

+ #[async_trait::async_trait]
impl game_state::GameStateRepository for GameStateRepositoryImpl {
    async fn get_game_state(&self) -> game_state::GameState {
        match self.valorant_api.get_game_state().await {
            Ok(_) => game_state::GameState::Login,
            Err(_) => game_state::GameState::None,
        }
    }
}

anyhowthiserror

ただでさえシンプルなrustのエラーハンドリングがさらに楽になります。

thiserrorで std::error::Error トレートを実装したエラーが簡単に作成でき、anyhowでそれやio:Errorなどをまとめて処理することができます。

// ↓便利
#[derive(Debug, thiserror::Error)]
enum MyError {
    #[error("this is less than {0}")]
    Small(i32),
    #[error("this is more than {0}")]
    Big(i32),
}

// 本来なら `pub fn sample(i: i32) -> Result<i32, io::Error | MyError> `みたいなことを書く必要があるが、anyhowならまとめて処理できる
pub fn sample2(i: i32) -> anyhow::Result<i32> {
    if i == 0 {
        Err(io::Error::from(io::ErrorKind::Other))?
    } else if i < 5 {
        Err(MyError::Small(i))?
    } else if i > 10 {
        Err(MyError::Big(i))?
    } else {
        Ok(i)
    }
}

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?