Unit Test

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:#6bcb77

Unit 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.
  • let lebih baik dari variabel instance di beforelet lazy 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 deskriptifit "mengembalikan nil jika pengguna tidak ditemukan" jauh lebih berguna dari it "works correctly".
  • context untuk skenario berbeda — gunakan context "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 eksplisitexpect { ... }.to raise_error(TipeError) lebih baik dari menangkap exception secara manual.
  • change matcher untuk side effectsexpect { ... }.to change(Model, :count).by(1) lebih jelas dari expect(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.

← Sebelumnya: Web Server   Berikutnya: Mocking →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact