---
title: "優雅的 RSpec 測試 - 前置處理"
date: 2023-02-10T00:00:00+08:00
publishDate: 2023-02-10T00:00:00Z
lastmod: 2023-09-03T14:30:07+08:00
tags: ["RSpec","教學","測試","結構","Hook"]
series: "elegant-rspec"
toc: true
permalink: "https://blog.aotoki.me/posts/2023/02/10/elegant-rspec-prepare-test/"
language: "zh-tw"
---


在上一個段落中，我們有提到需要限制 Let 的使用，然而當我們有比較複雜的測試前置條件時，就很難避免撰寫過於複雜的測試主題，此時就可以利用「前置處理」的機制來解決這個問題。

<!--more-->

## Before

Before 通常會在一個測試案例前執行，因此我們可以用來設定一些測試資料的建立。舉例來說，我們希望在一個訂單中預先加入商品，然後用來驗證這筆訂單是否符合預期，就可以使用 Before 來進行處理。

```ruby
RSpec.describe Order do
  subject(:order) { Order.new }

  before do
    order.add_item(OrderItem.new(amount: 100))
    order.add_item(OrderItem.new(amount: 100))
  end

  it { is_expected.to have_attributes(subtotal: 200) }
  it { is_expected.to have_attributes(items_count: 2) }
end
```

以上面的例子，我們會在開始執行 `it` 所給予的測試案例前，先呼叫 `Order#add_item` 來加入兩個金額為 `100` 的品項，當我們正式執行 `it` 來驗證時，就不會是一個什麼都沒有的 Order 物件，而是已經跟兩個 OrderItem 有所關聯的物件，因此就能夠用來驗證 `#subtotal` 和 `#items_count` 兩個方法是否正確。

## After

跟 Before 不同的地方是 After 是測試案例執行結束後才被執行，通常我們會用來清理 Before 所產生的一些暫時性資料，或者將一些開關切換回去。

```ruby
RSpec.describe Worker do
  subject(:worker) { Worker.new { true } }

  describe "#execute" do
    subject { worker.execute }

    before { Worker.mode = :inline }
    after { Worker.mode = :async }

    it { is_expected.to be_truthy }
  end
end
```

## Around

因為 After 很多時候都是跟著 Before 一起搭配使用，因此我們可以改用 Around 來處理，這樣在一些暫存檔案之類的處理，我們就不需要另外放一個 Let 來紀錄暫存檔案。

```ruby
RSpec.describe Parser do
  subject(:parser) { Parser.new }

  describe '#parse' do
    subject { parser.parse }

    around do |example|
      temp_data = Tempfile.new
      temp_data.write('{ "data": "example" }')
      temp_data.rewind
      parser.add(temp_data)

      example.run

      temp_data.unlink
    end

    it { is_expected.to include('data' => 'example')}
  end
end
```

使用 Around 的時候我們可以拿到 `example` 作為測試案例對應的物件，只需要在我們處理完前置條件後呼叫 `example.run` 就可以執行測試，並且在後續進行對測試前置處理的清理動作，可以根據情境判斷使用 Around 還是個別呼叫 Before / After 來對測試進行預先準備。

## Hook Type

在預設的狀況下我們的 Hook（鉤）是針對單一測試案例的，然而有時候我們希望一次性的針對整個測試、測試群組來設定，就可以利用指定 `suite`、`all` 的選項來設定觸發的時機點。

```ruby
RSpec.describe Worker do
  around(:suite) do |example|
    Worker.inline! { example.run }
  end

  context 'when worker not in parallel' do
    before(:all) { Worker.max_job = 1 }

    # ...
  end
end
```

以上面的例子，我們希望整個測試都使用 `Worker.inline!` 模式為前提處理，同時在特定的情況下，限制不要平行處理，這樣我們就不用在每一個測試案例都重複的呼叫這些設定變更，在測試案例非常多的狀況下，可以有效地改善執行的速度。

---

如果想在第一時間收到更新，歡迎[訂閱弦而時習之](https://mailchi.mp/aotoki/graceful-rspec)在這系列文章更新時收到通知，如果有希望了解的知識，可以利用[優雅的 RSpec 測試回饋表單](https://us4.list-manage.com/survey?u=dd3d68032c0510041f1302539&id=5ddf86cae1&attribution=false)告訴我。

