Unit Test #
Test bukan sekadar jaring pengaman — ia adalah cara berpikir tentang kode. Developer yang terbiasa menulis test sebelum atau bersamaan dengan kode produksi cenderung menghasilkan API yang lebih bersih, kelas yang lebih terfokus, dan kode yang lebih mudah di-refactor. Ruby punya ekosistem testing yang sangat kaya: MiniTest yang ringan dan sudah bawaan, RSpec yang ekspresif dan jadi standar de-facto komunitas Rails, ditambah alat pendukung seperti factory_bot untuk test data dan SimpleCov untuk mengukur coverage. Artikel ini membahas keduanya secara mendalam — bukan hanya sintaks, tapi filosofi di balik mengapa dan bagaimana menulis test yang benar-benar bermakna.
Mengapa Unit Test Penting #
Sebelum masuk ke kode, penting memahami nilai nyata dari testing:
Test memberikan:
✓ Kepercayaan diri untuk refactor — ubah implementasi tanpa takut merusak perilaku
✓ Dokumentasi yang hidup — test yang baik menjelaskan "apa" yang dilakukan kode
✓ Desain yang lebih baik — kode yang sulit ditest biasanya punya desain yang buruk
✓ Deteksi regresi — bug yang pernah ada tidak akan muncul kembali tanpa terdeteksi
✓ Feedback loop cepat — tahu dalam detik apakah perubahan merusak sesuatu
Tanpa test:
✗ Setiap perubahan adalah gambling
✗ "Bekerja di mesin saya" tidak bisa dibuktikan
✗ Refactoring menjadi pekerjaan yang menakutkan
✗ Bug yang sama bisa muncul berulang kali
Piramida Test #
flowchart TD
A["E2E / Integration Tests\n(sedikit, lambat, tapi high-value)\nSelenium, Capybara"] --> B
B["Service / Integration Tests\n(sedang, test antar komponen)\nRequest specs, service objects"] --> C
C["Unit Tests\n(banyak, cepat, isolated)\nModel specs, PORO, service objects"]
style A fill:#ff6b6b
style B fill:#ffd93d
style C fill:#6bcb77Unit test berada di dasar piramida — paling banyak jumlahnya, paling cepat dijalankan, dan memberikan feedback paling cepat ketika ada yang rusak. Artikel ini fokus pada level ini.
MiniTest — Testing Bawaan Ruby #
MiniTest sudah menjadi bagian dari standard library Ruby sejak versi 1.9. Ia ringan, cepat, dan cukup lengkap untuk sebagian besar kebutuhan.
Setup dan Struktur Dasar #
# Tidak perlu instalasi — sudah bawaan Ruby
require 'minitest/autorun'
# Kode yang akan ditest (biasanya di file terpisah)
class Kalkulator
def tambah(a, b)
raise ArgumentError, "Hanya menerima angka" unless a.is_a?(Numeric) && b.is_a?(Numeric)
a + b
end
def bagi(a, b)
raise ZeroDivisionError, "Tidak bisa bagi dengan nol" if b == 0
a.fdiv(b)
end
def faktorial(n)
raise ArgumentError, "Input harus non-negatif" if n < 0
return 1 if n <= 1
n * faktorial(n - 1)
end
end
# Test class — nama harus dimulai dengan "Test" atau diakhiri dengan "Test"
class KalkulatorTest < Minitest::Test
# setup dipanggil sebelum SETIAP test method
def setup
@kalkulator = Kalkulator.new
end
# teardown dipanggil setelah SETIAP test method
def teardown
# cleanup jika ada resource yang perlu ditutup
end
# Test method harus dimulai dengan "test_"
def test_tambah_dua_angka_positif
assert_equal 5, @kalkulator.tambah(2, 3)
end
def test_tambah_angka_negatif
assert_equal(-3, @kalkulator.tambah(-5, 2))
end
def test_tambah_float
assert_in_delta 0.3, @kalkulator.tambah(0.1, 0.2), 0.001
end
def test_bagi_normal
assert_in_delta 2.5, @kalkulator.bagi(5, 2), 0.001
end
def test_bagi_dengan_nol_melempar_exception
assert_raises(ZeroDivisionError) do
@kalkulator.bagi(10, 0)
end
end
def test_tambah_input_bukan_angka_melempar_exception
error = assert_raises(ArgumentError) do
@kalkulator.tambah("a", 1)
end
assert_match(/Hanya menerima angka/, error.message)
end
def test_faktorial_nol
assert_equal 1, @kalkulator.faktorial(0)
end
def test_faktorial_positif
assert_equal 120, @kalkulator.faktorial(5)
end
def test_faktorial_negatif_melempar_exception
assert_raises(ArgumentError) { @kalkulator.faktorial(-1) }
end
end
Semua Assertion MiniTest #
class AssertionContohTest < Minitest::Test
# Kesamaan nilai
assert_equal expected, actual # nilai sama
assert_in_delta 0.3, 0.1 + 0.2, 0.001 # Float dengan toleransi
assert_in_epsilon 3.14, Math::PI, 0.01 # toleransi relatif
# Boolean dan nil
assert kondisi # kondisi truthy
refute kondisi # kondisi falsy
assert_nil nilai # nilai adalah nil
refute_nil nilai # nilai bukan nil
assert_empty koleksi # koleksi kosong
refute_empty koleksi # koleksi tidak kosong
# String dan pattern
assert_match /pola/, string # cocok dengan regex
refute_match /pola/, string # tidak cocok dengan regex
assert_includes koleksi, elemen # elemen ada di koleksi
refute_includes koleksi, elemen # elemen tidak ada di koleksi
# Tipe dan identitas
assert_instance_of Kelas, objek # objek adalah instance tepat dari Kelas
assert_kind_of Kelas, objek # objek adalah Kelas atau subkelasnya
assert_respond_to objek, :method # objek punya method tersebut
assert_same obj1, obj2 # identitas objek sama (object_id)
# Exception
assert_raises(ErrorKelas) { kode_berisiko }
assert_raises(ArgumentError, TypeError) { kode_berisiko }
# Output
assert_output("expected\n") { puts "expected" }
assert_silent { kode_tanpa_output }
# Comparison
assert_operator 5, :>, 3 # 5 > 3
assert_predicate [].empty? # predikat truthy
# Selalu gagal / selalu lulus
flunk "Pesan kegagalan" # paksa gagal
pass # selalu lulus (placeholder)
end
# Menjalankan test MiniTest
ruby test/kalkulator_test.rb
# Dengan output yang lebih verbose
ruby test/kalkulator_test.rb --verbose
# Jalankan satu test method saja
ruby test/kalkulator_test.rb --name test_tambah_dua_angka_positif
# Jalankan semua test dengan Rake
rake test
# Output contoh:
# Run options: --seed 12345
# Running:
# .......F.
# Finished in 0.002345s, 3847.5 tests/s.
# 9 runs, 9 assertions, 1 failures, 0 errors, 0 skips
MiniTest Spec — Gaya yang Lebih Ekspresif #
MiniTest juga punya mode “spec” yang sintaksnya mirip RSpec:
require 'minitest/autorun'
require 'minitest/spec'
describe Kalkulator do
before { @kalkulator = Kalkulator.new }
describe "#tambah" do
it "menjumlahkan dua angka positif" do
_(@kalkulator.tambah(2, 3)).must_equal 5
end
it "menerima angka negatif" do
_(@kalkulator.tambah(-5, 2)).must_equal(-3)
end
it "melempar ArgumentError untuk input non-angka" do
_{ @kalkulator.tambah("a", 1) }.must_raise ArgumentError
end
end
describe "#bagi" do
it "melempar ZeroDivisionError jika pembagi nol" do
_{ @kalkulator.bagi(10, 0) }.must_raise ZeroDivisionError
end
end
end
RSpec — Framework Testing Standar Komunitas #
RSpec adalah framework BDD (Behavior-Driven Development) yang paling populer di ekosistem Ruby. Sintaksnya dirancang untuk dibaca seperti kalimat bahasa Inggris.
Setup dan Konfigurasi #
# Instalasi
gem install rspec
# Atau di Gemfile
# group :test do
# gem 'rspec', '~> 3.12'
# end
# Inisialisasi struktur direktori
rspec --init
# Membuat: .rspec, spec/spec_helper.rb
# .rspec — opsi default yang selalu digunakan
--require spec_helper
--format documentation
--color
--order random # jalankan test dalam urutan acak untuk deteksi order dependency
# spec/spec_helper.rb
RSpec.configure do |config|
# Nonaktifkan sintaks monkey-patching (should/describe tanpa RSpec.)
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true # cek apakah method benar-benar ada
end
config.shared_context_metadata_behavior = :apply_to_host_groups
config.filter_run_when_matching :focus # jalankan hanya :focus examples
config.example_status_persistence_file_path = ".rspec_status"
config.disable_monkey_patching!
config.warnings = true
config.order = :random
config.profile_examples = 10 # tampilkan 10 test paling lambat
end
Struktur dan Sintaks Dasar #
# spec/kalkulator_spec.rb
require 'spec_helper'
require_relative '../lib/kalkulator'
RSpec.describe Kalkulator do
# subject — objek yang sedang ditest
subject(:kalkulator) { described_class.new }
# let — lazy evaluation, dibuat ulang setiap example
let(:angka_positif) { 42 }
let(:angka_negatif) { -7 }
# let! — eager evaluation, dibuat sebelum setiap example
# let!(:user) { User.create!(nama: "Rina") }
describe "#tambah" do
context "ketika dua angka positif" do
it "mengembalikan jumlah yang benar" do
expect(kalkulator.tambah(2, 3)).to eq(5)
end
it "menghasilkan Integer jika kedua input Integer" do
expect(kalkulator.tambah(2, 3)).to be_a(Integer)
end
end
context "ketika salah satu angka negatif" do
it "tetap menghasilkan hasil yang benar" do
expect(kalkulator.tambah(-5, 3)).to eq(-2)
end
end
context "ketika input bukan angka" do
it "melempar ArgumentError" do
expect { kalkulator.tambah("a", 1) }.to raise_error(ArgumentError)
end
it "pesan error menjelaskan masalahnya" do
expect { kalkulator.tambah("a", 1) }
.to raise_error(ArgumentError, /Hanya menerima angka/)
end
end
end
describe "#bagi" do
it "mengembalikan Float" do
expect(kalkulator.bagi(7, 2)).to be_a(Float)
end
it "hasil pembagian mendekati nilai yang benar" do
expect(kalkulator.bagi(1, 3)).to be_within(0.001).of(0.333)
end
context "ketika pembagi nol" do
it "melempar ZeroDivisionError" do
expect { kalkulator.bagi(10, 0) }.to raise_error(ZeroDivisionError)
end
end
end
end
Semua Matcher RSpec yang Penting #
# Kesamaan
expect(nilai).to eq(5) # == operator
expect(obj1).to equal(obj2) # identitas (object_id sama)
expect(nilai).to eql(5) # == dan tipe sama
# Perbandingan
expect(nilai).to be > 3
expect(nilai).to be >= 3
expect(nilai).to be < 10
expect(nilai).to be_between(1, 10).inclusive
expect(nilai).to be_within(0.001).of(3.14) # untuk Float
# Boolean dan nil
expect(nilai).to be_truthy # truthy (bukan nil/false)
expect(nilai).to be_falsy # falsy (nil atau false)
expect(nilai).to be_nil
expect(nilai).not_to be_nil
expect(nilai).to be true # persis true
expect(nilai).to be false # persis false
# Predikat — method yang diakhiri ?
expect([]).to be_empty
expect(string).to be_blank # ActiveSupport
expect(string).to be_frozen
expect(0).to be_zero
expect(4).to be_even
expect(3).to be_odd
expect(user).to be_aktif # panggil user.aktif?
# String dan Regex
expect(string).to include("substring")
expect(string).to start_with("prefix")
expect(string).to end_with("suffix")
expect(string).to match(/pola regex/)
# Koleksi
expect(array).to include(1, 2, 3)
expect(array).to contain_exactly(3, 1, 2) # sama tapi urutan bebas
expect(array).to match_array([3, 1, 2]) # alias contain_exactly
expect(array).to have_attributes(length: 3)
expect(array).to all(be_positive) # semua elemen memenuhi matcher
expect(array).to include(be > 5) # ada elemen yang > 5
# Tipe
expect(objek).to be_a(Kelas)
expect(objek).to be_an_instance_of(Kelas)
expect(objek).to be_kind_of(Kelas)
expect(objek).to respond_to(:method_name)
expect(objek).to have_attributes(nama: "Rina", umur: 28)
# Exception
expect { kode }.to raise_error
expect { kode }.to raise_error(ArgumentError)
expect { kode }.to raise_error(ArgumentError, "pesan error")
expect { kode }.to raise_error(ArgumentError, /pattern/)
expect { kode }.not_to raise_error
# Output
expect { puts "halo" }.to output("halo\n").to_stdout
expect { warn "error" }.to output("error\n").to_stderr
# Perubahan nilai
expect { user.aktifkan! }.to change(user, :aktif?).from(false).to(true)
expect { list.push(item) }.to change(list, :length).by(1)
expect { list.push(item) }.to change { list.length }.by(1)
expect { proses }.to change { Model.count }.from(0).to(3)
Hooks — Before, After, Around #
RSpec.describe User do
# Urutan eksekusi:
# before(:suite) → sekali sebelum semua spec
# before(:all) → sekali sebelum semua example dalam describe/context ini
# before(:each) → sebelum setiap example ← paling umum
# around(:each) → membungkus setiap example
# after(:each) → setelah setiap example
# after(:all) → sekali setelah semua example
# after(:suite) → sekali setelah semua spec
before(:each) do
# setup database, buat objek, dll
@user = User.new(nama: "Rina", email: "[email protected]")
end
after(:each) do
# cleanup — bersihkan database, file temp, dll
User.destroy_all if defined?(User)
end
around(:each) do |example|
# Berguna untuk transaksi database
ActiveRecord::Base.transaction do
example.run
raise ActiveRecord::Rollback # rollback setelah setiap test
end
end
# before(:all) — hati-hati! objek dibagi antar semua example
before(:all) do
@koneksi = buka_koneksi_mahal # hanya buat sekali
end
after(:all) do
@koneksi.tutup
end
end
Subject dan Let #
RSpec.describe Produk do
# subject — objek yang sedang ditest, tersedia sebagai 'subject'
subject { Produk.new(nama: "Laptop", harga: 15_000_000) }
# Atau dengan nama yang lebih ekspresif:
subject(:produk) { Produk.new(nama: "Laptop", harga: 15_000_000) }
# let — lazy, dibuat saat pertama kali dipanggil, di-cache dalam satu example
let(:harga_diskon) { produk.harga * 0.9 }
let(:diskon_persen) { 0.1 }
# let! — eager, dibuat sebelum example dijalankan meskipun tidak dipanggil
let!(:kategori) { Kategori.create!(nama: "Elektronik") }
it "punya harga yang benar" do
expect(produk.harga).to eq(15_000_000)
end
it "menghitung harga diskon" do
expect(harga_diskon).to eq(13_500_000.0)
end
# its — shortcut untuk atribut subject
its(:nama) { is_expected.to eq("Laptop") }
its(:harga) { is_expected.to be > 0 }
# Nested describe dengan let yang berbeda
context "dengan harga murah" do
let(:produk) { Produk.new(nama: "Mouse", harga: 350_000) }
it "harganya lebih rendah" do
expect(produk.harga).to be < 1_000_000
end
end
end
Shared Examples dan Shared Context #
# Shared examples — untuk perilaku yang sama di banyak kelas
RSpec.shared_examples "objek yang dapat divalidasi" do
it "valid dengan data yang benar" do
expect(subject).to be_valid
end
it "tidak valid tanpa nama" do
subject.nama = nil
expect(subject).not_to be_valid
expect(subject.errors[:nama]).to include("tidak boleh kosong")
end
it "tidak valid dengan nama terlalu pendek" do
subject.nama = "A"
expect(subject).not_to be_valid
end
end
RSpec.shared_examples "objek dengan timestamp" do
it "punya created_at setelah disimpan" do
subject.save!
expect(subject.created_at).not_to be_nil
end
it "punya updated_at yang berubah setelah update" do
subject.save!
waktu_awal = subject.updated_at
sleep 0.01
subject.touch
expect(subject.updated_at).to be > waktu_awal
end
end
# Penggunaan
RSpec.describe Produk do
subject { build(:produk) } # dengan factory_bot
it_behaves_like "objek yang dapat divalidasi"
it_behaves_like "objek dengan timestamp"
end
RSpec.describe User do
subject { build(:user) }
it_behaves_like "objek yang dapat divalidasi"
it_behaves_like "objek dengan timestamp"
end
# Shared context — untuk setup yang sama di banyak describe
RSpec.shared_context "pengguna yang sudah login" do
let(:user) { create(:user) }
before do
# setup session atau token autentikasi
allow_any_instance_of(ApplicationController)
.to receive(:current_user).and_return(user)
end
end
RSpec.describe OrdersController do
include_context "pengguna yang sudah login"
it "menampilkan daftar pesanan user" do
get :index
expect(response).to have_http_status(:ok)
end
end
Factory Bot — Test Data yang Elegan #
factory_bot adalah library untuk membuat objek test yang mudah dikustomisasi:
# Gemfile (group :test)
# gem 'factory_bot', '~> 6.3'
# gem 'faker', '~> 3.2'
# spec/factories/users.rb
require 'faker'
FactoryBot.define do
factory :user do
nama { Faker::Name.name }
email { Faker::Internet.email }
umur { rand(18..60) }
aktif { true }
role { :user }
# Trait — variasi yang bisa digunakan secara opsional
trait :admin do
role { :admin }
email { "admin-#{Faker::Internet.email}" }
end
trait :nonaktif do
aktif { false }
end
trait :dengan_profil do
after(:create) do |user|
create(:profil, user: user)
end
end
# Factory turunan
factory :admin_user, traits: [:admin]
factory :user_nonaktif, traits: [:nonaktif]
end
factory :produk do
nama { Faker::Commerce.product_name }
harga { rand(50_000..50_000_000) }
stok { rand(0..100) }
kategori
trait :habis do
stok { 0 }
end
trait :mahal do
harga { rand(10_000_000..100_000_000) }
end
end
end
# Penggunaan di spec
RSpec.describe User do
# build — buat objek tapi tidak save ke DB
let(:user) { build(:user) }
# create — buat dan save ke DB
let(:admin) { create(:admin_user) }
# build_stubbed — objek tiruan yang terlihat seperti sudah disimpan
let(:fake_user) { build_stubbed(:user) }
# attributes_for — hanya hash attribut, tanpa objek
let(:valid_params) { attributes_for(:user) }
# Kustomisasi inline
let(:user_khusus) { create(:user, nama: "Rina", email: "[email protected]") }
# Dengan trait
let(:user_nonaktif) { create(:user, :nonaktif) }
let(:admin_dengan_profil) { create(:admin_user, :dengan_profil) }
# Buat banyak sekaligus
let(:users) { create_list(:user, 5) }
let(:admins) { create_list(:user, 3, :admin) }
end
Code Coverage dengan SimpleCov #
# Gemfile
# gem 'simplecov', require: false
# spec/spec_helper.rb — HARUS di baris paling atas sebelum require lain
require 'simplecov'
SimpleCov.start do
add_filter '/spec/' # abaikan folder spec
add_filter '/config/' # abaikan config
add_group "Models", "app/models"
add_group "Controllers", "app/controllers"
add_group "Services", "app/services"
minimum_coverage 90 # gagal jika coverage < 90%
end
# Setelah menjalankan rspec, buka coverage/index.html
# Jalankan dengan coverage
bundle exec rspec
# Output contoh di terminal:
# Coverage report generated to coverage/index.html
# Coverage: 94.23% (523/555 lines)
TDD — Red, Green, Refactor #
TDD (Test-Driven Development) adalah metodologi di mana test ditulis sebelum kode produksi:
# Siklus TDD:
# 1. RED — Tulis test yang gagal (kode produksi belum ada)
# 2. GREEN — Tulis kode produksi minimal agar test lulus
# 3. REFACTOR — Perbaiki kode tanpa mengubah perilaku (test tetap lulus)
# === LANGKAH 1: RED — Tulis test ===
RSpec.describe LayananDiskon do
describe ".hitung" do
context "untuk member premium" do
it "memberikan diskon 20%" do
expect(LayananDiskon.hitung(100_000, :premium)).to eq(80_000)
end
end
context "untuk member biasa" do
it "memberikan diskon 10%" do
expect(LayananDiskon.hitung(100_000, :biasa)).to eq(90_000)
end
end
context "untuk tamu" do
it "tidak memberikan diskon" do
expect(LayananDiskon.hitung(100_000, :tamu)).to eq(100_000)
end
end
context "ketika harga negatif" do
it "melempar ArgumentError" do
expect { LayananDiskon.hitung(-1, :biasa) }.to raise_error(ArgumentError)
end
end
end
end
# === LANGKAH 2: GREEN — Implementasi minimal ===
class LayananDiskon
DISKON = {
premium: 0.20,
biasa: 0.10,
tamu: 0.00
}.freeze
def self.hitung(harga, tipe_member)
raise ArgumentError, "Harga tidak boleh negatif" if harga < 0
persen = DISKON.fetch(tipe_member, 0)
harga * (1 - persen)
end
end
# === LANGKAH 3: REFACTOR — Perbaiki tanpa ubah perilaku ===
# Misalnya: ekstrak ke method terpisah, tambahkan validasi tipe_member
class LayananDiskon
DISKON = {
premium: 0.20,
biasa: 0.10,
tamu: 0.00
}.freeze
def self.hitung(harga, tipe_member)
validasi!(harga, tipe_member)
harga * (1 - persen_diskon(tipe_member))
end
private_class_method def self.validasi!(harga, tipe_member)
raise ArgumentError, "Harga tidak boleh negatif" if harga < 0
raise ArgumentError, "Tipe member tidak valid: #{tipe_member}" unless DISKON.key?(tipe_member)
end
private_class_method def self.persen_diskon(tipe_member)
DISKON[tipe_member]
end
end
# Test tetap lulus setelah refactoring ✓
Menjalankan Test dengan Efisien #
# Jalankan semua spec
bundle exec rspec
# Jalankan file tertentu
bundle exec rspec spec/models/user_spec.rb
# Jalankan baris tertentu (satu example)
bundle exec rspec spec/models/user_spec.rb:42
# Jalankan dengan tag tertentu
bundle exec rspec --tag focus
bundle exec rspec --tag ~slow # kecuali yang ditag :slow
# Format output berbeda
bundle exec rspec --format documentation # deskriptif
bundle exec rspec --format progress # titik-titik (default)
bundle exec rspec --format json # untuk CI
# Jalankan hanya yang gagal terakhir kali
bundle exec rspec --only-failures
# Jalankan test secara paralel (perlu parallel_tests gem)
bundle exec parallel_rspec spec/
# Dengan Guard — jalankan otomatis saat file berubah
bundle exec guard
Anti-Pattern Testing yang Harus Dihindari #
# ANTI-PATTERN 1: Test yang terlalu banyak assertion
it "memvalidasi user" do
expect(user.valid?).to be true
expect(user.nama).to eq("Rina")
expect(user.email).to include("@")
expect(user.umur).to be >= 18
# 10 assertion lagi...
end
# BENAR: satu test, satu tujuan
it "valid dengan data yang benar" do
expect(user).to be_valid
end
it "memiliki nama yang ditetapkan" do
expect(user.nama).to eq("Rina")
end
# ANTI-PATTERN 2: Test yang bergantung pada state test lain
it "membuat user" do
User.create!(nama: "Rina") # state: 1 user di DB
end
it "menghitung total user" do
expect(User.count).to eq(1) # bergantung pada test di atas!
end
# BENAR: setiap test setup sendiri
it "menghitung total user" do
create(:user)
expect(User.count).to eq(1)
end
# ANTI-PATTERN 3: Test yang menguji implementasi, bukan perilaku
it "menggunakan algoritma merge sort" do
expect(sorter).to receive(:merge_sort) # terlalu detail
sorter.urutkan([3, 1, 2])
end
# BENAR: test perilaku (output), bukan implementasi (caranya)
it "mengurutkan array secara ascending" do
expect(sorter.urutkan([3, 1, 2])).to eq([1, 2, 3])
end
Ringkasan #
- MiniTest untuk proyek sederhana, RSpec untuk ekosistem Rails — MiniTest sudah bawaan dan cukup untuk banyak kasus; RSpec lebih ekspresif dan punya ekosistem yang lebih kaya.
letlebih baik dari variabel instance dibefore—letlazy dan hanya dibuat saat dibutuhkan; tidak ada overhead untuk test yang tidak membutuhkannya.- Satu test, satu tujuan — test dengan banyak assertion menggabungkan kekhawatiran yang berbeda; ketika gagal, sulit tahu mana yang salah.
- Nama test yang deskriptif —
it "mengembalikan nil jika pengguna tidak ditemukan"jauh lebih berguna dariit "works correctly".contextuntuk skenario berbeda — gunakancontext "ketika..."untuk mengelompokkan variasi kondisi; buat test yang mudah dibaca seperti cerita.- Factory bot menggantikan fixture — fixture statis sulit dipelihara; factory_bot dengan trait memberikan fleksibilitas penuh untuk membuat test data yang tepat.
- Test exception secara eksplisit —
expect { ... }.to raise_error(TipeError)lebih baik dari menangkap exception secara manual.changematcher untuk side effects —expect { ... }.to change(Model, :count).by(1)lebih jelas dariexpect(Model.count).to eq(before_count + 1).- Jangan test implementasi — test perilaku (apa yang dilakukan), bukan cara melakukannya; test implementasi pecah setiap kali refactoring.
- SimpleCov untuk mengukur coverage — coverage 100% bukan tujuan, tapi di bawah 80% biasanya menandakan ada area penting yang tidak ditest.