Alur kerja TDD

Siklus red-green-refactor yang dipraktikkan langsung pada sebuah fitur kecil.

Daftar isi

TDD (test-driven development) adalah alur kerja yang membalik urutan lazim, yaitu uji ditulis sebelum kode produksinya, lalu kode ditulis seminimal mungkin hingga ujinya lulus. Topik ini mempraktikkan satu siklus utuh red-green-refactor pada sebuah fitur kecil, mulai dari keluaran uji yang gagal, implementasi pertama yang membuat uji lulus, hingga perbaikan struktur yang tidak mengubah perilaku. Keluaran pada setiap langkah ditampilkan apa adanya sehingga perubahan dari keadaan merah menjadi hijau dapat dibandingkan langsung.

Uji yang ditulis lebih dulu#

Fitur yang dipraktikkan adalah metode fahrenheit_ke_celsius untuk kelas KonverterSuhu. Kelas tersebut sudah memiliki metode celsius_ke_fahrenheit dari iterasi sebelumnya, dan iterasi ini menambahkan konversi ke arah sebaliknya. Sesuai alur TDD, uji untuk fitur baru ditulis lebih dulu, lengkap dengan uji iterasi sebelumnya yang tetap dipertahankan.

ruby konverter_suhu_test.rb
# tidak-dijalankan: keluaran penuh uji ditampilkan pada blok bash di bawah dengan seed tetap
require "minitest/autorun"
require_relative "konverter_suhu"

class KonverterSuhuTest < Minitest::Test
  def test_celsius_ke_fahrenheit_mengubah_titik_didih
    konverter = KonverterSuhu.new
    assert_equal 212, konverter.celsius_ke_fahrenheit(100)
    assert_equal 32, konverter.celsius_ke_fahrenheit(0)
  end

  def test_fahrenheit_ke_celsius_mengubah_titik_didih
    konverter = KonverterSuhu.new
    assert_equal 100, konverter.fahrenheit_ke_celsius(212)
    assert_equal 0, konverter.fahrenheit_ke_celsius(32)
  end
end

Kedua metode uji menggunakan titik didih air sebagai kasus pemeriksaan, yaitu 100 derajat Celsius yang setara dengan 212 derajat Fahrenheit, karena angkanya pasti dan mudah dihitung ulang. Keadaan kode produksi pada awal siklus masih berisi iterasi sebelumnya saja, dan berkasnya dibuat ulang dengan perintah berikut agar keadaan awalnya pasti.

bash
# keadaan awal siklus: KonverterSuhu hanya memiliki metode dari iterasi sebelumnya
cat > konverter_suhu.rb <<'EOF'
class KonverterSuhu
  def celsius_ke_fahrenheit(celsius)
    celsius * 9 / 5 + 32
  end
end
EOF

Seluruh uji kemudian dijalankan. Uji untuk fitur baru dipastikan merah terlebih dahulu sebelum implementasinya ditulis.

bash
# langkah merah: uji dijalankan sebelum fitur fahrenheit_ke_celsius ada
ruby konverter_suhu_test.rb --seed 456 2>&1 | sed '/^Finished in/d'
text Keluaran
Run options: --seed 456

# Running:

.E


  1) Error:
KonverterSuhuTest#test_fahrenheit_ke_celsius_mengubah_titik_didih:
NoMethodError: undefined method 'fahrenheit_ke_celsius' for an instance of KonverterSuhu
    konverter_suhu_test.rb:14:in 'KonverterSuhuTest#test_fahrenheit_ke_celsius_mengubah_titik_didih'

2 runs, 2 assertions, 0 failures, 1 errors, 0 skips

Keluaran di atas adalah keadaan merah. Huruf E pada baris progres menyatakan error, dan bagian Error menunjukkan penyebabnya: metode fahrenheit_ke_celsius belum ada, sehingga pemanggilannya menimbulkan NoMethodError. Ringkasan mencatat dua uji berjalan dengan satu error, dan uji iterasi sebelumnya tetap hijau. Keadaan merah semacam ini bukan kemunduran, melainkan konfirmasi bahwa uji baru benar-benar menguji sesuatu yang belum ada. Uji yang langsung hijau tanpa pernah merah belum membuktikan apa pun, karena belum diketahui apakah assertion-nya benar-benar memeriksa perilaku.

Langkah hijau#

Langkah berikutnya adalah menulis kode paling sederhana yang membuat seluruh uji lulus. Kata “paling sederhana” diartikan secara harfiah: implementasi ditulis untuk kasus yang diuji, dan perilaku yang belum diminta uji tidak ditulis lebih dulu. Implementasi fahrenheit_ke_celsius pada langkah ini cukup satu baris, yaitu rumus konversinya.

bash
# langkah hijau: implementasi paling sederhana yang membuat seluruh uji lulus
cat > konverter_suhu.rb <<'EOF'
class KonverterSuhu
  def celsius_ke_fahrenheit(celsius)
    celsius * 9 / 5 + 32
  end

  def fahrenheit_ke_celsius(f)
    (f - 32) * 5 / 9
  end
end
EOF

Uji yang sama dijalankan ulang tanpa perubahan apa pun pada berkas ujinya.

bash
# langkah hijau: uji dijalankan ulang setelah implementasi ditulis
ruby konverter_suhu_test.rb --seed 456 2>&1 | sed '/^Finished in/d'
text Keluaran
Run options: --seed 456

# Running:

..

2 runs, 4 assertions, 0 failures, 0 errors, 0 skips

Keluaran berubah menjadi dua titik tanpa error, dan ringkasannya menyatakan bahwa keempat assertion seluruhnya lulus. Pada titik ini siklus sudah mencapai tujuan langkahnya, yaitu keadaan hijau. Rumus (f - 32) * 5 / 9 menghasilkan 100 untuk masukan 212 dan 0 untuk masukan 32, persis seperti yang diminta kedua assertion. Godaan untuk menulis kode yang lebih lengkap, misalnya pembulatan atau penanganan tipe pecahan, sengaja ditunda karena belum ada uji yang memintanya.

Langkah refactor#

Keadaan hijau bukan akhir siklus. Langkah ketiga memperbaiki struktur kode tanpa mengubah perilakunya, dan uji yang tetap hijau menjadi buktinya. Pada iterasi ini, perbaikan yang dilakukan adalah penamaan: parameter f diubah menjadi fahrenheit agar nama metode, parameter, dan variabel lokal di badan metode menggunakan istilah yang sama.

bash
# langkah refactor: nama parameter diperjelas tanpa mengubah perilaku
cat > konverter_suhu.rb <<'EOF'
class KonverterSuhu
  def celsius_ke_fahrenheit(celsius)
    celsius * 9 / 5 + 32
  end

  def fahrenheit_ke_celsius(fahrenheit)
    (fahrenheit - 32) * 5 / 9
  end
end
EOF

Uji dijalankan untuk ketiga kalinya, dan keluarannya harus identik dengan langkah hijau sebelumnya.

bash
# langkah refactor: uji dijalankan ulang untuk membuktikan perilaku tidak berubah
ruby konverter_suhu_test.rb --seed 456 2>&1 | sed '/^Finished in/d'
text Keluaran
Run options: --seed 456

# Running:

..

2 runs, 4 assertions, 0 failures, 0 errors, 0 skips

Keluarannya sama persis dengan langkah hijau, sehingga refactor terbukti aman. Refactor pada contoh ini sengaja dibuat kecil, tetapi polanya sama untuk perubahan yang lebih besar seperti memecah metode atau mengganti struktur data, yaitu ubah struktur, jalankan uji, dan pastikan keluarannya tidak berubah. Berkas utuh konverter_suhu.rb setelah satu siklus diperlihatkan berikut ini.

ruby konverter_suhu.rb
class KonverterSuhu
  def celsius_ke_fahrenheit(celsius)
    celsius * 9 / 5 + 32
  end

  def fahrenheit_ke_celsius(fahrenheit)
    (fahrenheit - 32) * 5 / 9
  end
end

Berkas tersebut tidak mencetak apa pun ketika dijalankan, karena seluruh perilakunya diperiksa oleh berkas uji. Siklus berikutnya, misalnya fitur konversi untuk satuan Kelvin, kembali dimulai dari langkah merah dengan menulis ujinya terlebih dahulu.

Kesalahan umum#

Kesalahan pertama adalah menulis implementasi sebelum ujinya, sehingga keadaan merah tidak pernah terlihat. Alur semacam ini kehilangan satu jaminan penting dari TDD, yaitu bukti bahwa assertion benar-benar dapat gagal. Perbaikannya adalah menahan diri untuk tidak mengetik kode produksi sebelum uji untuk fitur tersebut selesai dan terbukti merah.

Kesalahan kedua adalah menulis banyak uji sekaligus sebelum satu pun diimplementasikan. Keadaan merah yang menumpuk menyulitkan pemilihan langkah berikutnya, dan kegagalan yang muncul bersamaan saling menutupi penyebabnya. Disiplin yang sehat adalah satu metode uji untuk satu siklus, yaitu merah, hijau, lalu refactor, sebelum uji berikutnya ditulis.

Kesalahan ketiga adalah melunakkan assertion ketika uji sulit dibuat hijau, misalnya mengganti nilai yang diharapkan agar cocok dengan keluaran kode yang sudah ditulis. Langkah itu membalik arah alur kerja, karena uji justru menuruti kode yang sudah ada, padahal kode produksilah yang seharusnya menyesuaikan uji. Nilai yang diharapkan pada assertion ditetapkan dari spesifikasi perilaku, seperti titik didih air pada contoh di atas, dan bukan dari keluaran implementasi.

Ringkasan#

  • Siklus TDD bergerak dari uji yang gagal (merah), implementasi paling sederhana yang lulus (hijau), hingga perbaikan struktur tanpa perubahan perilaku (refactor).
  • Keadaan merah adalah konfirmasi bahwa uji baru benar-benar menguji fitur yang belum ada, dan uji lama dipertahankan berjalan pada setiap langkah.
  • Langkah hijau hanya menulis perilaku yang diminta uji, sedangkan langkah refactor dibuktikan dengan keluaran uji yang identik dengan langkah sebelumnya.
  • Setiap siklus memproses satu metode uji, sehingga kegagalan yang muncul selalu mudah ditelusuri.

Referensi#