---
title: "優雅的 RSpec 測試 - RSpec 概觀"
date: 2023-01-20T00:00:00+08:00
publishDate: 2023-01-20T00:00:00Z
lastmod: 2023-09-03T14:30:07+08:00
tags: ["RSpec","教學","測試","DSL"]
series: "elegant-rspec"
toc: true
permalink: "https://blog.aotoki.me/posts/2023/01/20/elegant-rspec-overview/"
language: "zh-tw"
---


幾乎所有的 Ruby 開發者都聽過 RSpec 然而 Rails 和 Ruby 都是使用名為 Minitest 的測試框架進行測試的，那麼 RSpec 做了什麼事情，讓更多開發者比起使用 Minitest 更喜歡使用 RSpec 呢？

<!--more-->

## DSL

DSL（Domain Specific Language，領域特定語言）是 Ruby 非常重要的語言特性，因為 Ruby 的類別（Class）本身也是物件，因此我們可以針對類別定義方法，同時允許省略括號等語法的特性，讓 DSL 在 Ruby 變得非常容易實現。

RSpec 就善用了這一點，讓我們能夠以 Ruby 撰寫出極度接近「人類語言」的測試，我們可以看一下這段範例。

```ruby
RSpec.describe "User" do
  subject { User.new(admin: false) }

  it { is_expected.not_to be_admin }

  context 'when user is admin' do
    subject { User.new(admin: true) }

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

閱讀起來基本上不太困難，可以看出來是描述「使用者」同時，一開始的對象是「非管理員使用者」並且，測試的案例為「預期不作為管理員」除此之外還有一個「當使用者是管理員」的情境，用「預期作為使用者」的測試案例。

假設我們要使用 Minitest 來撰寫這個測試，則會變成像這樣

```ruby
require "minitest/autorun"

class TestUser < Minitest::Test
  def test_user_not_be_admin
    @user = User.new(admin: false)
    assert_equal false, @user.admin?
  end

  def test_user_be_a_admin
    @user = User.new(admin: true)
    assert_equal false, @user.admin?
  end
end
```

看起來似乎就沒那麼直覺，雖然有一部分原因是是因為對 Minitest 不太熟悉，然而這樣的撰寫方式仍讓人不是那麼容易學習。

> 現在 Minitest 有支援 [Specs](https://github.com/minitest/minitest#label-Specs) 的寫法，更接近 RSpec 的的寫法。

## Behavior-Driven Development

除了 DSL 的特性外，RSpec 本身也是一個「行為驅動開發」的測試框架。我們在開發軟體的過程中，會很習慣的以「物件」或者「方法」的角度去思考，然而在 RSpec 中我們更關注的是對「行為」進行測試。

類似於上一個章節提到的 A-TDD（驗收測試區動開發）的方式，我們希望在實作之前先撰寫測試，如果 A-TDD 是針對「功能」來撰寫，那麼 BDD 則是針對「行為」來撰寫。

功能和行為的差異在哪裡呢？簡單來說「功能」是使用者一系列操作的組合，而行為則是某個程式邏輯，也就是說非常適合用來撰寫「單元測試」類型的測試案例。

> 我在 [LeSS in Action](https://vocus.cc/less-in-action-tw/home) 課程中學到的一個有趣觀念，單元測試跟端對端測試（E2E Testing）的差異，主要是「範圍」的大小，而 BDD 大多剛好是一個「單元」大小的測試。

舉例來說，我們想要實作一個「訂閱」的動作，就可以像這樣撰寫一個測試

```ruby
RSpec.describe User do
  # ...

  describe '#subscribe!' do
    subject(:subscribe) { user.subscribe! }

    it { is_expected.to be_truthy }

    context 'when use is subscribed' do
      before { subscribe }

      it { expect { subscribe }.to raise_error(User::AlreadySubscribed) }
    end
  end
end
```

我們可以描述一個 `#subscribe!` 方法定為這個測試的主題，如果一切正常的話則回傳 `true` 除此之外，有一個「已經訂閱」的情境，我們先在測試前執行一次「訂閱」當我們再次驗證「訂閱」動作時，就會預期拋出 `User::AlreadySubscribed` 的例外，撰寫測試就變得相當輕鬆。

我認為這樣直覺的撰寫方式，以及完善的生態系，是讓 RSpec 深受大部分 Ruby 開發者喜愛的原因。

---

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

