---
title: "驗收測試 - 重新思考 Rails 架構"
date: 2024-10-11T00:00:00+08:00
publishDate: 2024-10-11T00:00:00+08:00
lastmod: 2024-06-02T17:03:47+08:00
tags: ["Rails","Domain-Driven Design","設計","Clean Architecture"]
series: "rethink-rails-architecture"
toc: true
permalink: "https://blog.aotoki.me/posts/2024/10/11/rethink-rails-architecture-acceptance-test/"
language: "zh-tw"
---


在實際開始實作之前，透過測試確認行為以及開發過程中進行驗證都會是個不錯的方式。我們會透過 [Cucumber 的文件測試法](https://blog.aotoki.me/series/test-with-cucumber/)中的方式，來描述一個運送狀態更新的設計。

<!--more-->

## 描述功能{#describe-feature}

假設我們要實現一個跨時區的物流運送流程，那麼可以撰寫如下的測試。

```gherkin
Feature: Shipment
  Scenario: I can update the simpment route
    Given there have some shipment
      | id |  state    |
      | 1  |  shipping |
    And there have some shipment route
      | route_id | shipment_id | delivered_at         |
      | OKA      | 1           | 2024-03-19T00:00:00Z |
    When I make a POST request to "/shipments/1/routes"
      """
      {
        "route_id": "TPE",
        "date": "0319",
        "time": "1000"
      }
      """
    Then the response should be
      """
      {
        "id": 1,
        "state": "shipping",
        "routes": [
          {
            "route_id": "OKA",
            "shipment_id": 1,
            "delivered_at": "2024-03-19T00:00:00.000Z"
          },
          {
            "route_id": "TPE",
            "shipment_id": 1,
            "delivered_at": "2024-03-19T02:00:00.000Z"
          }
        ]
      }
      """
```

上面的例子我們透過描述 API 的行為，來呈現一些資訊。

* 運送狀態是對應某筆訂單
* 運送過程會有多個路徑（Route）
* 填入時間時，會以 `date` 和 `time` 的格式搭配，並且以當地時間為基準

除此之外，在系統的設計中我們預期 Order 和 Shipment 是一對一的關係，因此在這裡不會有 `order_id` 的欄位，從實體（Entity）的角度來看辨識 Order 和辨識 Shipment 是相同的。

然而，在 Route 的觀點來看，一個實體會是 Shipment ID + Route ID 的組合，上述的情境中我們仍可以使用資料庫的 Auto Increment 機制來管理，然而在實際使用時不一定會直接參考資料庫的流水號。

> 針對回傳內容的檢查，因為完整比對 JSON 有時候不一定是個好的處理方式，可以考慮利用 JMESPath 這類套件，使用 `routes[0].route_id` 來找到 `OKA` 進行比對，會相對有彈性
## 步驟定義{#step-definition}

有了測試的步驟後，就可以來撰寫步驟定義。

```ruby
Given('there have some shipment') do |table|
  table.hashes.each do |row|
    Shipment.create!(**row)
  end
end

Given('there have some shipment route') do |table|
  table.hashes.each do |row|
    ShipmentRoute.create!(**row)
  end
end

When('I make a POST request to {string}') do |path, raw_body|
  body = JSON.parse(raw_body)
  @response = post path, body
end

Then('the response should be') do |raw_body|
  expected = JSON.parse(raw_body)
  actual = JSON.parse(@response.body)

  expect(actual).to eq(expected)
end
```

我們直接透過 ActiveRecord 定義的 Model 來生成測試用的資料，並且直接對回傳的內容轉換成 JSON 進行比對，這樣就可以有初步的雛形。

> 實際開發時會盡可能推遲 Model 層級的實作，因為這系列的重點不在測試的實作，因此會跳過比較多細節。

接下來就可以切入到 Controller 的部分來進行實作，至於測試案例可以根據情況在這個階段多描述並以 `@wip` 標記註記會在之後處理，或者在後續開發時有發現新的案例時補上。

