###### tags: `YouTube`
# RSpec 入門
## 環境構築
本講座では、Docker を使って開発を行います。
しかし、ゼロから `Dockerfile` や `docker-compose.yml` を作成するといったことは行いません。**Rails 7.0.4** と **Ruby 3.1.3** に対応した Docker 化されたアプリを使います。
今回、Nick Janetakis 氏が公開している下記リポジトリを採用させて頂きました。
https://github.com/nickjj/docker-rails-example
環境構築の詳細・立ち上げるコンテナの役割については、下記の書籍で解説しています。
https://zenn.dev/farstep/books/7f169cdc597ada
### リポジトリのクローン
```bash
$ git clone git@github.com:nickjj/docker-rails-example.git rspec_tutorial
$ cd rspec_tutorial
```
```bash
$ cp .env.example .env
```
### 環境変数の編集(`.env`)
```diff env
- export COMPOSE_PROJECT_NAME=hellorails
+ export COMPOSE_PROJECT_NAME=rspec_tutorial
```
```diff env
- #export POSTGRES_DB=hello
+ export POSTGRES_DB=rspec_tutorial
```
### アプリケーションのタイトルの編集(`app/views/layouts/application.html.erb`)
```diff html
- <title>Docker + Rails</title>
+ <title>Rspec Tutorial</title>
```
### インストールする gem の追加(`Gemfile`)
```diff ruby
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby "3.1.3"
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 7.0.4"
# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
gem "sprockets-rails"
# Use postgresql as the database for Active Record
gem "pg", "~> 1.1"
# Use the Puma web server [https://github.com/puma/puma]
gem "puma", "~> 6.0"
# Bundle and transpile JavaScript [https://github.com/rails/jsbundling-rails]
gem "jsbundling-rails"
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails"
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails"
# Bundle and process CSS [https://github.com/rails/cssbundling-rails]
gem "cssbundling-rails"
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"
# Use Redis adapter to run Action Cable in production
gem "redis", "~> 5.0"
# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
# gem "kredis"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ]
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
# gem "image_processing", "~> 1.2"
# Execute jobs in the background [https://github.com/mperham/sidekiq]
gem "sidekiq", "~> 7.0"
group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri mingw x64_mingw ]
# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false
end
group :development do
# Use console on exceptions pages [https://github.com/rails/web-console]
gem "web-console"
# Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
gem "rack-mini-profiler"
# Speed up commands on slow machines / big apps [https://github.com/rails/spring]
# gem "spring"
end
group :test do
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
gem "capybara"
gem "selenium-webdriver"
gem "webdrivers"
+ gem "rspec-rails"
+ gem "factory_bot_rails"
+ gem "faker"
end
```
| gem | 用途 |
| -------- | -------- |
| rspec-rails | テストフレームワーク |
| factory_bot_rails | テスト用オブジェクトの生成 |
| faker | ダミーデータの作成 |
### データベースの設定(`docker-compose.yml`・`config/database.yml`)
```diff yml
services:
postgres:
deploy:
resources:
limits:
cpus: "${DOCKER_POSTGRES_CPUS:-0}"
memory: "${DOCKER_POSTGRES_MEMORY:-0}"
environment:
POSTGRES_USER: "${POSTGRES_USER}"
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
+ POSTGRES_DB: "${POSTGRES_DB}"
image: "postgres:15.1-bullseye"
profiles: ["postgres"]
restart: "${DOCKER_RESTART_POLICY:-unless-stopped}"
stop_grace_period: "3s"
volumes:
- "postgres:/var/lib/postgresql/data"
```
```diff yml
default: &default
adapter: "postgresql"
encoding: "unicode"
+ database: "<%= ENV.fetch("POSTGRES_DB") { "rspec_tutorial" } %>"
username: "<%= ENV.fetch("POSTGRES_USER") { "hello" } %>"
password: "<%= ENV.fetch("POSTGRES_PASSWORD") { "password" } %>"
host: "<%= ENV.fetch("POSTGRES_HOST") { "postgres" } %>"
port: "<%= ENV.fetch("POSTGRES_PORT") { 5432 } %>"
# http://guides.rubyonrails.org/configuring.html#database-pooling
pool: "<%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>"
development:
<<: *default
+ database: <%= ENV.fetch("POSTGRES_DB") { "rspec_tutorial" } %>_development
test:
<<: *default
+ database: <%= ENV.fetch("POSTGRES_DB") { "rspec_tutorial" } %>_test
production:
<<: *default
+ database: <%= ENV.fetch("POSTGRES_DB") { "rspec_tutorial" } %>_production
```
### 自動生成ファイルの設定(`config/application.rb`)
```diff ruby
require_relative "boot"
require "rails/all"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module Hello
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.0
# Log to STDOUT because Docker expects all processes to log here. You could
# then collect logs using journald, syslog or forward them somewhere else.
logger = ActiveSupport::Logger.new(STDOUT)
logger.formatter = config.log_formatter
config.logger = ActiveSupport::TaggedLogging.new(logger)
# Set Redis as the back-end for the cache.
config.cache_store = :redis_cache_store, {
url: ENV.fetch("REDIS_URL") { "redis://redis:6379/1" },
namespace: "cache"
}
# Set Sidekiq as the back-end for Active Job.
config.active_job.queue_adapter = :sidekiq
# Mount Action Cable outside the main process or domain.
config.action_cable.mount_path = nil
config.action_cable.url = ENV.fetch("ACTION_CABLE_FRONTEND_URL") { "ws://localhost:28080" }
# Only allow connections to Action Cable from these domains.
origins = ENV.fetch("ACTION_CABLE_ALLOWED_REQUEST_ORIGINS") { "http:\/\/localhost*" }.split(",")
origins.map! { |url| /#{url}/ }
config.action_cable.allowed_request_origins = origins
+ # Customizing Rails Generators
+ config.generators do |g|
+ g.assets false # CSSやJavaScriptファイルを生成しない
+ g.helper false # ヘルパーを生成しない
+ g.jbuilder false # .json.jbuilderファイルを生成しない
+ g.test_framework :rspec, # テストフレームワークとしてRSpecを指定
+ fixtures: false, # テストデータを作るfixtureを作成しない
+ request_specs: false, # リクエストスペックを作成しない
+ view_specs: false, # ビュー用のスペックを作成しない
+ helper_specs: false, # ヘルパー用のスペックを作成しない
+ routing_specs: false # ルーティングのスペックを作成しない
+ end
end
end
```
| 記述 | 理由 |
| -------- | -------- |
| `fixtures: false` | FactoryBot を使用するため |
| `request_specs: false` | 結合テスト(System Spec)でカバーできるため |
| `view_specs: false` | 結合テスト(System Spec)でカバーできるため |
| `helper_specs: false` | 今回ヘルパーを作成しないため |
| `routing_specs: false` | 結合テスト(System Spec)でカバーできるため |
### Docker イメージの構築(ビルド)
```bash
$ docker-compose build
```
### データベースの作成・マイグレーションの実行
```bash
$ docker-compose run --rm web bash
```
```bash
$ rails db:create
$ rails db:migrate
```
## アプリケーションの作成
### モデル・テーブル・コントローラ・ビューの作成
```bash
$ rails g scaffold tweet content
```
```ruby
class CreateTweets < ActiveRecord::Migration[7.0]
def change
create_table :tweets do |t|
t.string :content, null:false
t.timestamps
end
end
end
```
### エラーメッセージ
config/locales/en.yml
```yaml
en:
errors:
messages:
not_saved:
one: "1 error prohibited this %{resource} from being saved:"
other: "%{count} errors prohibited this %{resource} from being saved:"
```
### Tailwind CSS による UI の作成
app/views/layouts/application.html.erb
```erb
<% if flash[:notice] %>
<div class="p-4 mb-4 text-md text-blue-700 text-center font-bold">
<%= notice %>
</div>
<% end %>
<% if flash[:alert] %>
<div class="p-4 mb-4 text-md text-red-700 text-center font-bold">
<%= alert %>
</div>
<% end %>
<main class="container mx-auto sm:w-10/12 lg:w-9/12 mb-8 z-0">
<%= yield %>
</main>
```
app/views/tweets/_form.html.erb
```erb
<div class='flex flex-wrap justify-center'>
<%= form_with model: tweet, class: "xl:w-8/12 md:w-10/12" do |f| %>
<% if tweet.errors.any? %>
<div class="bg-orange-100 border-l-4 border-orange-500 text-orange-700 p-4 mb-5" role="alert">
<p class="font-bold">
<%= I18n.t("errors.messages.not_saved",
count: tweet.errors.count,
resource: tweet.class.model_name.human.downcase)
%>
</p>
<ul>
<% tweet.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="mb-6">
<%= f.label :content, class: "mb-2 block text-sm text-gray-600" %>
<%= f.text_area :content, rows: "3", class: "w-full rounded-md border border-gray-300 py-2 px-3 placeholder:text-gray-300 focus:border-indigo-300 focus:outline-none focus:ring focus:ring-indigo-100" %>
</div>
<%= f.submit class: "inline-flex w-full items-center justify-center rounded-md bg-indigo-500 p-3 text-white duration-100 ease-in-out hover:bg-indigo-600 focus:outline-none cursor-pointer" %>
<% end %>
</div>
```
app/views/tweets/_tweet.html.erb
```erb
<div id="<%= dom_id tweet %>" class="text-center mb-5">
<p class="text-xl">
<strong>Content:</strong>
<%= tweet.content %>
</p>
</div>
```
app/views/tweets/edit.html.erb
```erb
<div class="mb-8 text-center">
<span class="text-3xl font-bold">
Editing tweet
</span>
</div>
<%= render "form", tweet: @tweet %>
<br>
<div class="text-center">
<%= link_to "Show this tweet", @tweet %> |
<%= link_to "Back to tweets", tweets_path %>
</div>
```
app/views/tweets/index.html.erb
```erb
<div class="mb-8 text-center">
<span class="text-3xl font-bold">
Tweets
</span>
</div>
<div class="flex flex-wrap">
<div class="flex-none w-full px-6">
<%= link_to new_tweet_path, class: "mb-4 inline-flex items-center justify-center rounded-md bg-indigo-500 p-3 text-white duration-100 ease-in-out hover:bg-indigo-600 focus:outline-none" do %>
New tweet
<% end %>
<div class="relative flex flex-col min-w-0 mb-6 break-words bg-white border-0 border-transparent border-solid shadow-soft-xl rounded-2xl bg-clip-border">
<div class="flex-auto pb-2">
<div class="p-0 overflow-x-auto">
<table class="items-center w-full align-top border-gray-200 text-slate-500">
<thead class="align-bottom">
<tr>
<th class="p-3 font-bold text-left uppercase align-middle bg-transparent border-b border-gray-200 shadow-none text-lg border-b-solid tracking-none whitespace-nowrap text-slate-400 opacity-70">Content</th>
<th class="p-3 bg-transparent border-b border-gray-200 border-solid"></th>
</tr>
</thead>
<tbody class="align-bottom">
<% @tweets.each do |tweet| %>
<tr>
<td class="p-3 align-middle bg-transparent border-b whitespace-nowrap shadow-transparent">
<p class="font-semibold leading-tight text-md"><%= tweet.content %></p>
</td>
<td class="p-3 align-middle bg-transparent border-b whitespace-nowrap shadow-transparent">
<%= link_to tweet, class: "font-semibold leading-tight text-md text-slate-400" do %>
Show this tweet
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
```
app/views/tweets/new.html.erb
```erb
<div class="mb-8 text-center">
<span class="text-3xl font-bold">
New tweet
</span>
</div>
<%= render "form", tweet: @tweet %>
<br>
<div class="text-center">
<%= link_to "Back to tweets", tweets_path %>
</div>
```
app/views/tweets/show.html.erb
```erb
<div class="mb-8 text-center">
<span class="text-3xl font-bold">
Tweet
</span>
</div>
<%= render @tweet %>
<div class="text-center">
<%= link_to "Edit this tweet", edit_tweet_path(@tweet) %> |
<%= link_to "Back to tweets", tweets_path %>
<%= button_to "Destroy this tweet", @tweet, method: :delete, class: "font-bold text-red-500" %>
</div>
```
## RSpec によるテスト
### RSpec のセットアップ
```bash
$ rails g rspec:install
```
| 生成されるファイル | 役割 |
| -------- | -------- |
| `.rspec` | 基本設定ファイル |
| `spec/rails_helper.rb` | Rails 特有の設定を記述するファイル |
| `spec_helper.rb` | RSpec の全体的な設定を記述するファイル |
.rspec
```ruby:
--require spec_helper
--format documentation
```
表示の出力をキレイにする `--format documentation` というオプションを追加。
spec/rails_helper.rb
```ruby
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end
```
テスト用オブジェクトを生成するときに `create()` や `build()` が使えるようになる。(**シンタックスシュガー**)
```ruby
# 【example】
# before
tweet = FactoryBot.create(:tweet)
# after
tweet = create(:tweet)
```
環境変数 `RAILS_ENV` を `test` に変更する。
spec/rails_helper.rb
```diff ruby
- ENV['RAILS_ENV'] ||= 'test'
+ ENV['RAILS_ENV'] = 'test'
```
### Model Spec を書く
**Model Spec** とはバリデーション等の Rails のモデルのテストをする。
先ほど、Scaffold によって `spec/models/tweet_spec.rb` が生成されているはず。 このファイルに Model Spec を記述していく。
最初に、FactoryBot と Faker を使ってテストデータを生成する準備を行う。
```bash
$ mkdir spec/factories && touch spec/factories/tweets.rb
```
spec/factories/tweets.rb
```ruby
FactoryBot.define do
factory :tweet do
# 英数字のランダムな文字列を生成する(100文字)
content { Faker::Lorem.characters(number: 100) }
end
end
```
上記の記述により、`build(:tweet)` といったコードを書くことで、テストデータを生成できる。
今回、tweets テーブルの content カラムは
- 空でない
- 140 文字以内
という仕様があると想定する。すると、テストコードは下記のようになる。
spec/models/tweet_spec.rb
```ruby
require 'rails_helper'
RSpec.describe Tweet, type: :model do
let(:tweet) { build(:tweet) }
it "is valid with a valid content" do
expect(tweet).to be_valid
end
it "is not valid without a content" do
tweet.content = ''
expect(tweet).to_not be_valid
end
it "is not valid with a content longer than 140" do
tweet.content = Faker::Lorem.characters(number: 141)
expect(tweet).to_not be_valid
end
end
```
#### describe・context・it について
![](https://i.imgur.com/8X8EgZ7.png)
**describe** には、テストの対象が何かを記述する。
**context** には、特定の条件が何かを記述する。
(例. ログインしている場合としていない場合を分ける)
**it** には、アウトプットが何かを記述する。
(例. 期待する内容)
#### `build()` について
`build()` はオブジェクト化行う。データベースに登録されない。
`build(:tweet)` とすると、content カラムに 100 文字の英数字が格納された Tweet オブジェクトが生成される。
#### let について
- describe か context の中でのみ使用可能。
- 定義した定数が初めて出てきたときに評価される(**遅延評価**)。
`let(:tweet) { build(:tweet) }` `expect(tweet).to be_valid` と記述すると
1. `expect(tweet)` が実行され `tweet` が初めて出てくる。
2. `tweet` が build される。
3. `to` が実行される。
4. `be_valid` が実行される。
という流れになる。
#### be_valid について
`be_valid` とは、Rails の ActiveRecord が提供するメソッドで、モデルオブジェクトのバリデーションが成功したかどうか真偽値で返す。
```
$ bundle exec rspec
```
```ruby
Tweet
is valid with a valid content
is not valid without a content (FAILED - 1)
is not valid with a content longer than 140 (FAILED - 2)
Failures:
1) Tweet is not valid without a content
Failure/Error: expect(tweet).to_not be_valid
expected #<Tweet id: nil, content: "", created_at: nil, updated_at: nil> not to be valid
# ./spec/models/tweet_spec.rb:10:in `block (2 levels) in <top (required)>'
2) Tweet is not valid with a content longer than 140
Failure/Error: expect(tweet).to_not be_valid
expected #<Tweet id: nil, content: "w44f4jow0b97azkzcd35qbj3680deqscw3t3k5ver7mnm6xabn...", created_at: nil, updated_at: nil> not to be valid
# ./spec/models/tweet_spec.rb:14:in `block (2 levels) in <top (required)>'
Finished in 0.12862 seconds (files took 6.77 seconds to load)
3 examples, 2 failures
```
バリデーションを記述していないため、テストが失敗する。
app/models/tweet.rb
```ruby
class Tweet < ApplicationRecord
validates :content, presence: true
validates :content, length: { maximum: 140 }
end
```
バリデーションを記述した後、テストが通る。
```ruby
Tweet
is valid with a valid content
is not valid without a content
is not valid with a content longer than 140
Finished in 0.07432 seconds (files took 3.69 seconds to load)
3 examples, 0 failures
```
最初にテストを書き、そのテストが動作する必要最低限な実装をとりあえず行った後、コードを洗練させる、というスタイル(**テスト開発駆動**)
```ruby
class Tweet < ApplicationRecord
validates :content, presence: true, length: { maximum: 140 }
end
```
### System Spec を書く
**System Spec** とは **Capybara** を使ってブラウザの操作をシミュレートするテストのこと。
#### Capybara について
Capybara は結合テストや E2E テスト(システム全体を通したテスト)等に使用されるツールである。Capybara ではアプリケーションを実際に動かしているかのようなシュミレートによってテストを行ってくれる。
Capybara を動作させるときには「ドライバ」と言われる実行環境を選択できる。
今回は、高速に動作する **rack_test** を採用する。(JavaScript のテスト項目がある場合は Selenium を使う必要がある。)
`spec/spec_helper.rb` で、System Spec を実行する際に rack_test をドライバとして選択することを明記する。
```ruby
RSpec.configure do |config|
config.before(:each, type: :system) do
driven_by :rack_test
end
...略
end
```
#### PagesController の system spec
```bash
$ rails g rspec:system pages
```
spec/system/pages_spec.rb
```ruby
require 'rails_helper'
RSpec.describe "Pages", type: :system do
describe '#home' do
it 'renders the top page' do
visit root_path
expect(page.status_code).to eq(200)
end
end
end
```
#### TweetsController の system spec
```bash
$ rails g rspec:system tweets
```
spec/system/tweets_spec.rb
```ruby
require 'rails_helper'
RSpec.describe Tweet, type: :system do
let!(:tweet) { create(:tweet) }
describe 'List screen' do
before do
visit tweets_path
end
describe '#index' do
it 'displays content of the tweet' do
expect(page).to have_content tweet.content
end
it 'displays a link to the details screen' do
expect(page).to have_link 'Show this tweet', href: "/tweets/#{tweet.id}"
end
it 'displays a link to post a tweet' do
expect(page).to have_link 'New tweet', href: "/tweets/new"
end
end
end
describe 'Post screen' do
before do
visit new_tweet_path
end
describe '#new' do
it "displays a form for content" do
expect(page).to have_field 'tweet[content]'
end
it 'displays a Create Tweet Button' do
expect(page).to have_button 'Create Tweet'
end
it 'displays a link to back to list screen' do
expect(page).to have_link 'Back to tweets', href: "/tweets"
end
end
describe '#create' do
it "redirects to the details screen with a valid content" do
fill_in 'tweet[content]', with: Faker::Lorem.characters(number: 100)
click_button 'Create Tweet'
expect(current_path).to eq tweet_path(Tweet.last)
end
it "displays created tweet with a valid content" do
fill_in 'tweet[content]', with: Faker::Lorem.characters(number: 100)
click_button 'Create Tweet'
expect(page).to have_content Tweet.last.content
end
it "displays message 'Content can't be blank' without content" do
click_button 'Create Tweet'
expect(page).to have_content "Content can't be blank"
end
it "returns a 422 Unprocessable Entity status without content" do
click_button 'Create Tweet'
expect(page).to have_http_status(422)
end
it "displays message 'Content is too long (maximum is 140 characters)' with a content longer than 140" do
fill_in 'tweet[content]', with: Faker::Lorem.characters(number: 141)
click_button 'Create Tweet'
expect(page).to have_content "Content is too long (maximum is 140 characters)"
end
it "returns a 422 Unprocessable Entity status with a content longer than 140" do
fill_in 'tweet[content]', with: Faker::Lorem.characters(number: 141)
click_button 'Create Tweet'
expect(page).to have_http_status(422)
end
end
end
describe 'Details screen' do
before do
visit tweet_path(tweet)
end
describe '#show' do
it "displays content of the tweet" do
expect(page).to have_content tweet.content
end
it "displays a link to the editing screen" do
expect(page).to have_link 'Edit this tweet', href: "/tweets/#{tweet.id}/edit"
end
it "displays a link to back to list screen" do
expect(page).to have_link 'Back to tweets', href: "/tweets"
end
end
describe '#destroy' do
it "decreases the number of tweet" do
count = Tweet.count
click_on 'Destroy this tweet'
expect(Tweet.count).to eq (count - 1)
end
it "redirects to the list screen" do
click_on 'Destroy this tweet'
expect(current_path).to eq tweets_path
end
end
end
describe 'Editing screen' do
before do
visit edit_tweet_path(tweet)
end
describe '#edit' do
it "displays the content in the form" do
expect(page).to have_field 'tweet[content]', with: tweet.content
end
it 'displays a Update Tweet Button' do
expect(page).to have_button 'Update Tweet'
end
it 'displays a link to the details screen' do
expect(page).to have_link 'Show this tweet', href: "/tweets/#{tweet.id}"
end
it 'displays a link to back to list screen' do
expect(page).to have_link 'Back to tweets', href: "/tweets"
end
end
describe '#update' do
it "redirects to the details screen with a valid content" do
fill_in 'tweet[content]', with: Faker::Lorem.characters(number: 100)
click_button 'Update Tweet'
expect(current_path).to eq tweet_path(Tweet.last)
end
it "displays created tweet with a valid content" do
fill_in 'tweet[content]', with: Faker::Lorem.characters(number: 100)
click_button 'Update Tweet'
expect(page).to have_content Tweet.last.content
end
it "displays message 'Content can't be blank' without content" do
fill_in 'tweet[content]', with: ""
click_button 'Update Tweet'
expect(page).to have_content "Content can't be blank"
end
it "returns a 422 Unprocessable Entity status without content" do
fill_in 'tweet[content]', with: ""
click_button 'Update Tweet'
expect(page).to have_http_status(422)
end
it "displays message 'Content is too long (maximum is 140 characters)' with a content longer than 140" do
fill_in 'tweet[content]', with: Faker::Lorem.characters(number: 141)
click_button 'Update Tweet'
expect(page).to have_content "Content is too long (maximum is 140 characters)"
end
it "returns a 422 Unprocessable Entity status with a content longer than 140" do
fill_in 'tweet[content]', with: Faker::Lorem.characters(number: 141)
click_button 'Update Tweet'
expect(page).to have_http_status(422)
end
end
end
end
```
#### `create()` と `build()` について
`build()` はオブジェクト化するだけ。データベースに登録されない。
下記はパスしないテストの例。
```ruby
let(:tweet) { build(:tweet) }
it 'get the tweet' do
expect(Tweet.first).to eq tweet
end
```
`create()` はデータベースへアクセスし、インスタンスを保存する。
#### `let` と `let!` について
`let` で指定されたものは **遅延評価** といって it の中で `tweet` が出てきたときにはじめて実行される。
`let!` で指定されたものは **事前評価** といって、各テストのブロック実行前に定義した定数を作る。
下記のようなテストにおいて、`let!` と `let` にするとテストがパスしない。
```ruby
it 'displays content of the tweet' do
expect(page).to have_content tweet.content
end
```
`let` を使った場合は、
1. `expect(page)` が実行される。
2. `.to` が実行される。
3. `have_content tweet.content` が実行される。(ここで、`tweet` が初めて登場する。まだ tweet が生成されていないため `have_content tweet.content` が失敗する。)
(4. `tweet` が生成される。)
という流れになる。
#### `before` について
`before ~ do` で囲んだ処理は `it` の直前に毎回呼び出される。
```ruby
before do
visit tweets_path
end
```
と記述すると、`it` の前に毎回ツイートの一覧画面に遷移する。
#### 画面やフォームの検証について
| メソッド | 意味 |
| -------- | -------- |
| `have_content` | ページ内に特定の文字列が表示されていることを検証する |
| `have_link` | ページ内に特定のリンクが表示されていることを検証する |
| `have_button` | ページ内に特定のボタンが表示されていることを検証する |
| `have_field` | ページ内に特定の入力フィールドがあることを検証する( name 属性で判断する) |
#### Capybara の基本メソッドについて
| メソッド | 操作 |
| -------- | -------- |
| `visit` | 指定したページに遷移 |
| `click_link` | 指定したリンク文字列を持つ a タグをクリック |
| `fill_in` | 入力したい文字列を指定のフォームに入力 |
| `click_button` | 指定したラベルを持つボタンをクリック |
#### 422 Unprocessable Entity status について
処理できないエンティティ。REST において、入力値の検証の失敗(バリデーションエラー)を伝える目的で使用する場合もある。
#### SQL のログ表示について
spec/rails_helper.rb
```diff ruby
+ # SQL Logs
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
RSpec.configure do |config|
# 省略
end
```
## 完成版のコード
https://github.com/FarStep131/rspec_tutorial