1. Pengenalan Testing dan RSpec
Testing adalah proses memverifikasi bahwa kode Anda berfungsi sebagaimana mestinya. Dalam pengembangan software, testing sangat penting untuk memastikan kualitas kode, mencegah bug, dan memudahkan refactoring. RSpec adalah framework testing paling populer di Ruby yang mengikuti pendekatan BDD (Behavior-Driven Development).
BDD menggabungkan prinsip TDD (Test-Driven Development) dengan sintaks yang lebih natural dan mudah dibaca. RSpec menulis test dalam bentuk "specification" β deskripsi perilaku yang diharapkan dari kode, bukan sekadar test teknis.
TDD vs BDD
| Aspek | TDD (Test-Driven) | BDD (Behavior-Driven) |
|---|---|---|
| Fokus | Unit test individual | Perilaku dan spesifikasi |
| Sintaks | assert_equal 5, result | expect(result).to eq(5) |
| Bahasa | Teknis | Mendekati bahasa manusia |
| Framework | Minitest | RSpec |
| Siklus | Red β Green β Refactor | Given β When β Then |
| Dokumentasi | Tidak otomatis | Bisa jadi dokumentasi hidup |
Mengapa Menggunakan RSpec?
| Keunggulan | Penjelasan |
|---|---|
| Sintaks Elegan | Membaca test seperti membaca spesifikasi dalam bahasa Inggris |
| Matchers Kaya | Ratusan matcher built-in dan kemampuan membuat custom matcher |
| Nested Context | Bisa mengelompokkan test berdasarkan konteks dan kondisi |
| Shared Examples | Bagikan test patterns antar spec files |
| Ecosystem | FactoryBot, Capybara, Shoulda Matchers, SimpleCov |
| Output Readable | Output yang berwarna dan terstruktur saat test dijalankan |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β RSPEC ECOSYSTEM β β β β ββββββββββββ ββββββββββββ ββββββββββββββββββββ β β β RSpec β β Factory β β Matchers β β β β Core β β Bot β β eq, include β β β β describeβ β FactoryBotβ β be_, have_ β β β β context β β traits β β raise_error β β β ββββββββββββ ββββββββββββ ββββββββββββββββββββ β β β β ββββββββββββ ββββββββββββ ββββββββββββββββββββ β β β Capybara β βCoverage β β Mocking β β β β Feature β β SimpleCovβ β allow/expect β β β β Scenariosβ β Reports β β double/instanceβ β β ββββββββββββ ββββββββββββ ββββββββββββββββββββ β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
2. Setup RSpec
Instalasi RSpec
# Instal RSpec gem gem install rspec # Verifikasi instalasi rspec --version # rspec 3.13.0 # Untuk proyek Ruby biasa, buat file spec_helper.rb rspec --init # Menghasilkan: # - .rspec (configurasi) # - spec/spec_helper.rb # Untuk Rails: # Tambahkan ke Gemfile: # group :development, :test do # gem 'rspec-rails' # gem 'factory_bot_rails' # gem 'faker' # end # Jalankan bundle install bundle install # Generate RSpec di Rails rails generate rspec:install # Menghasilkan: # - .rspec # - spec/spec_helper.rb # - spec/rails_helper.rb
Konfigurasi .rspec
# File: .rspec --format documentation --color --require spec_helper # Opsi yang tersedia: # --format documentation : Output detail dengan indentasi # --format progress : Output ringkas (titik-titik) # --color : Output berwarna # --require spec_helper : Auto-require spec_helper # --fail-fast : Berhenti di test pertama yang gagal # --bisect : Cari kombinasi test yang menyebabkan kegagalan
Konfigurasi spec_helper.rb
# spec/spec_helper.rb
RSpec.configure do |config|
# Output format
config.formatter = :documentation
# Filter
config.filter_run_when_matching :focus
# Random order (untuk mendeteksi test yang bergantung satu sama lain)
config.order = :random
Kernel.srand config.seed
# Shared context
config.shared_context_metadata_behavior = :apply_to_host_groups
# Hooks
config.before(:suite) do
puts "=== Memulai test suite ==="
end
config.after(:suite) do
puts "=== Test suite selesai ==="
end
# Disable monkey patching (recommended)
config.disable_monkey_patching!
# Expose DSL globally
config.expose_dsl_globally = true
end
Menjalankan RSpec
# Jalankan semua test rspec # Jalankan file tertentu rspec spec/models/user_spec.rb # Jalankan test tertentu berdasarkan baris rspec spec/models/user_spec.rb:25 # Jalankan dengan format tertentu rspec --format documentation # Jalankan test yang mengandung tag rspec --tag focus rspec --tag ~slow # Skip test bertanda :slow # Jalankan dengan backtrace lengkap rspec --backtrace # Jalankan test secara acak rspec --order random # Seed untuk reproduce urutan random rspec --seed 12345
3. RSpec Basics: describe, context, it
RSpec memiliki tiga blok utama yang membentuk struktur test: describe, context, dan it.
Struktur Dasar
# Class yang akan di-test
# lib/calculator.rb
class Calculator
def add(a, b)
a + b
end
def subtract(a, b)
a - b
end
def multiply(a, b)
a * b
end
def divide(a, b)
raise ZeroDivisionError, "Tidak bisa bagi dengan nol" if b == 0
a.to_f / b
end
end
# spec/calculator_spec.rb
require 'calculator'
# describe β mengelompokkan test untuk class/method tertentu
RSpec.describe Calculator do
# describe untuk method tertentu
describe '#add' do
# it β satu test case individual
it 'menjumlahkan dua angka positif' do
calc = Calculator.new
expect(calc.add(2, 3)).to eq(5)
end
it 'menjumlahkan angka negatif' do
calc = Calculator.new
expect(calc.add(-1, -3)).to eq(-4)
end
it 'menjumlahkan angka nol' do
calc = Calculator.new
expect(calc.add(0, 5)).to eq(5)
end
end
describe '#subtract' do
it 'mengurangi dua angka' do
calc = Calculator.new
expect(calc.subtract(10, 3)).to eq(7)
end
end
describe '#multiply' do
it 'mengalikan dua angka' do
calc = Calculator.new
expect(calc.multiply(4, 5)).to eq(20)
end
end
describe '#divide' do
# context β mengelompokkan test berdasarkan kondisi
context 'dengan pembagi valid' do
it 'membagi dua angka' do
calc = Calculator.new
expect(calc.divide(10, 2)).to eq(5.0)
end
it 'mengembalikan float' do
calc = Calculator.new
expect(calc.divide(7, 2)).to eq(3.5)
end
end
context 'dengan pembagi nol' do
it 'melempar ZeroDivisionError' do
calc = Calculator.new
expect { calc.divide(10, 0) }.to raise_error(ZeroDivisionError)
end
it 'melempar error dengan pesan yang benar' do
calc = Calculator.new
expect { calc.divide(10, 0) }.to raise_error("Tidak bisa bagi dengan nol")
end
end
end
end
Output RSpec
# Format documentation: # Calculator # #add # menambahkan dua angka positif # menambahkan angka negatif # menambahkan angka nol # #subtract # mengurangi dua angka # #multiply # mengalikan dua angka # #divide # dengan pembagi valid # membagi dua angka # mengembalikan float # dengan pembagi nol # melempar ZeroDivisionError # melempar error dengan pesan yang benar # # Finished in 0.00234 seconds (files took 0.15678 seconds to load) # 9 examples, 0 failures # Format progress (default): # ......... # # Finished in 0.00234 seconds # 9 examples, 0 failures
Pending Test
# Pending test β test yang belum selesai ditulis
RSpec.describe Calculator do
# Menggunakan xit β test yang di-skip
xit 'fitur masa depan' do
# Test ini tidak dijalankan
end
# Menggunakan pending β test yang belum selesai
it 'fitur yang belum selesai' do
pending "Belum diimplementasikan"
expect(Calculator.new.add(1, 1)).to eq(3) # Akan gagal, tapi dianggap pending
end
# Menggunakan skip β test yang di-skip
it 'test yang di-skip', skip: 'Untuk nanti' do
expect(true).to be true
end
# Focus β menjalankan hanya test yang difokuskan
it 'test fokus', :focus do
expect(Calculator.new.add(1, 1)).to eq(2)
end
end
4. Matchers
Matchers adalah inti dari RSpec. Matcher menentukan apa yang diharapkan dari hasil test. RSpec memiliki ratusan matcher built-in.
Equality Matchers
RSpec.describe 'Equality Matchers' do
# eq β equality (==)
it 'eq: equality' do
expect(2 + 2).to eq(4)
expect("hello").to eq("hello")
end
# eql β strict equality (eql?)
it 'eql: strict equality' do
expect(2 + 2).to eql(4)
expect(2.0).not_to eql(2) # Float != Integer
end
# equal β identity (equal?, same object)
it 'equal: identity check' do
a = "hello"
b = a
expect(a).to equal(b) # Same object
expect(a).not_to equal("hello") # Different object
end
# be β alias for equal
it 'be: identity' do
x = [1, 2, 3]
y = x
expect(x).to be(y)
expect(x).not_to be([1, 2, 3])
end
# be_nil, be_truthy, be_falsey, be_falsy
it 'be_nil' do
expect(nil).to be_nil
expect(0).not_to be_nil
end
it 'be_truthy dan be_falsey' do
expect(true).to be_truthy
expect(42).to be_truthy
expect(false).to be_falsey
expect(nil).to be_falsey
end
end
Comparison Matchers
RSpec.describe 'Comparison Matchers' do
# be_ comparison matchers
it 'be_, be_, be_, be_' do
expect(10).to be > 5
expect(3).to be < 10
expect(7).to be >= 7
expect(5).to be <= 10
expect(5).to be_between(1, 10)
end
# be_within
it 'be_within' do
expect(3.14159).to be_within(0.01).of(3.14)
expect(100).to be_within(5).of(98)
end
# be_instance_of dan be_kind_of
it 'be_instance_of dan be_kind_of' do
expect("hello").to be_instance_of(String)
expect(42).to be_instance_of(Integer)
expect(42).to be_kind_of(Numeric) # Integer is a kind of Numeric
end
# respond_to
it 'respond_to' do
expect("hello").to respond_to(:length)
expect("hello").to respond_to(:upcase, :downcase)
end
# have_attributes
it 'have_attributes' do
user = OpenStruct.new(name: "Budi", age: 25, email: "budi@mail.com")
expect(user).to have_attributes(name: "Budi", age: 25)
end
end
Collection Matchers
RSpec.describe 'Collection Matchers' do
# include
it 'include' do
expect([1, 2, 3]).to include(2)
expect([1, 2, 3]).to include(1, 3)
expect("hello world").to include("world")
expect({ a: 1, b: 2 }).to include(a: 1)
end
# all
it 'all' do
expect([2, 4, 6]).to all(be_even)
expect([1, 3, 5]).to all(be_odd)
expect(["hello", "world"]).to all(be_a(String))
end
# contain_exactly β same elements regardless of order
it 'contain_exactly' do
expect([3, 1, 2]).to contain_exactly(1, 2, 3)
expect([3, 1, 2]).not_to contain_exactly(1, 2, 4)
end
# match_array β alias for contain_exactly
it 'match_array' do
expect([3, 1, 2]).to match_array([1, 2, 3])
end
# start_with dan end_with
it 'start_with dan end_with' do
expect([1, 2, 3]).to start_with(1)
expect([1, 2, 3]).to end_with(3)
expect("hello world").to start_with("hello")
expect("hello world").to end_with("world")
end
end
String dan Regex Matchers
RSpec.describe 'String Matchers' do
# match β regex matcher
it 'match' do
expect("hello123").to match(/\w+\d+/)
expect("2024-01-15").to match(/\d{4}-\d{2}-\d{2}/)
end
# match? (Ruby 2.4+)
it 'match?' do
expect("test@mail.com").to match?(/\A[\w.]+@[\w.]+\z/)
end
end
5. let, before, after, subject
RSpec menyediakan helper let, before, after, dan subject untuk setup test yang lebih efisien dan bersih.
let dan let!
RSpec.describe User do
# let β lazy evaluation (dijalankan saat pertama kali dipanggil)
let(:user) { User.new("Budi", 25) }
let(:admin) { User.new("Admin", 30, role: "admin") }
# let! β eager evaluation (dijalankan sebelum setiap test)
let!(:saved_user) { User.create("Rina", 22) }
# Perbedaan penting:
# - let: objek dibuat saat pertama kali dipanggil di test
# - let!: objek dibuat SEBELUM setiap test (selalu)
it 'menggunakan lazy let' do
# user dibuat di sini, saat pertama kali dipanggil
expect(user.name).to eq("Budi")
expect(user.age).to eq(25)
end
it 'mereferensi let yang sama di test yang sama' do
# user adalah objek yang SAMA dalam test ini
expect(user).to equal(user)
end
it 'memiliki saved_user yang sudah dibuat' do
# saved_user sudah dibuat sebelum test ini
expect(User.all).to include(saved_user)
end
# let di-share dalam satu test
it 'berbeda antar test' do
expect(user).not_to equal(nil)
# Setiap test mendapat instance baru
end
end
before dan after Hooks
RSpec.describe 'Database Tests' do
# before(:each) β dijalankan sebelum setiap test
before(:each) do
@db = Database.new
@db.connect
puts "Setup: Database connected"
end
# after(:each) β dijalankan setelah setiap test
after(:each) do
@db.disconnect
puts "Teardown: Database disconnected"
end
# before(:all) β dijalankan sekali sebelum semua test di blok ini
before(:all) do
puts "=== Memulai group test ==="
end
# after(:all) β dijalankan sekali setelah semua test selesai
after(:all) do
puts "=== Selesai group test ==="
end
# Contoh dengan database transaction
context 'dengan data' do
before(:each) do
@db.begin_transaction
end
after(:each) do
@db.rollback_transaction # Bersihkan data setelah test
end
it 'menyimpan data' do
@db.execute("INSERT INTO users (name) VALUES ('Budi')")
expect(@db.query("SELECT * FROM users").length).to eq(1)
end
it 'tidak menyimpan data dari test sebelumnya' do
# Karena rollback, data test sebelumnya sudah hilang
expect(@db.query("SELECT * FROM users").length).to eq(0)
end
end
end
subject
RSpec.describe User do
# subject β objek utama yang sedang di-test
subject { User.new("Budi", 25) }
# named subject
subject(:user) { User.new("Budi", 25) }
# subject! β eager evaluation
subject!(:admin) { User.new("Admin", 30) }
# is_expected β shorthand untuk expect(subject)
it { is_expected.to be_a(User) }
it { is_expected.to respond_to(:name) }
it { is_expected.to respond_to(:age) }
# Menggunakan named subject
it 'memiliki nama' do
expect(user.name).to eq("Budi")
end
# Implicit subject: class.new
# Jika tidak didefinisikan, subject = User.new (tanpa argumen)
RSpec.describe Array do
# subject implicitly adalah Array.new (empty array)
it { is_expected.to be_empty }
end
end
6. Mocks dan Stubs
Mocks dan Stubs memungkinkan Anda mengisolasi unit test dari dependensi eksternal. Stub mengganti perilaku method, sedangkan Mock mengharapkan method dipanggil dengan cara tertentu.
Test Double
# Test Double β objek palsu untuk test
RSpec.describe 'Test Doubles' do
# double β objek palsu dengan method yang diharapkan
it 'double sederhana' do
user = double("User")
allow(user).to receive(:name).and_return("Budi")
expect(user.name).to eq("Budi")
end
# double dengan multiple methods
it 'double dengan banyak method' do
user = double("User", name: "Budi", age: 25, email: "budi@mail.com")
expect(user.name).to eq("Budi")
expect(user.age).to eq(25)
expect(user.email).to eq("budi@mail.com")
end
# instance_double β lebih ketat (cek method benar-benar ada)
it 'instance_double' do
# Ini hanya bisa menggunakan method yang benar-benar ada di User
user = instance_double("User", name: "Budi")
expect(user.name).to eq("Budi")
# user.nonexistent # Error! Method tidak ada di User
end
# class_double
it 'class_double' do
user_class = class_double("User")
allow(user_class).to receive(:find).and_return(double("User", name: "Budi"))
expect(user_class.find(1).name).to eq("Budi")
end
end
Stubs (allow)
# Stub β mengganti perilaku method
RSpec.describe 'Stubs' do
# allow().to receive().and_return()
it 'stub method sederhana' do
api = double("API")
allow(api).to receive(:fetch_users).and_return(["Budi", "Rina"])
expect(api.fetch_users).to eq(["Budi", "Rina"])
end
# Stub dengan multiple return values
it 'stub dengan return values berbeda' do
die = double("Die")
allow(die).to receive(:roll).and_return(1, 5, 3)
expect(die.roll).to eq(1)
expect(die.roll).to eq(5)
expect(die.roll).to eq(3)
end
# Stub dengan block (dynamic return)
it 'stub dengan block' do
counter = double("Counter")
allow(counter).to receive(:count) { rand(1..100) }
result = counter.count
expect(result).to be_between(1, 100)
end
# Stub yang melempar error
it 'stub yang melempar error' do
api = double("API")
allow(api).to receive(:fetch).and_raise(Timeout::Error, "Request timeout")
expect { api.fetch }.to raise_error(Timeout::Error)
end
# Stub pada object yang sudah ada
it 'stub pada object nyata' do
user = User.new("Budi", 25)
allow(user).to receive(:age).and_return(30) # Override age
expect(user.age).to eq(30) # Bukan 25!
expect(user.name).to eq("Budi") # Tetap normal
end
# and_call_original β panggil method asli
it 'and_call_original' do
user = User.new("Budi", 25)
allow(user).to receive(:age).and_call_original # Method asli
expect(user.age).to eq(25) # Memanggil method asli
end
end
Mocks (expect)
# Mock β mengharapkan method dipanggil
RSpec.describe 'Mocks' do
# expect().to receive() β mock yang harus dipanggil
it 'mock: method harus dipanggil' do
mailer = double("Mailer")
expect(mailer).to receive(:send_email).with("budi@mail.com", "Halo!")
mailer.send_email("budi@mail.com", "Halo!")
# Jika mailer.send_email tidak dipanggil, test gagal!
end
# Mock dengan arguments
it 'mock: harus dipanggil dengan argumen tertentu' do
logger = double("Logger")
expect(logger).to receive(:log).with("User created").once
logger.log("User created")
end
# Mock: tidak boleh dipanggil
it 'mock: tidak boleh dipanggil' do
mailer = double("Mailer")
expect(mailer).not_to receive(:send_email)
# mailer.send_email tidak boleh dipanggil
end
# Mock dengan jumlah panggilan
it 'mock: jumlah panggilan' do
counter = double("Counter")
expect(counter).to receive(:increment).exactly(3).times
3.times { counter.increment }
end
# Mock dengan ordered
it 'mock: urutan panggilan' do
api = double("API")
expect(api).to receive(:connect).ordered
expect(api).to receive(:fetch_data).ordered
expect(api).to receive(:disconnect).ordered
api.connect
api.fetch_data
api.disconnect
end
# Menggunakan expect dengan message object
it 'mock: dengan message expectation' do
user = double("User")
expect(user).to receive(:name).at_least(:once).and_return("Budi")
# Dipanggil di dalam kode yang sedang di-test
result = user.name
expect(result).to eq("Budi")
end
end
7. Shared Examples dan Contexts
Shared examples memungkinkan Anda mendefinisikan sekumpulan test yang bisa digunakan di beberapa spec files. Ini sangat berguna untuk menguji behavior yang dimiliki bersama oleh beberapa class.
Shared Examples
# Definisikan shared example
RSpec.shared_examples 'a timestamped model' do
it 'memiliki created_at' do
expect(subject).to respond_to(:created_at)
end
it 'memiliki updated_at' do
expect(subject).to respond_to(:updated_at)
end
it 'created_at terisi setelah save' do
subject.save
expect(subject.created_at).not_to be_nil
end
end
RSpec.shared_examples 'paginatable' do
describe '.paginated' do
before { 20.times { |i| described_class.create(name: "Item #{i}") } }
it 'mengembalikan 10 item per halaman' do
result = described_class.paginated(page: 1, per_page: 10)
expect(result.length).to eq(10)
end
it 'mengembalikan halaman kedua' do
result = described_class.paginated(page: 2, per_page: 10)
expect(result.length).to eq(10)
end
end
end
# Menggunakan shared examples
RSpec.describe User do
it_behaves_like 'a timestamped model'
it_behaves_like 'paginatable'
end
RSpec.describe Article do
it_behaves_like 'a timestamped model'
it_behaves_like 'paginatable'
end
# Shared context β untuk setup yang dibagikan
RSpec.shared_context 'authenticated user' do
let(:current_user) { User.new("Budi", 25, role: "admin") }
before do
allow_any_instance_of(ApplicationController)
.to receive(:current_user).and_return(current_user)
end
end
RSpec.describe 'Dashboard' do
include_context 'authenticated user'
it 'menampilkan nama user' do
expect(current_user.name).to eq("Budi")
end
end
RSpec.describe 'Settings' do
include_context 'authenticated user'
it 'menampilkan halaman pengaturan' do
expect(current_user.role).to eq("admin")
end
end
Custom Matchers
# Custom matcher β buat matcher Anda sendiri
RSpec::Matchers.define :be_valid_email do
match do |actual|
actual =~ /\A[\w.]+@[\w.]+\.[a-z]{2,}\z/
end
failure_message do |actual|
"'#{actual}' bukan format email yang valid"
end
failure_message_when_negated do |actual|
"'#{actual}' seharusnya bukan email valid"
end
description do
"memeriksa apakah string adalah email yang valid"
end
end
# Menggunakan custom matcher
RSpec.describe 'Email Validation' do
it 'memvalidasi email' do
expect("user@mail.com").to be_valid_email
expect("invalid-email").not_to be_valid_email
expect("@mail.com").not_to be_valid_email
end
end
# Custom matcher yang lebih kompleks
RSpec::Matchers.define :have_role do |expected_role|
match do |user|
user.role == expected_role
end
chain :in_group do |group|
@group = group
end
description do
"memiliki role '#{expected_role}'"
end
failure_message do |user|
"expected #{user.name} memiliki role '#{expected_role}', tapi role-nya '#{user.role}'"
end
end
RSpec.describe 'User Roles' do
let(:admin) { double("User", name: "Budi", role: "admin") }
let(:user) { double("User", name: "Rina", role: "member") }
it 'admin memiliki role admin' do
expect(admin).to have_role("admin")
end
it 'member memiliki role member' do
expect(user).to have_role("member")
end
end
8. FactoryBot
FactoryBot (sebelumnya FactoryGirl) adalah library untuk membuat test data secara efisien. FactoryBot sangat umum digunakan bersama RSpec dalam pengembangan Rails.
Mendefinisikan Factory
# spec/factories/users.rb
FactoryBot.define do
factory :user do
name { "Budi Santoso" }
email { "budi#{rand(1000)}@mail.com" }
age { 25 }
role { "member" }
active { true }
# Traits β variasi dari factory
trait :admin do
role { "admin" }
name { "Admin User" }
end
trait :inactive do
active { false }
end
trait :young do
age { 18 }
end
trait :with_posts do
after(:create) do |user|
create_list(:post, 3, user: user)
end
end
# Nested factory
factory :admin_user, traits: [:admin]
factory :inactive_user, traits: [:inactive]
end
end
# spec/factories/posts.rb
FactoryBot.define do
factory :post do
title { Faker::Lorem.sentence(word_count: 5) }
body { Faker::Lorem.paragraph(sentence_count: 10) }
status { "draft" }
association :user
trait :published do
status { "published" }
published_at { Time.current }
end
trait :with_comments do
after(:create) do |post|
create_list(:comment, 5, post: post)
end
end
end
end
Menggunakan FactoryBot
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
# build β buat objek tanpa menyimpan ke database
describe 'attributes' do
it 'memiliki atribut yang benar' do
user = build(:user)
expect(user.name).to be_present
expect(user.email).to be_present
end
end
# create β buat objek dan simpan ke database
describe 'database' do
it 'menyimpan user ke database' do
user = create(:user)
expect(User.find(user.id)).to eq(user)
end
end
# build_stubbed β buat objek palsu dengan ID
describe 'fast test' do
it 'tidak perlu database' do
user = build_stubbed(:user)
expect(user.id).not_to be_nil # Ada ID palsu
expect(user).to be_persisted # Mengaku persisted
end
end
# Menggunakan traits
describe 'traits' do
it 'admin user' do
admin = create(:user, :admin)
expect(admin.role).to eq("admin")
end
it 'inactive user' do
inactive = create(:user, :inactive)
expect(inactive).not_to be_active
end
it 'admin dengan posts' do
admin = create(:user, :admin, :with_posts)
expect(admin.posts.count).to eq(3)
end
end
# attributes_for β buat hash atribut
describe 'attributes_for' do
it 'mengembalikan hash atribut' do
attrs = attributes_for(:user)
expect(attrs).to include(:name, :email, :age)
expect(attrs[:name]).to eq("Budi Santoso")
end
end
# build_list dan create_list
describe 'list' do
it 'membuat banyak user' do
users = create_list(:user, 5)
expect(User.count).to eq(5)
end
it 'build banyak user tanpa simpan' do
users = build_list(:user, 10)
expect(users.length).to eq(10)
end
end
end
9. RSpec dengan Rails
Rails menggunakan RSpec melalui gem rspec-rails yang menyediakan type-specific helpers untuk model, controller, request, dan feature specs.
Model Spec
require 'rails_helper'
RSpec.describe User, type: :model do
describe 'validations' do
it 'valid dengan atribut lengkap' do
user = build(:user)
expect(user).to be_valid
end
it 'tidak valid tanpa nama' do
user = build(:user, name: nil)
expect(user).not_to be_valid
expect(user.errors[:name]).to include("can't be blank")
end
it 'tidak valid dengan email duplikat' do
create(:user, email: "test@mail.com")
user = build(:user, email: "test@mail.com")
expect(user).not_to be_valid
expect(user.errors[:email]).to include("has already been taken")
end
it 'tidak valid dengan email format salah' do
user = build(:user, email: "not-an-email")
expect(user).not_to be_valid
expect(user.errors[:email]).to be_present
end
it 'tidak valid dengan umur negatif' do
user = build(:user, age: -1)
expect(user).not_to be_valid
end
end
describe 'associations' do
it 'memiliki banyak posts' do
user = create(:user)
expect(user).to respond_to(:posts)
end
it 'menghapus posts saat user dihapus' do
user = create(:user, :with_posts)
expect { user.destroy }.to change(Post, :count).by(-3)
end
end
describe 'scopes' do
it '.active mengembalikan user aktif' do
active = create(:user, active: true)
inactive = create(:user, :inactive)
expect(User.active).to include(active)
expect(User.active).not_to include(inactive)
end
end
describe '#full_name' do
it 'mengembalikan nama lengkap' do
user = build(:user, name: "Budi Santoso")
expect(user.full_name).to eq("Budi Santoso")
end
end
end
Request Spec (Controller)
require 'rails_helper'
RSpec.describe 'Users API', type: :request do
describe 'GET /users' do
before { create_list(:user, 3) }
it 'mengembalikan semua user' do
get users_path
expect(response).to have_http_status(:ok)
expect(response.body).to include(User.first.name)
end
end
describe 'GET /users/:id' do
let(:user) { create(:user) }
it 'mengembalikan user tertentu' do
get user_path(user)
expect(response).to have_http_status(:ok)
expect(response.body).to include(user.name)
end
end
describe 'POST /users' do
context 'dengan data valid' do
let(:valid_params) { { user: attributes_for(:user) } }
it 'membuat user baru' do
expect {
post users_path, params: valid_params
}.to change(User, :count).by(1)
expect(response).to have_http_status(:redirect)
expect(flash[:notice]).to eq("User berhasil dibuat!")
end
end
context 'dengan data tidak valid' do
let(:invalid_params) { { user: attributes_for(:user, name: nil) } }
it 'tidak membuat user' do
expect {
post users_path, params: invalid_params
}.not_to change(User, :count)
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
describe 'DELETE /users/:id' do
let!(:user) { create(:user) }
it 'menghapus user' do
expect {
delete user_path(user)
}.to change(User, :count).by(-1)
expect(response).to redirect_to(users_path)
end
end
end
10. Best Practices
Conventions dan Tips
# β
BEST PRACTICES:
# 1. One expectation per test (kecuali integration test)
# BAD
it 'memvalidasi semua field' do
expect(user.name).to eq("Budi")
expect(user.email).to eq("budi@mail.com")
expect(user.age).to eq(25)
end
# GOOD
it 'memiliki nama yang benar' do
expect(user.name).to eq("Budi")
end
# 2. Gunakan described_class untuk referensi class yang sedang di-test
RSpec.describe User do
it 'membuat user baru' do
user = described_class.new # Sama dengan User.new
expect(user).to be_a(described_class)
end
end
# 3. Jangan test implementation, test behavior
# BAD β test implementation
it 'memanggil method validate_name' do
expect(user).to receive(:validate_name)
user.save
end
# GOOD β test behavior
it 'tidak valid tanpa nama' do
user.name = nil
expect(user).not_to be_valid
end
# 4. Gunakan let untuk setup, bukan instance variables
# BAD
before(:each) do
@user = User.new("Budi", 25)
end
# GOOD
let(:user) { User.new("Budi", 25) }
# 5. Gunakan context untuk mengelompokkan skenario
RSpec.describe User do
describe '#full_name' do
context 'ketika semua field terisi' do
it 'mengembalikan nama lengkap' do
# ...
end
end
context 'ketika nama belakang kosong' do
it 'mengembalikan nama depan saja' do
# ...
end
end
end
end
# 6. Test nama yang deskriptif
# BAD
it 'berhasil' do
# ...
end
# GOOD
it 'mengembalikan daftar user yang aktif' do
# ...
end
# 7. Jalankan test secara acak untuk mendeteksi dependencies
# .rspec: --order random
Coverage dengan SimpleCov
# Gemfile # gem 'simplecov', require: false, group: :test # spec/spec_helper.rb β TAMBAHKAN DI PALING ATAS require 'simplecov' SimpleCov.start do add_filter '/spec/' add_filter '/config/' add_filter '/vendor/' add_group 'Models', 'app/models' add_group 'Controllers', 'app/controllers' add_group 'Services', 'app/services' minimum_coverage 80 # Minimal 80% coverage end # Jalankan test: # bundle exec rspec # Buka coverage/index.html untuk melihat laporan coverage # Target coverage: # - Line coverage: minimal 90% # - Branch coverage: minimal 80% # - Critical models/controllers: 100%
11. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Ruby Testing dengan RSpec: