【Rails】テスト時にRuntimeError: UNIQUE constraint failedが発生する

# 結論

`test/fixture/テーブル名.yml`の**データが被っている**ことが原因でした。

`User`モデルの`email`カラムに**Unique制約を後から付与**した結果、テストデータの`email`カラムの**値が被っていた**ため制約に違反しているというエラーが出ていたみたいです。

```diff_yaml:test/fixture/users.yaml
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

one:
  name: MyString
  email: MyString

two:
  name: MyString
+ email: MyString2 # 被ってなければエラーは出ない
- email: MyString
```


## 参考

https://stackoverflow.com/questions/31095429/rails-tutorial-sqlite3constraintexception-unique-constraint-failed-users-em

↑この回答がなければ多分詰んでました。


# やったこと

何をやったら上のエラーが発生するのか、また私が発生した時の状況を書いておきます。

:::note warn
書いてる人が初心者のため、全ての情報を鵜呑みにするのはやめたほうがいいです。
貼ってあるリンクのページを見るか、自分で調べることを推奨します。
:::


## 環境

* MacOS Sonoma 14.1
* 開発環境: Docker 25.0.3
    * Dockerイメージ: [Ruby](mcr.microsoft.com/devcontainers/ruby:1-3.3-bullseye)
    * コンテナの起動方法: Dev containersを使用
* Rubyのバージョン: 3.3.0
* Railsのバージョン: 7.1.3.2
* データベース: SQLite（Dockerイメージについているものを使用）


## Userモデルを作成

`User`モデルは、Ruby on Railsの勉強のために作成しているアプリケーションで、とりあえず生成することにしたモデルです。
今のところはユーザー登録に使う予定です。

```bash:生成コマンド
# Userモデルを生成
rails g model User name:string email:string
```

カラムの詳細はこんな感じです。

| カラム名 | 型 | 備考 | 例 |
|:--|:--| :-- | :-- |
| `name` | string |ユーザー名、被ってもいい | `nanasi`
| `email` | string |メールアドレス、ユニークにしたい | `nanasi@example.com`

すると、以下のファイルが生成されます。

* `app/models/user.rb`
* `db/migrate/現在の日時_create_users.rb`
* `test/fixture/users.yml`
* `test/models`

### users.yml

この中で今回関係あるのは`test/fixture/users.yml`のみです。
このファイルは、簡単に言えばテストデータを書くことができるファイルです。

こんな感じのが生成されると思います。

```yaml:test/fixture/users.yml
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

one:
  name: MyString
  email: MyString

two:
  name: MyString
  email: MyString
```

細かくは読んでいないのですが、この記事がわかりやすかったので、詳しい説明はこちらを見てください。

https://qiita.com/itkrt2y/items/ca34fea17fc7dde56b7a


### マイグレート

マイグレートというのは、マイグレーションファイルの内容をデータベースとモデルに適用させるみたいなやつです。

以下の記事にマイグレーションファイルを使ってカラムを追加/削除する方法が書かれています。
詳しく知りたい方はこちらを見てください。

https://qiita.com/azusanakano/items/a2847e4e582b9a627e3a

一度ここでマイグレートしておきます。

```bash:マイグレートする
rails db:migrate
```

この際`db/schema.rb`が生成されると思います。


## インデックスを作成

インデックスとは、簡単に言えばデータの検索が早くなるやつです。
また、ユニーク制約を設けるときにも作ります。

下のページがわかりやすかったので、詳しく知りたい方はこちらを見てください。

https://plog.kobacchi.com/rails-add-index-to-column/

インデックスの作成方法は、簡単に言えばこうです。

1. マイグレーションファイルを作成
1. マイグレーションファイルを、インデックスを追加する内容になるよう編集
1. データベースをマイグレートする


### マイグレーションファイルを作成

以下のコマンドを実行します。

```bash:マイグレーションファイルを作成
rails g migration User
```

すると、`db/migrate`に以下のファイルが作られます。

```ruby:db/migrate/現在の日時_users.rb
class Users < ActiveRecord::Migration[7.1]
  def change
  end
end
```

これがマイグレーションファイルです。
このファイルを変更し、マイグレートすることで、モデルとDBに変更を加えることができます。


### マイグレーションファイルを編集

インデックスの追加には、`add_index`メソッドを使います。
引数にカラム名のシンボルを入れると、そのカラムにインデックスが追加されます。

ファイルを以下のように編集します。

```diff_ruby:db/migrate/日時_users.rb
class Users < ActiveRecord::Migration[7.1]
  def change
+   add_index :users, :email, unique: true
  end
end
```

`unique: true`という箇所は、`email`カラムにユニーク制約を設ける設定です。
ちなみに`:users`という箇所はよくわかってません。


### データベースをマイグレート

先ほどと同じく、以下のコマンドでできます。

```bash:マイグレートのコマンド
rails db:migrate
```


## テストを実行、エラーが発生

:::note info
もともとこのアプリケーションには、`User`モデルとは全く関係のないコントローラーがあり、そちらの動作を確認するためのテストが書かれていました。
:::

動作確認のため、以下のコマンドでテストを実行しました。

```sh:テストを実行
rails t
```

すると、以下のエラーが発生しました。

```terminal:エラー内容
ERROR StaticPagesControllerTest#テスト名1 (0.15s)
Minitest::UnexpectedError:         ActiveRecord::RecordNotUnique: RuntimeError: UNIQUE constraint failed: users.email
# スタックは略

ERROR StaticPagesControllerTest#テスト名2 (0.15s)
Minitest::UnexpectedError:         ActiveRecord::RecordNotUnique: RuntimeError: UNIQUE constraint failed: users.email
# スタックは略

# これがテストの個数分（7個）続く
```


### エラー内容

エラーメッセージには`UNIQUE constraint failed: users.email`と書かれています。
これを日本語にすると「`users.email`がユニーク制約に違反している」です（多分）。

確かに`email`カラムにはユニーク制約を追加しましたが、一体どういうことでしょうか？


### データベースの内容を見る

とりあえず、データベースの内容を見てみます。
`dbconsole`を使ってデータベースに`SELECT`クエリを叩いてみました。

```terminal:ターミナル
rails dbconosle

sqlite>
SELECT * FROM users;
1|test|test@sample.com|日付|日付
.quit

>dockerのプロンプト
```

見ての通り、そもそもデータが１つしかないので、ユニーク制約違反など起こしようがありません。


### サーバーを建ててアクセスしてみる

テストではない場所ならどうなのかということで、開発環境で試してみました。

私の場合、あらかじめユーザー一覧をJSON形式で取得するAPIを用意しているので、特に準備は不要でした。

`rails s`コマンドでサーバーを立ち上げます。
そして、`/users`（前述のAPI）にアクセスします。

ステータスコードは`200`で、以下のようなレスポンスが返ってきました。

```json:レスポンス
[
    {
        "id": 1,
        "name": "admin",
        "email": "test@sample.com",
        "created_at": "日付",
        "updated_at": "日付"
    }
]
```

しっかり取得できているみたいです。


### データをDBに追加してみる

ここで、本当にユニーク制約が動作しているのか見るため、データをDBに追加してみました。

```console:ターミナル
irb

>irb
user = User.new
user.name = '被ってないよ'
user.email = 'test@sample.com' # 被ってる
user.save

# なんか失敗してる
```

`email`が被っているデータは追加できないみたいです。
ここは意図した通りに動作しています。


### エラー文をコピペして調べる

ここでやっとエラー文をコピペして調べることにしました。
我ながら遅いですね。

そして、先ほども書いた以下のStackOverflowの質問を見つけました。

https://stackoverflow.com/questions/31095429/rails-tutorial-sqlite3constraintexception-unique-constraint-failed-users-em

どうやら`test/fixture/users.yml`を編集するといいみたいです。
このファイルはなんなのか？という疑問を持ったため調べたら、[先ほども書いたこの記事](https://qiita.com/itkrt2y/items/ca34fea17fc7dde56b7a)に出会いました。


## users.ymlを修正する

もとの`users.yml`は、以下のように`email`カラムの値がどちらも`MyString`になっていました。

```yaml:もとのusers.yml
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

one:
  name: MyString
  email: MyString

two:
  name: MyString
  email: MyString
```

おそらく、テストを実行しようとした際に上のテストデータをDBに登録しようとし、その結果ユニーク制約に違反してしまってエラーが出た、ということだと思います。
<sub>これは詰まるって...</sub>

ということで、どうせならテストデータ風に`users.yml`を編集してみました。
最終的に出来上がったのがこちらです。

```yaml/test/fixture/users.yml
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

# ここの値を被せるとキー制約に違反する

normal:
  name: nanasi
  email: nanasi@sample.com

error:
  name: ''
  email: 'not_email'
```


## テストを実行する

ということで、テストを実行します。

```bash:テストを実行
rails t

Running 7 tests in a single process (parallelization threshold is 50)
Started with run options --seed 42455

  7/7: [==================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.39507s
7 tests, 11 assertions, 0 failures, 0 errors, 0 skips
```

ハイライトでは赤くなってますが（実際には緑）、無事にテストが成功しています。
<sub>よかったー...</sub>