TeachingSoftware EngineeringTest-Driven Development (TDD)
Workshop

Test-Driven Development (TDD)

๐Ÿงช Test-Driven Development (TDD)


๐Ÿ“‘ Table of Contents

Bagian 1: Pemahaman Konsep

  1. Apa itu Test-Driven Development?
  2. Siklus Red-Green-Refactor
  3. Mengapa TDD?
  4. Kapan Sebaiknya Pakai TDD?

Bagian 2: Tutorial

  1. Setup Environment
  2. Project: Grade Calculator
  3. Siklus TDD 1: Grade A
  4. Siklus TDD 2: Grade A-, B+, B, B-
  5. Siklus TDD 3: Grade C, D, E
  6. Siklus TDD 4: Refactoring
  7. Siklus TDD 5: Konversi ke Bobot GPA
  8. Siklus TDD 6: Edge Cases & Validasi
  9. Siklus TDD 7: Multi-Course GPA Calculator

Bagian 3: GitHub Integration

  1. Setup Repository di GitHub
  2. Workflow TDD dengan Git
  3. GitHub Actions untuk Automated Testing
  4. Code Coverage Badge

Bagian 4: Best Practices

  1. Anti-Patterns yang Harus Dihindari
  2. Tips untuk TDD yang Efektif
  3. Submission Checklist

Bagian 1: Pemahaman Konsep

1. Apa itu Test-Driven Development?

Test-Driven Development (TDD) adalah praktik pengembangan software di mana kamu menulis test SEBELUM menulis kode produksi.


2. Siklus Red-Green-Refactor

TDD mengikuti siklus sederhana yang diulang terus-menerus:

   ๐Ÿ”ด RED
      โ†“
   ๐ŸŸข GREEN
      โ†“
   ๐Ÿ”ต REFACTOR
      โ†“
   (ulang)

๐Ÿ”ด RED โ€” Tulis test yang FAIL

Tulis test untuk fitur yang belum ada. Jalankan test โ†’ harus gagal (karena kodenya belum ditulis).

Mengapa harus gagal dulu? Untuk membuktikan bahwa test-mu valid โ€” test yang langsung pass tanpa kode bisa jadi test yang salah.

๐ŸŸข GREEN โ€” Tulis kode minimal agar test PASS

Tulis kode sesederhana mungkin yang membuat test berhasil. Jangan over-engineer. Jangan tulis fitur yang belum diuji.

๐Ÿ’ก Prinsip kunci: "Make it work first, make it right later."

๐Ÿ”ต REFACTOR โ€” Perbaiki kode tanpa mengubah behavior

Setelah test pass, perbaiki kualitas kode: rename variable, ekstrak fungsi, hilangkan duplikasi. Test yang sudah ada menjadi safety net โ€” jika ada yang rusak, test akan langsung memberi tahu.


3. Mengapa TDD?

Manfaat utama TDD:

1. Confidence saat mengubah kode โ€” kamu berani refactor karena ada test yang akan memberi tahu kalau ada yang rusak.

2. Desain yang lebih baik โ€” menulis test memaksamu memikirkan bagaimana kode akan digunakan sebelum menulisnya, menghasilkan API yang lebih bersih.

3. Dokumentasi hidup โ€” test berfungsi sebagai contoh penggunaan yang selalu up-to-date.

4. Lebih sedikit bug di production โ€” bug terdeteksi saat masih sangat murah diperbaiki.

5. Fokus yang lebih tajam โ€” kamu mengerjakan satu hal kecil pada satu waktu, tidak terdistraksi.


4. Kapan Sebaiknya Pakai TDD?

Cocok untuk:

  • Kode dengan logika bisnis kompleks
  • Code yang akan dimaintain jangka panjang
  • Library/framework yang dipakai banyak orang
  • Refactoring kode legacy
  • Belajar bahasa/framework baru

Mungkin overkill untuk:

  • Prototype yang akan dibuang
  • Eksperimen/spike untuk validasi konsep
  • Throwaway scripts
  • UI yang sangat visual dan sering berubah

Bagian 2: Tutorial

5. Setup Environment

5.1 Install Python

Pastikan Python 3.8+ sudah terinstall:

python --version
# atau
python3 --version

Jika belum, download di python.org.

5.2 Install pytest

pip install pytest pytest-cov

pytest adalah testing framework Python yang paling populer. pytest-cov untuk mengukur code coverage.

5.3 Verifikasi Setup

pytest --version
# pytest 7.4.0 (atau versi lebih baru)

5.4 Setup Editor

Rekomendasi: VS Code dengan extension:

  • Python (Microsoft)
  • Python Test Explorer
  • Pylance

6. Project: Grade Calculator

Kita akan membangun Grade Calculator untuk konversi nilai numerik ke huruf grade dan bobot GPA, mengikuti matriks penilaian BINUS:

Matriks Penilaian

Nilai Rentang Bobot
A 90-100 4.0
A- 85-89 3.67
B+ 80-84 3.33
B 75-79 3.0
B- 70-74 2.5
C 65-69 2.0
D 50-64 1.0
E 0-49 0.0

Yang Akan Dibangun

Fungsi-fungsi berikut akan kita bangun dengan TDD:

  1. score_to_grade(score) โ€” konversi nilai ke huruf grade
  2. grade_to_gpa(grade) โ€” konversi huruf ke bobot GPA
  3. score_to_gpa(score) โ€” kombinasi: nilai langsung ke GPA
  4. calculate_semester_gpa(courses) โ€” hitung GPA semester dari multiple mata kuliah

6.1 Struktur Project

Buat folder project:

mkdir grade-calculator
cd grade-calculator

Struktur file yang akan kita buat:

grade-calculator/
โ”œโ”€โ”€ grade_calculator.py      # Kode produksi
โ”œโ”€โ”€ test_grade_calculator.py # Test cases
โ”œโ”€โ”€ README.md
โ””โ”€โ”€ .gitignore

Buat file kosong dulu:

touch grade_calculator.py test_grade_calculator.py README.md

7. Siklus TDD 1: Grade A

Sekarang masuk ke praktik TDD pertama. Kita mulai dengan kasus paling sederhana: nilai dalam range A (90-100).

๐Ÿ”ด RED โ€” Tulis Test Pertama

Buka test_grade_calculator.py dan tulis:

from grade_calculator import score_to_grade


def test_score_100_returns_A():
    assert score_to_grade(100) == "A"

Jalankan test:

pytest test_grade_calculator.py -v

Output (ERROR):

ImportError: cannot import name 'score_to_grade' from 'grade_calculator'

โœ… Bagus! Test FAIL โ€” karena fungsinya belum ada. Ini fase RED yang benar.

๐Ÿ’ก Insight: Error import adalah tanda yang valid bahwa kita ada di fase RED. Kita belum boleh menulis kode sampai kita yakin test akan fail.

๐ŸŸข GREEN โ€” Tulis Kode Minimal

Buka grade_calculator.py dan tulis kode paling minimal yang membuat test pass:

def score_to_grade(score):
    return "A"

"Hardcode return A"?! Ya, betul! Ini adalah prinsip TDD: tulis kode seminimal mungkin. Kalau test cuma minta nilai 100 = "A", kita kasih "A" saja. Nanti test berikutnya akan memaksa kita generalisasi.

Jalankan test:

pytest test_grade_calculator.py -v

Output:

test_grade_calculator.py::test_score_100_returns_A PASSED โœ…

๐ŸŽ‰ GREEN tercapai!

๐Ÿ’ก Prinsip TDD: "Fake it till you make it." Mulai dari hardcode, biarkan test mendorong kita ke generalisasi.

โž• Tambah Test untuk Range A

Sekarang kita pastikan semua nilai di range A (90-100) menghasilkan "A":

def test_score_90_returns_A():
    assert score_to_grade(90) == "A"

def test_score_95_returns_A():
    assert score_to_grade(95) == "A"

Jalankan test:

pytest test_grade_calculator.py -v

Semua PASS karena kode kita masih hardcode "A". โœ…

๐Ÿ”ต REFACTOR โ€” Belum Perlu

Kode masih sangat sederhana, tidak ada yang perlu di-refactor. Lanjut ke siklus berikutnya.


8. Siklus TDD 2: Grade A-, B+, B, B-

Sekarang kita perkenalkan grade selain "A".

๐Ÿ”ด RED โ€” Test untuk A-

def test_score_89_returns_A_minus():
    assert score_to_grade(89) == "A-"

Jalankan test:

pytest test_grade_calculator.py -v

Output (FAIL):

AssertionError: assert 'A' == 'A-'

Test gagal karena kode kita masih hardcode "A". โœ… RED tercapai.

๐ŸŸข GREEN โ€” Generalisasi

Sekarang kita tambah logika untuk A-:

def score_to_grade(score):
    if score >= 90:
        return "A"
    if score >= 85:
        return "A-"
    return "A"  # default, akan kita perbaiki nanti

Test pass. โœ…

โž• Lanjut: B+, B, B-

Tambah test bertahap:

def test_score_84_returns_B_plus():
    assert score_to_grade(84) == "B+"

def test_score_79_returns_B():
    assert score_to_grade(79) == "B"

def test_score_74_returns_B_minus():
    assert score_to_grade(74) == "B-"

Implementasi:

def score_to_grade(score):
    if score >= 90:
        return "A"
    if score >= 85:
        return "A-"
    if score >= 80:
        return "B+"
    if score >= 75:
        return "B"
    if score >= 70:
        return "B-"
    return "B-"  # default, akan diperbaiki

Semua test pass. โœ…

๐Ÿ’ก Catatan: Perhatikan urutan kondisi penting! Cek dari range tertinggi ke terendah. Kalau dibalik, semua nilai akan masuk kondisi pertama yang terpenuhi.


9. Siklus TDD 3: Grade C, D, E

๐Ÿ”ด RED โ€” Tambah Test

def test_score_69_returns_C():
    assert score_to_grade(69) == "C"

def test_score_64_returns_D():
    assert score_to_grade(64) == "D"

def test_score_49_returns_E():
    assert score_to_grade(49) == "E"

def test_score_0_returns_E():
    assert score_to_grade(0) == "E"

Jalankan test โ†’ FAIL.

๐ŸŸข GREEN โ€” Lengkapi Implementasi

def score_to_grade(score):
    if score >= 90:
        return "A"
    if score >= 85:
        return "A-"
    if score >= 80:
        return "B+"
    if score >= 75:
        return "B"
    if score >= 70:
        return "B-"
    if score >= 65:
        return "C"
    if score >= 50:
        return "D"
    return "E"

Jalankan semua test:

pytest test_grade_calculator.py -v

Semua PASS. โœ…

โœ… Tambah Test Boundary

Ini penting! Mari kita test nilai-nilai tepat di batas (boundary) setiap range, mengaplikasikan Boundary Value Analysis dari materi sebelumnya:

def test_boundary_85_returns_A_minus():
    assert score_to_grade(85) == "A-"

def test_boundary_80_returns_B_plus():
    assert score_to_grade(80) == "B+"

def test_boundary_75_returns_B():
    assert score_to_grade(75) == "B"

def test_boundary_70_returns_B_minus():
    assert score_to_grade(70) == "B-"

def test_boundary_65_returns_C():
    assert score_to_grade(65) == "C"

def test_boundary_50_returns_D():
    assert score_to_grade(50) == "D"

Semua harus PASS. Kalau ada yang FAIL, ada bug logic di kode kita yang perlu diperbaiki.

๐Ÿ’ก Lesson: Boundary value testing adalah teknik paling efektif untuk menemukan bug operator (>= vs >, atau <= vs <).


10. Siklus TDD 4: Refactoring

Sekarang kode kita berfungsi, tapi ada code smell: rantai if-statement yang panjang. Mari kita refactor!

๐Ÿ”ต REFACTOR โ€” Pakai Lookup Table

GRADE_THRESHOLDS = [
    (90, "A"),
    (85, "A-"),
    (80, "B+"),
    (75, "B"),
    (70, "B-"),
    (65, "C"),
    (50, "D"),
    (0,  "E"),
]


def score_to_grade(score):
    for threshold, grade in GRADE_THRESHOLDS:
        if score >= threshold:
            return grade
    return "E"  # safeguard, secara teori tidak akan tercapai

Jalankan semua test:

pytest test_grade_calculator.py -v

Semua masih PASS. โœ…

Inilah kekuatan TDD โ€” kita bisa refactor dengan percaya diri karena test akan langsung memberi tahu kalau ada yang rusak.

๐Ÿ’ก Tip: Refactor selalu dilakukan setelah semua test pass, dan setiap perubahan dijalankan ulang testnya. Jangan refactor sambil menulis fitur baru!


11. Siklus TDD 5: Konversi ke Bobot GPA

Sekarang tambah fungsi kedua: grade_to_gpa(grade) yang konversi huruf grade ke bobot GPA.

๐Ÿ”ด RED โ€” Test Pertama

from grade_calculator import score_to_grade, grade_to_gpa


def test_grade_A_returns_4():
    assert grade_to_gpa("A") == 4.0

Jalankan test:

pytest test_grade_calculator.py -v

Output (FAIL):

ImportError: cannot import name 'grade_to_gpa' from 'grade_calculator'

โœ… RED.

๐ŸŸข GREEN โ€” Implementasi Minimal

def grade_to_gpa(grade):
    return 4.0

Test pass. โœ…

โž• Tambah Test untuk Grade Lain

def test_grade_A_minus_returns_3_67():
    assert grade_to_gpa("A-") == 3.67

def test_grade_B_plus_returns_3_33():
    assert grade_to_gpa("B+") == 3.33

def test_grade_B_returns_3():
    assert grade_to_gpa("B") == 3.0

def test_grade_B_minus_returns_2_5():
    assert grade_to_gpa("B-") == 2.5

def test_grade_C_returns_2():
    assert grade_to_gpa("C") == 2.0

def test_grade_D_returns_1():
    assert grade_to_gpa("D") == 1.0

def test_grade_E_returns_0():
    assert grade_to_gpa("E") == 0.0

๐ŸŸข GREEN โ€” Implementasi Lengkap

GPA_WEIGHTS = {
    "A":  4.0,
    "A-": 3.67,
    "B+": 3.33,
    "B":  3.0,
    "B-": 2.5,
    "C":  2.0,
    "D":  1.0,
    "E":  0.0,
}


def grade_to_gpa(grade):
    return GPA_WEIGHTS[grade]

Semua test PASS. โœ…

โž• Combine: score_to_gpa

Sekarang gabungkan kedua fungsi:

def test_score_95_returns_gpa_4():
    assert score_to_gpa(95) == 4.0

def test_score_80_returns_gpa_3_33():
    assert score_to_gpa(80) == 3.33

def test_score_50_returns_gpa_1():
    assert score_to_gpa(50) == 1.0

Import dan implementasi:

def score_to_gpa(score):
    grade = score_to_grade(score)
    return grade_to_gpa(grade)

Lihat betapa sederhananya fungsi ini โ€” karena kita sudah punya building blocks-nya. Inilah kekuatan TDD dan komposisi fungsi.

Update import di test file:

from grade_calculator import score_to_grade, grade_to_gpa, score_to_gpa

Semua test PASS. โœ…


12. Siklus TDD 6: Edge Cases & Validasi

Sekarang kita perkuat fungsi dengan menangani input yang tidak valid. Ini adalah error handling yang sering dilupakan.

๐Ÿ”ด RED โ€” Test untuk Input Negatif

import pytest


def test_negative_score_raises_error():
    with pytest.raises(ValueError):
        score_to_grade(-1)


def test_score_above_100_raises_error():
    with pytest.raises(ValueError):
        score_to_grade(101)

Jalankan test:

Output (FAIL):

Failed: DID NOT RAISE <class 'ValueError'>

โœ… RED โ€” fungsi kita belum melempar error.

๐ŸŸข GREEN โ€” Tambah Validasi

def score_to_grade(score):
    if score < 0 or score > 100:
        raise ValueError(f"Score must be between 0 and 100, got {score}")

    for threshold, grade in GRADE_THRESHOLDS:
        if score >= threshold:
            return grade
    return "E"

Test PASS. โœ…

โž• Test untuk Tipe Data Tidak Valid

def test_non_numeric_score_raises_error():
    with pytest.raises(TypeError):
        score_to_grade("seratus")


def test_none_score_raises_error():
    with pytest.raises(TypeError):
        score_to_grade(None)

๐ŸŸข GREEN โ€” Tambah Type Check

def score_to_grade(score):
    if not isinstance(score, (int, float)):
        raise TypeError(f"Score must be a number, got {type(score).__name__}")
    if score < 0 or score > 100:
        raise ValueError(f"Score must be between 0 and 100, got {score}")

    for threshold, grade in GRADE_THRESHOLDS:
        if score >= threshold:
            return grade
    return "E"

โž• Test untuk Invalid Grade

def test_invalid_grade_raises_error():
    with pytest.raises(KeyError):
        grade_to_gpa("Z")


def test_grade_to_gpa_with_lowercase_should_handle():
    # Decision: kita ingin case-insensitive?
    # Mari kita putuskan: ya, sistem harus accept "a" dan "A"
    assert grade_to_gpa("a") == 4.0

๐ŸŸข GREEN โ€” Implementasi Case-Insensitive

def grade_to_gpa(grade):
    if not isinstance(grade, str):
        raise TypeError(f"Grade must be a string, got {type(grade).__name__}")
    normalized = grade.upper()
    if normalized not in GPA_WEIGHTS:
        raise KeyError(f"Invalid grade: {grade}")
    return GPA_WEIGHTS[normalized]

๐Ÿ’ก Lesson: TDD bagus untuk memikirkan edge case karena kita dipaksa menulis test sebelum implementasi. Banyak bug dihindari di tahap ini!


13. Siklus TDD 7: Multi-Course GPA Calculator

Fitur final โ€” hitung GPA semester dari beberapa mata kuliah dengan SKS yang berbeda-beda.

๐ŸŽฏ Spesifikasi

Fungsi calculate_semester_gpa(courses) menerima list of tuples (score, credits) dan mengembalikan GPA tertimbang:

GPA = ฮฃ(bobot ร— SKS) / ฮฃ(SKS)

๐Ÿ”ด RED โ€” Test Pertama (Satu Mata Kuliah)

from grade_calculator import (
    score_to_grade,
    grade_to_gpa,
    score_to_gpa,
    calculate_semester_gpa,
)


def test_single_course_returns_its_gpa():
    courses = [(95, 3)]  # Nilai 95, 3 SKS
    assert calculate_semester_gpa(courses) == 4.0

๐ŸŸข GREEN โ€” Implementasi Minimal

def calculate_semester_gpa(courses):
    score, credits = courses[0]
    return score_to_gpa(score)

Test PASS. โœ…

โž• Test untuk Multiple Courses

def test_multiple_courses_with_equal_credits():
    courses = [
        (95, 3),  # A, GPA 4.0
        (80, 3),  # B+, GPA 3.33
    ]
    # Average: (4.0 + 3.33) / 2 = 3.665
    expected = (4.0 + 3.33) / 2
    assert calculate_semester_gpa(courses) == pytest.approx(expected)

๐Ÿ’ก Tip: Gunakan pytest.approx() untuk membandingkan floating-point numbers karena ketidakakuratan komputasi desimal.

Output (FAIL):

AssertionError: assert 4.0 == 3.665

โœ… RED.

๐ŸŸข GREEN โ€” Generalisasi

def calculate_semester_gpa(courses):
    total_weighted = 0
    total_credits = 0
    for score, credits in courses:
        total_weighted += score_to_gpa(score) * credits
        total_credits += credits
    return total_weighted / total_credits

Test PASS. โœ…

โž• Test untuk SKS Berbeda

def test_weighted_gpa_with_different_credits():
    courses = [
        (95, 4),  # A, 4 SKS โ†’ 4.0 ร— 4 = 16.0
        (70, 2),  # B-, 2 SKS โ†’ 2.5 ร— 2 = 5.0
        (80, 3),  # B+, 3 SKS โ†’ 3.33 ร— 3 = 9.99
    ]
    # Total weighted: 16.0 + 5.0 + 9.99 = 30.99
    # Total credits: 9
    # GPA: 30.99 / 9 โ‰ˆ 3.443
    expected = (4.0 * 4 + 2.5 * 2 + 3.33 * 3) / 9
    assert calculate_semester_gpa(courses) == pytest.approx(expected)

Test PASS karena implementasi kita sudah benar. โœ…

โž• Edge Cases

def test_empty_courses_raises_error():
    with pytest.raises(ValueError):
        calculate_semester_gpa([])


def test_zero_credits_raises_error():
    with pytest.raises(ValueError):
        calculate_semester_gpa([(85, 0)])

๐ŸŸข GREEN โ€” Tambah Validasi

def calculate_semester_gpa(courses):
    if not courses:
        raise ValueError("Course list cannot be empty")

    total_weighted = 0
    total_credits = 0
    for score, credits in courses:
        if credits <= 0:
            raise ValueError(f"Credits must be positive, got {credits}")
        total_weighted += score_to_gpa(score) * credits
        total_credits += credits
    return total_weighted / total_credits

Semua test PASS. โœ…

๐ŸŽ‰ Hasil Akhir

File grade_calculator.py:

GRADE_THRESHOLDS = [
    (90, "A"),
    (85, "A-"),
    (80, "B+"),
    (75, "B"),
    (70, "B-"),
    (65, "C"),
    (50, "D"),
    (0,  "E"),
]

GPA_WEIGHTS = {
    "A":  4.0,
    "A-": 3.67,
    "B+": 3.33,
    "B":  3.0,
    "B-": 2.5,
    "C":  2.0,
    "D":  1.0,
    "E":  0.0,
}


def score_to_grade(score):
    if not isinstance(score, (int, float)):
        raise TypeError(f"Score must be a number, got {type(score).__name__}")
    if score < 0 or score > 100:
        raise ValueError(f"Score must be between 0 and 100, got {score}")

    for threshold, grade in GRADE_THRESHOLDS:
        if score >= threshold:
            return grade
    return "E"


def grade_to_gpa(grade):
    if not isinstance(grade, str):
        raise TypeError(f"Grade must be a string, got {type(grade).__name__}")
    normalized = grade.upper()
    if normalized not in GPA_WEIGHTS:
        raise KeyError(f"Invalid grade: {grade}")
    return GPA_WEIGHTS[normalized]


def score_to_gpa(score):
    grade = score_to_grade(score)
    return grade_to_gpa(grade)


def calculate_semester_gpa(courses):
    if not courses:
        raise ValueError("Course list cannot be empty")

    total_weighted = 0
    total_credits = 0
    for score, credits in courses:
        if credits <= 0:
            raise ValueError(f"Credits must be positive, got {credits}")
        total_weighted += score_to_gpa(score) * credits
        total_credits += credits
    return total_weighted / total_credits

๐Ÿ“Š Check Code Coverage

pytest --cov=grade_calculator --cov-report=term-missing

Output:

Name                   Stmts   Miss  Cover   Missing
----------------------------------------------------
grade_calculator.py       28      0   100%
----------------------------------------------------
TOTAL                     28      0   100%

๐ŸŽ‰ 100% coverage โ€” semua baris kode kita sudah tertutup oleh test!


Bagian 3: GitHub Integration

14. Setup Repository di GitHub

14.1 Buat Repository

  1. Login ke github.com
  2. Klik + โ†’ New repository
  3. Repository name: grade-calculator-tdd
  4. Description: "TDD practice with Grade Calculator"
  5. Pilih Public
  6. Centang Add a README file
  7. Add .gitignore: pilih Python
  8. Klik Create repository

14.2 Clone Repository

git clone https://github.com/USERNAME/grade-calculator-tdd.git
cd grade-calculator-tdd

14.3 Setup README

Edit README.md:

# Grade Calculator (TDD Practice)

Latihan Test-Driven Development dengan Python.

## Setup
\`\`\`bash
pip install pytest pytest-cov
\`\`\`

## Run Tests
\`\`\`bash
pytest -v
\`\`\`

## Run Coverage
\`\`\`bash
pytest --cov=grade_calculator --cov-report=term-missing
\`\`\`

14.4 Copy Files

Pindahkan grade_calculator.py dan test_grade_calculator.py ke folder repository.


15. Workflow TDD dengan Git

15.1 Filosofi: Commit per Siklus TDD

Best practice: setiap siklus TDD (Red-Green) = satu commit. Ini membuat history-mu menjadi "story" pengembangan.

15.2 Workflow Detail

Untuk setiap fitur baru:

# 1. Buat branch baru
git checkout -b feature/score-to-grade-A

# 2. RED phase
# Tulis test yang fail
git add test_grade_calculator.py
git commit -m "test: add test for score 100 returns A (RED)"

# 3. GREEN phase
# Tulis kode minimal
git add grade_calculator.py
git commit -m "feat: implement score_to_grade for A (GREEN)"

# 4. REFACTOR (jika perlu)
git add grade_calculator.py
git commit -m "refactor: extract grade thresholds to constant"

# 5. Push ke GitHub
git push origin feature/score-to-grade-A

# 6. Create Pull Request di GitHub
# 7. Merge setelah review

15.3 Conventional Commits

Format commit message yang direkomendasikan:

  • test: untuk menambah/mengubah test
  • feat: untuk fitur baru
  • fix: untuk bug fix
  • refactor: untuk refactoring
  • docs: untuk dokumentasi
  • chore: untuk maintenance

Contoh:

test: add boundary tests for grade thresholds
feat: implement grade_to_gpa conversion
refactor: replace if-chain with lookup table
fix: handle negative score with proper error message

15.4 Pull Request Workflow

Saat membuat PR di GitHub:

Judul PR:

feat: implement grade_to_gpa function

Body PR:

## Apa yang ditambahkan?
- Fungsi `grade_to_gpa(grade)` untuk konversi grade ke bobot GPA
- Support untuk semua grade (A, A-, B+, B, B-, C, D, E)
- Case-insensitive (menerima "a" dan "A")
- Error handling untuk grade tidak valid

## Test Coverage
- 8 test cases ditambahkan
- Coverage tetap 100%

## TDD Cycles
1. RED-GREEN untuk grade A
2. RED-GREEN untuk grade A-
3. ... (semua grade)
4. RED-GREEN untuk case-insensitivity
5. REFACTOR: extract GPA_WEIGHTS dictionary

## Screenshot
![pytest output](url-to-screenshot)

16. GitHub Actions untuk Automated Testing

GitHub Actions akan otomatis menjalankan test setiap kali ada push atau PR.

16.1 Buat Workflow File

Buat folder .github/workflows/ dan file tests.yml:

mkdir -p .github/workflows
touch .github/workflows/tests.yml

16.2 Workflow Configuration

Isi tests.yml:

name: Run Tests

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        python-version: ["3.9", "3.10", "3.11", "3.12"]

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install pytest pytest-cov

      - name: Run tests with coverage
        run: |
          pytest --cov=grade_calculator --cov-report=term-missing --cov-report=xml

      - name: Upload coverage report
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report-${{ matrix.python-version }}
          path: coverage.xml

16.3 Penjelasan Workflow

Apa yang terjadi:

  1. Setiap push ke main atau setiap PR โ†’ workflow trigger
  2. Workflow run di Ubuntu dengan 4 versi Python (matrix testing)
  3. Install dependencies โ†’ run pytest dengan coverage
  4. Upload coverage report sebagai artifact

Hasilnya: PR akan menampilkan โœ… atau โŒ berdasarkan hasil test. Tidak ada lagi "tapi di laptop saya jalan" โ€” semua harus pass di environment standar!

16.4 Commit dan Push

git add .github/workflows/tests.yml
git commit -m "ci: add GitHub Actions for automated testing"
git push origin main

Buka tab Actions di GitHub repository untuk lihat workflow berjalan!


17. Code Coverage Badge

Tambahkan badge yang menunjukkan coverage di README.

17.1 Setup Codecov (Gratis)

  1. Buka codecov.io
  2. Sign in dengan GitHub
  3. Authorize Codecov
  4. Pilih repository grade-calculator-tdd

17.2 Update Workflow

Tambahkan step upload ke Codecov di tests.yml:

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v4
        with:
          file: ./coverage.xml
          fail_ci_if_error: true

17.3 Tambah Badge di README

Update README.md:

# Grade Calculator (TDD Practice)

![Tests](https://github.com/USERNAME/grade-calculator-tdd/workflows/Run%20Tests/badge.svg)
[![codecov](https://codecov.io/gh/USERNAME/grade-calculator-tdd/branch/main/graph/badge.svg)](https://codecov.io/gh/USERNAME/grade-calculator-tdd)

Latihan Test-Driven Development dengan Python.

Ganti USERNAME dengan GitHub username-mu.

Hasil akhirnya, README-mu akan menampilkan badge cantik yang menunjukkan status test pass dan persentase coverage. Profesional!


Bagian 4: Best Practices

18. Anti-Patterns yang Harus Dihindari

โŒ Anti-Pattern 1: Skip RED Phase

Salah:

# Langsung tulis kode
def score_to_grade(score):
    # ... implementasi lengkap

# Baru tulis test yang sudah pasti pass
def test_works():
    assert score_to_grade(95) == "A"

Mengapa salah? Test yang langsung pass tanpa fase RED bisa jadi test yang salah. Kita tidak punya bukti bahwa test-nya valid.

โŒ Anti-Pattern 2: Over-Engineering di Fase GREEN

Salah:

# Test: assert score_to_grade(100) == "A"

# Kode yang ditulis (terlalu jauh dari requirement):
def score_to_grade(score):
    # Implementasi lengkap untuk SEMUA grade
    if score >= 90: return "A"
    if score >= 85: return "A-"
    # ... dst

Yang benar:

def score_to_grade(score):
    return "A"  # cukup ini dulu!

Biarkan test berikutnya mendorong generalisasi.

โŒ Anti-Pattern 3: Test yang Bergantung pada Test Lain

Salah:

def test_1():
    global result
    result = score_to_grade(95)

def test_2():
    assert result == "A"  # bergantung pada test_1!

Test harus independent โ€” bisa dijalankan dalam urutan apa pun.

โŒ Anti-Pattern 4: Mengubah Test untuk Pass

Salah:

# Test gagal:
def test_score_90_returns_A():
    assert score_to_grade(90) == "A"

# AssertionError: 'A-' != 'A'

# Solusi yang SALAH:
def test_score_90_returns_A():
    assert score_to_grade(90) == "A-"  # ubah test agar pass

Test gagal artinya kode yang harus diperbaiki, bukan test. Test adalah spesifikasi.

โŒ Anti-Pattern 5: Commit dengan Test Fail

Jangan pernah commit kode dengan test yang fail (kecuali sengaja untuk fase RED dan langsung dilanjutkan ke GREEN).

# Selalu cek sebelum commit:
pytest
git add .
git commit -m "..."

19. Tips untuk TDD yang Efektif

โœ… Tip 1: Mulai dari Test Paling Sederhana

Selalu mulai dari kasus paling mudah. Untuk grade calculator, mulai dari score = 100 โ†’ "A", bukan dari edge case yang kompleks.

โœ… Tip 2: Satu Test pada Satu Waktu

Jangan tulis 10 test sekaligus. Tulis 1, jalankan (RED), implement (GREEN), refactor jika perlu, baru lanjut test berikutnya.

โœ… Tip 3: Test Harus Cepat

Jika test memerlukan database, file system, atau network, pertimbangkan untuk menggunakan mock. Test yang lambat akan membuat developer malas menjalankannya.

โœ… Tip 4: Test Names Harus Deskriptif

Buruk: def test_1(), def test_grade() Bagus: def test_score_90_returns_grade_A()

โœ… Tip 5: Gunakan AAA Pattern

Struktur test yang baik:

def test_calculate_semester_gpa():
    # Arrange โ€” siapkan data
    courses = [(95, 3), (80, 3)]

    # Act โ€” jalankan fungsi
    result = calculate_semester_gpa(courses)

    # Assert โ€” verifikasi hasil
    expected = (4.0 + 3.33) / 2
    assert result == pytest.approx(expected)

โœ… Tip 6: Refactor Test Juga!

Test code adalah code juga. Kalau ada duplikasi atau pattern yang berulang, refactor!

# Sebelum (banyak duplikasi):
def test_score_100_returns_A():
    assert score_to_grade(100) == "A"

def test_score_95_returns_A():
    assert score_to_grade(95) == "A"

def test_score_90_returns_A():
    assert score_to_grade(90) == "A"

# Sesudah (parametrize):
import pytest

@pytest.mark.parametrize("score,expected_grade", [
    (100, "A"),
    (95, "A"),
    (90, "A"),
    (89, "A-"),
    (85, "A-"),
    (80, "B+"),
])
def test_score_to_grade(score, expected_grade):
    assert score_to_grade(score) == expected_grade

Jauh lebih bersih!

โœ… Tip 7: Coverage Bukan Tujuan, Tapi Indikator

100% coverage bukan jaminan bebas bug. Tapi coverage rendah (< 70%) hampir pasti tanda kode tidak teruji dengan baik. Fokus pada test yang bermakna.


๐Ÿ“‹ Submission Checklist

Sebelum submit project, pastikan:

๐Ÿงช Testing

  • Semua test pass (pytest โ†’ 0 failed)
  • Code coverage minimal 90% (pytest --cov)
  • Test mencakup happy path dan edge cases
  • Tidak ada test yang di-skip atau di-comment

๐Ÿ“ Code Quality

  • Kode bersih, tidak ada commented-out code
  • Penamaan jelas (fungsi, variabel, test)
  • Tidak ada hardcoded magic number tanpa penjelasan
  • Error handling untuk input tidak valid

๐Ÿ™ Git & GitHub

  • Commit history menunjukkan TDD workflow (red-green-refactor)
  • Commit messages mengikuti convention
  • Setidaknya 1 Pull Request dibuat dan di-merge
  • GitHub Actions workflow setup dan running
  • Test pass di CI (badge hijau)

๐Ÿ“„ Dokumentasi

  • README lengkap dengan setup instruction
  • Badge tests dan coverage tampil di README
  • Penjelasan fungsi-fungsi utama

๐ŸŽ“ Penutup

Selamat! Kamu telah membangun aplikasi Grade Calculator lengkap dengan TDD, dari konsep hingga deployment dengan CI/CD.

Yang Sudah Kamu Pelajari

โœ… Filosofi dan siklus TDD (Red-Green-Refactor) โœ… Menulis test sebelum kode produksi โœ… Membangun fungsi bertahap dengan TDD โœ… Refactoring dengan percaya diri โœ… Error handling dan edge cases โœ… Git workflow yang baik untuk TDD โœ… GitHub Actions untuk automated testing โœ… Code coverage dan badge

๐Ÿ’ก Pesan Akhir: TDD bukan tentang menulis lebih banyak test โ€” ini tentang mengubah cara berpikir tentang software development. Dengan TDD, kamu tidak hanya menulis kode yang berfungsi, tapi kode yang bisa dipercaya, bisa diubah dengan aman, dan terdokumentasi dengan baik secara otomatis.


Software Engineering โ€” D7123