この記事は、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::State
や tauri::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));
}
}
dotenvyとenvy
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,
}
}
}
anyhowとthiserror
ただでさえシンプルな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)
}
}