iT邦幫忙

2023 iThome 鐵人賽

DAY 6
0
Software Development

當rust 遇上 cqrs & es系列 第 6

D6 測試event apply & rollback

  • 分享至 

  • xImage
  •  

ES裡 aggregate 的狀態由 events的紀錄所決定,所以要能夠從頭建立event,因此自帶audit log功能,若要進從不同時序的追蹤,使用snapshot可以減少逐項apply event的成本,而有時要還原到某時點在snapshot之前,使用rollback提供反向回滾能比較有效率處理,甚或因為事件的不可修改性,若真的遇到要修改歷歷誤植資料,也能成為處理手法的工具之一。

試跑昨日測試,先準備共用commands

// 圖書建檔
fn create_book_command() -> BookCommand {
    BookCommand::CreateBook {
        id: "test-book-id".to_string(),
        title: "test-book-title".to_string(),
        isbn10: "1234567890".to_string(),
        description: "test-book-description".to_string(),
    }
}

// 圖書入庫
fn ingest_book_command() -> BookCommand {
    BookCommand::IngestBook {
        id: "test-book-id".to_string(),
        copies: 1,
    }
}

// 圖書借閱
fn lending_book_command() -> BookCommand {
    BookCommand::LendBook(
        LendingRecord {
            reader_id: "test-reader-id".to_string(),
            lent_date: Utc::now(),
            due_date: Utc::now().add(chrono::Duration::days(7)),
        })
}

// 圖書歸還
fn return_book_command() -> BookCommand {
    BookCommand::ReturnBook (
        LentRecord {
            reader_id: "test-reader-id".to_string(),
            lent_date: Utc::now(),
            due_date: Utc::now().add(chrono::Duration::days(7)),
            returned_date: Some(Utc::now()),
        }
    )
}

書籍資料建檔

#[tokio::test]
async fn test_create_book() {
    let command = create_book_command();
    let mut aggregate = Book::default();
    let events = aggregate.handle(command).await.unwrap();

    for event in events {
        aggregate.apply(event);
    }

    assert_eq!(aggregate.id, "test-book-id".to_string());
    assert_eq!(aggregate.title, "test-book-title".to_string());
    assert_eq!(aggregate.isbn10, "1234567890".to_string());
    assert_eq!(aggregate.description, "test-book-description".to_string());
}

書籍入庫

#[tokio::test]
async fn test_ingest_book() {
    let command = create_book_command();
    let mut aggregate = Book::default();
    let events = aggregate.handle(command).await.unwrap();
    let event = &events[0];
    aggregate.apply(event.clone());

    let command = ingest_book_command();
    let events = aggregate.handle(command).await.unwrap();

    let event = &events[0];
    aggregate.apply(event.clone());
    assert_eq!(aggregate.copies, 1);    // 庫存為1

    aggregate.rollback(event.clone());  // 回滾事件
    assert_eq!(aggregate.copies, 0);    // 庫存為0
}

書籍借閱,happy path

#[tokio::test]
async fn test_lending_book() {
    let command = create_book_command();
    let mut aggregate = Book::default();
    let events = aggregate.handle(command).await.unwrap();
    let event = &events[0];
    aggregate.apply(event.clone());

    let command = ingest_book_command();
    let events = aggregate.handle(command).await.unwrap();
    let event = &events[0];
    aggregate.apply(event.clone());

    let command = lending_book_command();
    let events = aggregate.handle(command).await.unwrap();
    let event = &events[0];
    aggregate.apply(event.clone());
    assert_eq!(aggregate.lending_records.len(), 1);     // 借閱紀錄 1 筆
    assert_eq!(aggregate.lending_records[0].reader_id, "test-reader-id".to_string());
    assert_eq!(aggregate.available_copies(), 0);        // 庫存數量 0 本

    aggregate.rollback(event.clone());              // 資料回滾
    assert_eq!(aggregate.lending_records.len(), 0); // 借閱紀錄 0 筆
    assert_eq!(aggregate.available_copies(), 1);    // 庫存數量 1 本
}

書籍借閱,驗證檢核邏輯

#[tokio::test]
async fn test_lending_book_validations() {
    // 準備資料入庫2本書,已借出一本
    let mut aggregate = Book::default();
    aggregate.apply(BookEvent::BookIngested {
        id: "test-book-id".to_string(),
        copies: 2,
    });
    let date = Utc::now();
    aggregate.apply(BookEvent::BookLent(
        LendingRecord {
            reader_id: "test-reader-id".to_string(),
            lent_date: date.clone(),
            due_date: Utc::now().add(chrono::Duration::days(7)),
        }));

    // 同一讀書再行借閱
    let err = aggregate.handle(
        BookCommand::LendBook(LendingRecord {
            reader_id: "test-reader-id".to_string(),
            lent_date: date.clone(),
            due_date: Utc::now().add(chrono::Duration::days(7)),
        })).await.unwrap_err();
    assert_eq!(err, book_lib::domain::book::BookError("讀者已借出同一本書".to_string()));

    // 安排把另一本書也借走,剩餘庫存為0
    aggregate.apply(BookEvent::BookLent(
        LendingRecord {
            reader_id: "test-reader-id-2".to_string(),
            lent_date: date.clone(),
            due_date: Utc::now().add(chrono::Duration::days(7)),
        }));
    assert_eq!(aggregate.available_copies(), 0);

    // 安排另一讀書借閱
    let err = aggregate.handle(
        BookCommand::LendBook(LendingRecord {
            reader_id: "test-reader-id".to_string(),
            lent_date: date.clone(),
            due_date: Utc::now().add(chrono::Duration::days(7)),
        })).await.unwrap_err();
    assert_eq!(err, book_lib::domain::book::BookError("書籍已無庫存".to_string()));
}

還書

#[tokio::test]
async fn test_return_book() {
    // 準備已借出書籍
    let mut aggregate = Book::default();
    aggregate.apply(BookEvent::BookIngested {
        id: "test-book-id".to_string(),
        copies: 1,
    });
    aggregate.apply(BookEvent::BookLent(
        LendingRecord {
            reader_id: "test-reader-id".to_string(),
            lent_date: Utc::now(),
            due_date: Utc::now().add(chrono::Duration::days(7)),
        }));
    assert_eq!(aggregate.available_copies(), 0);

    // 執行還書操作
    let command = return_book_command();
    let events = aggregate.handle(command).await.unwrap();
    let event = &events[0];
    aggregate.apply(event.clone());
    assert_eq!(aggregate.lending_records.len(), 0);
    assert_eq!(aggregate.lent_history.len(), 1);
    assert_eq!(aggregate.available_copies(), 1);

    // 回滾還書事件
    aggregate.rollback(event.clone());
    assert_eq!(aggregate.lending_records.len(), 1);
    assert_eq!(aggregate.lent_history.len(), 0);
    assert_eq!(aggregate.available_copies(), 0);
}

上一篇
D5 實現基本資料結構(2)
下一篇
D7 ~~測試~~ 用 es 存放 book 事件
系列文
當rust 遇上 cqrs & es30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言