Unit Testing
Pengenalan
Unit Testing adalah salah satu teknik pengujian perangkat lunak yang paling fundamental dalam Software Development Lifecycle (SDLC). Dengan melakukan unit testing secara konsisten, developer dapat menangkap bug lebih awal dan meningkatkan kualitas kode secara keseluruhan.
Definisi Unit Testing
Apa itu Unit Testing?
Unit Testing adalah teknik pengujian perangkat lunak di mana unit-unit individual dari kode (biasanya fungsi, metode, atau class) diuji secara terpisah dan terisolasi untuk memastikan bahwa setiap unit berfungsi dengan benar sesuai spesifikasinya.
Definisi Formal:
1
2
3
Unit Testing adalah jenis pengujian teknis yang dilakukan oleh developer
untuk memverifikasi bahwa komponen atau unit kode terkecil (unit)
berfungsi dengan benar sesuai dengan desain dan spesifikasi.
Karakteristik Unit Testing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
✅ Single Responsibility
└─ Setiap test menguji satu fungsi/metode saja
✅ Isolated / Autonomous
└─ Test berdiri sendiri, tidak tergantung test lain
✅ Fast Execution
└─ Test harus cepat, biasanya < 1 detik per test
✅ Repeatable
└─ Test dapat dijalankan berkali-kali dengan hasil sama
✅ Deterministic
└─ Test selalu pass atau selalu fail, tidak random
✅ No External Dependencies
└─ Tidak bergantung pada database, API, file system, dll
✅ Clear & Readable
└─ Jelas apa yang sedang ditest dan expected result-nya
✅ Maintainable
└─ Mudah diupdate dan diperbaiki ketika kode berubah
Scope Unit Testing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
UNIT TESTING SCOPE (Testing Pyramid - Bottom Layer)
═════════════════════════════════════════════════════════════
/ \
/E2E\ E2E Testing (5%)
/─────\ ──────────────────
/ \
/ UI \ Integration Testing (15%)
/───────────\ ──────────────────
/ \
/ Unit \ Unit Testing (70-80%)
/─────────────────\ ──────────────────
╰───────────────────╯
UNIT TESTING FOKUS:
├─ Individual functions/methods
├─ Class methods
├─ Utility functions
├─ Business logic
└─ Edge cases & error handling
TIDAK termasuk:
├─ UI interactions
├─ Database operations (kecuali mocked)
├─ External API calls
├─ Multiple component interactions
└─ End-to-end workflows
Keunggulan Unit Testing
Mengapa Unit Testing itu Penting?
1. 🐛 Early Bug Detection
1
2
3
4
5
6
7
8
9
10
11
12
Benefit: Menangkap bug sedini mungkin
────────────────────────────────────────
❌ TANPA Unit Testing:
Developer A → Commit Code
Developer B → Integration Testing (1 minggu)
Bug ditemukan → Mahal untuk fix
✅ DENGAN Unit Testing:
Developer A → Unit Test → Commit Code
Bug ditemukan → Langsung fix
Lebih hemat waktu & biaya
2. 💪 Improved Code Quality
1
2
3
4
5
6
Unit Testing mendorong developer untuk:
✓ Menulis kode yang lebih modular
✓ Mengurangi coupling antar komponen
✓ Meningkatkan cohesion dalam fungsi
✓ Refactor dengan percaya diri
✓ Improve design & architecture
3. 🔄 Regression Prevention
1
2
3
4
5
Saat melakukan changes/refactoring:
✓ Unit tests memastikan tidak ada break existing functionality
✓ Confidence untuk melakukan optimization
✓ Catch unintended side effects
✓ Enable continuous deployment
4. 📚 Living Documentation
1
2
3
4
5
Unit tests berfungsi sebagai:
✓ Documentation cara menggunakan function
✓ Contoh konkret expected behavior
✓ Clear contract antara caller & callee
✓ Easier onboarding untuk developer baru
5. 🚀 Faster Development
1
2
3
4
5
6
7
8
Paradoks: Lebih banyak code (tests), tapi lebih cepat development
Alasan:
✓ Tidak perlu manual testing berkali-kali
✓ Automated testing lebih cepat
✓ Debugging lebih mudah dengan clear test cases
✓ Confidence untuk merilis fitur baru
✓ Less time spent on bug fixing
6. 💰 Lower Maintenance Cost
1
2
3
4
5
6
7
8
Cost of fixing bugs:
Tahap Development: $1 per bug
Tahap Integration: $10 per bug
Tahap Testing: $100 per bug
Tahap Production: $1,000 per bug
Unit Testing → Catch di Development Stage = HEMAT!
7. ✅ Confidence & Peace of Mind
1
2
3
4
5
Developer merasa percaya diri:
✓ Yakin bahwa code bekerja sesuai spec
✓ Tidak takut melakukan perubahan
✓ Merasa proud dengan kualitas code
✓ Sleep better at night 😴
Comparison: With vs Without Unit Testing
1
2
3
4
5
6
7
8
9
10
11
12
13
┌─────────────────────┬──────────────────────┬──────────────────────┐
│ Aspek │ TANPA Unit Testing │ DENGAN Unit Testing │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ Bug Detection │ Late (production) │ Early (development) │
│ Cost to Fix │ HIGH ($1000+) │ LOW ($1) │
│ Development Speed │ Appears faster init. │ Faster overall │
│ Code Quality │ Medium │ HIGH │
│ Refactoring Risk │ HIGH │ LOW │
│ Maintenance Cost │ HIGH │ LOW │
│ Deployment Risk │ HIGH │ LOW │
│ Developer Confidence│ LOW │ HIGH │
│ Production Incidents│ HIGH frequency │ LOW frequency │
└─────────────────────┴──────────────────────┴──────────────────────┘
Framework & Tools
Pengenalan Popular Testing Frameworks
Unit testing tidak dilakukan secara manual; ada berbagai framework dan tools yang memudahkan proses testing. Berikut adalah beberapa framework populer:
1. JUnit (Java)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
JUnit adalah testing framework paling populer untuk Java
Version: JUnit 5 (latest) - berbasis Jupiter
Previous: JUnit 4 (masih banyak digunakan)
Fitur:
✓ Annotations (e.g., @Test, @Before, @After)
✓ Assertions (assertEquals, assertTrue, assertThrows)
✓ Parameterized tests
✓ Nested test classes
✓ Dynamic tests
✓ Test lifecycle management
Keunggulan:
✓ Industry standard untuk Java
✓ Excellent IDE support
✓ Rich ecosystem
✓ Performance optimized
Instalasi:
1
2
3
4
5
6
7
Maven POM:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
Contoh Dasar:
1
2
3
4
5
6
7
8
9
10
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class CalculatorTest {
@Test
public void testAddition() {
Calculator calc = new Calculator();
assertEquals(4, calc.add(2, 2));
}
}
2. Jest (JavaScript/TypeScript)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Jest adalah testing framework modern untuk JavaScript/TypeScript
Developed by: Facebook (Meta)
Version: 29.x (latest)
Fitur:
✓ Zero configuration setup
✓ Snapshot testing
✓ Mocking built-in
✓ Code coverage
✓ Parallel execution
✓ Watch mode
Keunggulan:
✓ Very easy to setup
✓ Excellent for React
✓ Great developer experience
✓ Fast execution
Instalasi (NPM):
1
npm install --save-dev jest
Instalasi (Yarn):
1
yarn add --dev jest
Contoh Dasar:
1
2
3
4
test('addition of two numbers', () => {
const result = 2 + 2;
expect(result).toBe(4);
});
3. Pytest (Python)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Pytest adalah testing framework modern untuk Python
Version: 7.x (latest)
Creator: pytest development team
Fitur:
✓ Simple syntax (assert statements)
✓ Fixtures untuk setup/teardown
✓ Parameterization
✓ Plugins ecosystem
✓ Detailed failure reports
✓ Markers untuk test categorization
Keunggulan:
✓ Most pythonic way to test
✓ Minimal boilerplate
✓ Excellent documentation
✓ Large plugin ecosystem
Instalasi (pip):
1
2
pip install pytest
pip install pytest-cov # untuk coverage
Contoh Dasar:
1
2
3
def test_addition():
result = 2 + 2
assert result == 4
Framework Comparison
1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌──────────────────┬────────────────┬────────────────┬────────────────┐
│ Aspek │ JUnit (Java) │ Jest (JS/TS) │ Pytest (Python)│
├──────────────────┼────────────────┼────────────────┼────────────────┤
│ Language │ Java │ JavaScript │ Python │
│ Setup Difficulty │ Medium │ Very Easy │ Very Easy │
│ Learning Curve │ Medium │ Easy │ Very Easy │
│ Syntax │ Annotations │ Jest DSL │ Plain assert │
│ Fixtures │ @Before/@After │ beforeEach() │ @pytest.fixture│
│ Mocking │ Mockito │ Built-in │ unittest.mock │
│ Code Coverage │ JaCoCo │ Built-in │ pytest-cov │
│ Speed │ Fast │ Very Fast │ Fast │
│ Community │ Large │ Very Large │ Large │
│ IDE Support │ Excellent │ Excellent │ Excellent │
└──────────────────┴────────────────┴────────────────┴────────────────┘
Struktur Test: AAA Pattern
Apa itu AAA Pattern?
AAA (Arrange-Act-Assert) adalah pola dasar untuk menulis unit tests yang jelas dan konsisten.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
TEST STRUCTURE - AAA PATTERN
═════════════════════════════════════════════════════════════
┌────────────────────────────────────────────────────────────┐
│ ARRANGE (Setup) │
│ ══════════════════════════════════════════════════════════ │
│ Setup test conditions, create test data, initialize │
│ objects, mock dependencies, etc. │
│ │
│ • Create instances │
│ • Set initial state │
│ • Prepare test data │
│ • Setup mocks/stubs │
└────────────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────────────┐
│ ACT (Execute) │
│ ══════════════════════════════════════════════════════════ │
│ Execute the code being tested (call the function/method) │
│ │
│ • Call method under test │
│ • Perform the action │
│ • Execute business logic │
└────────────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────────────┐
│ ASSERT (Verify) │
│ ══════════════════════════════════════════════════════════ │
│ Verify that the result is as expected │
│ │
│ • Check result value │
│ • Verify state changes │
│ • Confirm side effects │
│ • Assert error conditions │
└────────────────────────────────────────────────────────────┘
Contoh AAA Pattern - Dengan Kode
Contoh 1: Simple Calculation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Contoh BMI Calculator Unit Test dengan AAA Pattern
def test_calculate_bmi_normal_weight():
"""Test BMI calculation untuk normal weight"""
# ARRANGE: Setup test data
weight = 55 # kg
height = 150 # cm
expected_bmi = 24.44
# ACT: Execute the function
calculator = BMICalculator()
result = calculator.calculate_bmi(weight, height)
# ASSERT: Verify the result
assert abs(result - expected_bmi) < 0.01 # Allow 0.01 precision
Contoh 2: Object Initialization
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def test_user_creation():
"""Test user object creation"""
# ARRANGE
name = "Ahmed Hassan"
email = "ahmed@example.com"
age = 25
# ACT
user = User(name=name, email=email, age=age)
# ASSERT
assert user.name == name
assert user.email == email
assert user.age == age
assert user.is_active == True # default value
Contoh 3: With Setup
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def test_database_query():
"""Test database query with setup"""
# ARRANGE: Setup
db = Database()
db.connect()
test_user = User(id=1, name="Test User")
db.insert(test_user)
# ACT
result = db.get_user(id=1)
# ASSERT
assert result.id == 1
assert result.name == "Test User"
# CLEANUP (implicit with fixture)
db.disconnect()
Assertions
Apa itu Assertions?
Assertions adalah pernyataan dalam test yang memverifikasi bahwa nilai aktual sesuai dengan nilai yang diharapkan. Jika assertion gagal, test fail.
Jenis-Jenis Assertions
1. Equality Assertions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Pytest Syntax
assert actual == expected # Equal
assert actual != expected # Not equal
assert a is b # Same object
assert a is not b # Different objects
# Contoh Praktis (BMI Calculator)
def test_equality():
calc = BMICalculator()
# assertEqual
assert calc.calculate_bmi(55, 150) == 24.44
# assertNotEqual
assert calc.calculate_bmi(85, 180) != 20.0
2. Comparison Assertions
1
2
3
4
5
6
7
8
9
10
11
12
assert value > 0 # Greater than
assert value >= 0 # Greater than or equal
assert value < 100 # Less than
assert value <= 100 # Less than or equal
# Contoh: BMI Classification
def test_comparison():
calc = BMICalculator()
bmi = calc.calculate_bmi(85, 180) # 26.23
assert bmi > 25 # Should be > 25 for overweight
assert bmi <= 30 # Should be <= 30 (not obese)
3. Boolean Assertions
1
2
3
4
5
6
7
8
9
10
11
assert condition # Assert True
assert not condition # Assert False
# Contoh
def test_boolean():
calc = BMICalculator()
# Check if BMI is valid
bmi = calc.calculate_bmi(55, 150)
assert bmi > 0 # BMI should be positive
assert not bmi < 0 # BMI should not be negative
4. Membership Assertions
1
2
3
4
5
6
7
8
9
10
11
assert item in collection # Item in collection
assert item not in collection # Item not in collection
# Contoh
def test_classification():
calc = BMICalculator()
bmi = calc.calculate_bmi(55, 150)
classification = calc.classify_bmi(bmi)
valid_classes = ["Underweight", "Normal", "Overweight", "Obese"]
assert classification in valid_classes
5. Type Assertions
1
2
3
4
5
6
7
8
9
10
assert isinstance(obj, type) # Check type
assert type(obj) == expected_type # Check exact type
# Contoh
def test_type():
calc = BMICalculator()
bmi = calc.calculate_bmi(55, 150)
assert isinstance(bmi, float) # Should return float
assert type(bmi) != str # Should not be string
6. Exception Assertions
1
2
3
4
5
6
7
8
9
10
11
12
import pytest
def test_raises():
"""Test that function raises exception"""
calc = BMICalculator()
# Using pytest.raises
with pytest.raises(ValueError):
calc.calculate_bmi(-55, 150) # Negative weight
with pytest.raises(TypeError):
calc.calculate_bmi("abc", 150) # Invalid type
7. Floating Point Assertions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# For comparing floats (handling precision issues)
import math
def test_float_precision():
calc = BMICalculator()
bmi = calc.calculate_bmi(55, 150)
expected = 24.44
# Method 1: Using absolute difference
assert abs(bmi - expected) < 0.01
# Method 2: Using isclose
assert math.isclose(bmi, expected, rel_tol=1e-9)
# Method 3: Using pytest.approx
assert bmi == pytest.approx(expected, abs=0.01)
Assertion Best Practices
1
2
3
4
5
6
7
8
9
10
11
12
13
✅ DO:
✓ Use descriptive assertion messages
✓ One assertion per test (usually)
✓ Assert on business logic, not implementation
✓ Use appropriate assertion methods
✓ Clear expected values
❌ DON'T:
✗ Multiple unrelated assertions in one test
✗ Complex assertion logic
✗ Testing framework behavior instead of code
✗ Vague error messages
✗ Testing multiple scenarios in one test
Studi Kasus: Live Coding dengan Pytest
Sekarang kita akan membuat unit test praktis untuk aplikasi BMI Calculator menggunakan Pytest.
Setup Environment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Install Python 3.9+
python --version
# Create project directory
mkdir bmi_calculator_project
cd bmi_calculator_project
# Create virtual environment
python -m venv venv
# Activate virtual environment
# On Windows:
venv\Scripts\activate
# On macOS/Linux:
source venv/bin/activate
# Install pytest
pip install pytest pytest-cov
# Verify installation
pytest --version
Project Structure
1
2
3
4
5
6
7
8
bmi_calculator_project/
├── bmi_calculator.py # Implementation
├── tests/
│ ├── __init__.py
│ ├── test_bmi_calculator.py # Unit tests
│ └── test_edge_cases.py # Edge case tests
├── requirements.txt
└── .gitignore
Implementation Code
File: bmi_calculator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
"""
BMI Calculator Module
Calculate Body Mass Index and provide classification
"""
class BMICalculator:
"""
A class to calculate BMI and classify weight categories
Formula: BMI = weight (kg) / height (m)²
Classification:
- Underweight: BMI < 18.5
- Normal: 18.5 ≤ BMI < 25
- Overweight: 25 ≤ BMI < 30
- Obese: BMI ≥ 30
"""
# BMI classification thresholds
UNDERWEIGHT_THRESHOLD = 18.5
NORMAL_THRESHOLD = 25.0
OVERWEIGHT_THRESHOLD = 30.0
def calculate_bmi(self, weight: float, height: float) -> float:
"""
Calculate Body Mass Index
Args:
weight: Weight in kilograms (float)
height: Height in centimeters (float)
Returns:
float: BMI value rounded to 2 decimal places
Raises:
ValueError: If weight or height is invalid
TypeError: If weight or height is not numeric
"""
# Validation
if not isinstance(weight, (int, float)) or not isinstance(height, (int, float)):
raise TypeError(f"Weight and height must be numeric. Got weight={type(weight).__name__}, height={type(height).__name__}")
if weight <= 0 or height <= 0:
raise ValueError(f"Weight and height must be positive. Got weight={weight}, height={height}")
if weight > 500: # Unrealistic weight
raise ValueError(f"Weight seems unrealistic: {weight} kg")
if height > 300: # Unrealistic height
raise ValueError(f"Height seems unrealistic: {height} cm")
# Convert height from cm to meters
height_m = height / 100
# Calculate BMI
bmi = weight / (height_m ** 2)
# Round to 2 decimal places
return round(bmi, 2)
def classify_bmi(self, bmi: float) -> str:
"""
Classify BMI into weight category
Args:
bmi: BMI value (float)
Returns:
str: Classification (Underweight, Normal, Overweight, Obese)
Raises:
ValueError: If BMI is invalid (negative or None)
"""
if not isinstance(bmi, (int, float)):
raise TypeError(f"BMI must be numeric. Got {type(bmi).__name__}")
if bmi < 0:
raise ValueError(f"BMI cannot be negative. Got {bmi}")
# Classify
if bmi < self.UNDERWEIGHT_THRESHOLD:
return "Underweight"
elif bmi < self.NORMAL_THRESHOLD:
return "Normal"
elif bmi < self.OVERWEIGHT_THRESHOLD:
return "Overweight"
else:
return "Obese"
def get_bmi_and_classification(self, weight: float, height: float) -> dict:
"""
Calculate BMI and get classification in one call
Args:
weight: Weight in kilograms (float)
height: Height in centimeters (float)
Returns:
dict: Contains 'bmi' and 'classification'
"""
bmi = self.calculate_bmi(weight, height)
classification = self.classify_bmi(bmi)
return {
'bmi': bmi,
'classification': classification,
'weight': weight,
'height': height
}
Unit Tests
File: tests/test_bmi_calculator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
"""
Unit tests for BMI Calculator Module
Using Pytest Framework and AAA Pattern
(Arrange, Act, Assert)
"""
import pytest
import math
from bmi_calculator import BMICalculator
class TestBMICalculation:
"""Test cases for BMI calculation"""
@pytest.fixture
def calculator(self):
"""Fixture: Create BMI calculator instance"""
return BMICalculator()
# ========== VALID INPUT TESTS ==========
def test_calculate_bmi_normal_weight(self, calculator):
"""Test BMI calculation with normal weight values"""
# ARRANGE
weight = 55
height = 150
expected_bmi = 24.44
# ACT
result = calculator.calculate_bmi(weight, height)
# ASSERT
assert result == expected_bmi
def test_calculate_bmi_overweight(self, calculator):
"""Test BMI calculation for overweight person"""
# ARRANGE
weight = 85
height = 180
# ACT
bmi = calculator.calculate_bmi(weight, height)
# ASSERT
expected = round(85 / (1.80 ** 2), 2)
assert bmi == expected
assert bmi > 25 # Overweight threshold
def test_calculate_bmi_underweight(self, calculator):
"""Test BMI calculation for underweight person"""
# ARRANGE
weight = 40
height = 170
# ACT
bmi = calculator.calculate_bmi(weight, height)
# ASSERT
assert bmi < 18.5 # Underweight threshold
def test_calculate_bmi_decimal_values(self, calculator):
"""Test BMI calculation with decimal input values"""
# ARRANGE
weight = 55.5
height = 150.2
# ACT
bmi = calculator.calculate_bmi(weight, height)
# ASSERT
assert isinstance(bmi, float)
assert bmi > 0
def test_calculate_bmi_returns_float(self, calculator):
"""Test that BMI calculation returns float type"""
# ARRANGE, ACT
bmi = calculator.calculate_bmi(70, 175)
# ASSERT
assert isinstance(bmi, float)
def test_calculate_bmi_precision(self, calculator):
"""Test that BMI is rounded to 2 decimal places"""
# ARRANGE
weight = 65
height = 172
# ACT
bmi = calculator.calculate_bmi(weight, height)
# ASSERT
# Check that BMI has at most 2 decimal places
assert len(str(bmi).split('.')[-1]) <= 2
# ========== INVALID INPUT TESTS ==========
def test_calculate_bmi_negative_weight(self, calculator):
"""Test that negative weight raises ValueError"""
# ARRANGE, ACT, ASSERT
with pytest.raises(ValueError, match="must be positive"):
calculator.calculate_bmi(-55, 150)
def test_calculate_bmi_zero_weight(self, calculator):
"""Test that zero weight raises ValueError"""
# ARRANGE, ACT, ASSERT
with pytest.raises(ValueError):
calculator.calculate_bmi(0, 150)
def test_calculate_bmi_negative_height(self, calculator):
"""Test that negative height raises ValueError"""
# ARRANGE, ACT, ASSERT
with pytest.raises(ValueError):
calculator.calculate_bmi(55, -150)
def test_calculate_bmi_zero_height(self, calculator):
"""Test that zero height raises ValueError"""
# ARRANGE, ACT, ASSERT
with pytest.raises(ValueError):
calculator.calculate_bmi(55, 0)
def test_calculate_bmi_non_numeric_weight(self, calculator):
"""Test that non-numeric weight raises TypeError"""
# ARRANGE, ACT, ASSERT
with pytest.raises(TypeError):
calculator.calculate_bmi("55", 150)
def test_calculate_bmi_non_numeric_height(self, calculator):
"""Test that non-numeric height raises TypeError"""
# ARRANGE, ACT, ASSERT
with pytest.raises(TypeError):
calculator.calculate_bmi(55, "150")
def test_calculate_bmi_unrealistic_weight(self, calculator):
"""Test that unrealistic weight raises ValueError"""
# ARRANGE, ACT, ASSERT
with pytest.raises(ValueError, match="unrealistic"):
calculator.calculate_bmi(600, 150)
def test_calculate_bmi_unrealistic_height(self, calculator):
"""Test that unrealistic height raises ValueError"""
# ARRANGE, ACT, ASSERT
with pytest.raises(ValueError, match="unrealistic"):
calculator.calculate_bmi(55, 400)
class TestBMIClassification:
"""Test cases for BMI classification"""
@pytest.fixture
def calculator(self):
"""Fixture: Create BMI calculator instance"""
return BMICalculator()
# ========== VALID CLASSIFICATION TESTS ==========
def test_classify_bmi_underweight(self, calculator):
"""Test BMI classification for underweight"""
# ARRANGE
bmi = 17.5 # < 18.5
# ACT
classification = calculator.classify_bmi(bmi)
# ASSERT
assert classification == "Underweight"
def test_classify_bmi_normal(self, calculator):
"""Test BMI classification for normal weight"""
# ARRANGE
bmi = 22.0 # Between 18.5 and 25
# ACT
classification = calculator.classify_bmi(bmi)
# ASSERT
assert classification == "Normal"
def test_classify_bmi_overweight(self, calculator):
"""Test BMI classification for overweight"""
# ARRANGE
bmi = 27.5 # Between 25 and 30
# ACT
classification = calculator.classify_bmi(bmi)
# ASSERT
assert classification == "Overweight"
def test_classify_bmi_obese(self, calculator):
"""Test BMI classification for obese"""
# ARRANGE
bmi = 32.0 # >= 30
# ACT
classification = calculator.classify_bmi(bmi)
# ASSERT
assert classification == "Obese"
def test_classify_bmi_boundary_underweight_normal(self, calculator):
"""Test BMI classification at underweight/normal boundary"""
# ARRANGE
bmi = 18.5 # Exact boundary
# ACT
classification = calculator.classify_bmi(bmi)
# ASSERT
assert classification == "Normal" # At boundary, should be normal
def test_classify_bmi_boundary_normal_overweight(self, calculator):
"""Test BMI classification at normal/overweight boundary"""
# ARRANGE
bmi = 25.0 # Exact boundary
# ACT
classification = calculator.classify_bmi(bmi)
# ASSERT
assert classification == "Overweight"
def test_classify_bmi_boundary_overweight_obese(self, calculator):
"""Test BMI classification at overweight/obese boundary"""
# ARRANGE
bmi = 30.0 # Exact boundary
# ACT
classification = calculator.classify_bmi(bmi)
# ASSERT
assert classification == "Obese"
# ========== INVALID CLASSIFICATION TESTS ==========
def test_classify_bmi_negative(self, calculator):
"""Test that negative BMI raises ValueError"""
# ARRANGE, ACT, ASSERT
with pytest.raises(ValueError):
calculator.classify_bmi(-5.0)
def test_classify_bmi_non_numeric(self, calculator):
"""Test that non-numeric BMI raises TypeError"""
# ARRANGE, ACT, ASSERT
with pytest.raises(TypeError):
calculator.classify_bmi("25.0")
class TestBMIAndClassificationCombined:
"""Test cases for combined BMI calculation and classification"""
@pytest.fixture
def calculator(self):
"""Fixture: Create BMI calculator instance"""
return BMICalculator()
def test_get_bmi_and_classification_returns_dict(self, calculator):
"""Test that combined method returns dictionary"""
# ARRANGE, ACT
result = calculator.get_bmi_and_classification(55, 150)
# ASSERT
assert isinstance(result, dict)
assert 'bmi' in result
assert 'classification' in result
assert 'weight' in result
assert 'height' in result
def test_get_bmi_and_classification_correct_values(self, calculator):
"""Test that combined method returns correct values"""
# ARRANGE
weight = 55
height = 150
# ACT
result = calculator.get_bmi_and_classification(weight, height)
# ASSERT
assert result['weight'] == 55
assert result['height'] == 150
assert result['bmi'] == 24.44
assert result['classification'] == "Normal"
@pytest.mark.parametrize("weight,height,expected_class", [
(40, 170, "Underweight"), # BMI ≈ 13.84
(55, 150, "Normal"), # BMI ≈ 24.44
(85, 180, "Overweight"), # BMI ≈ 26.23
(100, 170, "Obese"), # BMI ≈ 34.60
])
def test_get_bmi_and_classification_parametrized(
self, calculator, weight, height, expected_class
):
"""Test classification with multiple parameters"""
# ARRANGE, ACT
result = calculator.get_bmi_and_classification(weight, height)
# ASSERT
assert result['classification'] == expected_class
class TestIntegration:
"""Integration tests for BMI Calculator"""
@pytest.fixture
def calculator(self):
"""Fixture: Create BMI calculator instance"""
return BMICalculator()
def test_workflow_from_input_to_classification(self, calculator):
"""Test complete workflow from user input to classification"""
# ARRANGE
user_weight = 70
user_height = 175
# ACT
bmi = calculator.calculate_bmi(user_weight, user_height)
classification = calculator.classify_bmi(bmi)
result = calculator.get_bmi_and_classification(user_weight, user_height)
# ASSERT
assert bmi > 0
assert classification in ["Underweight", "Normal", "Overweight", "Obese"]
assert result['bmi'] == bmi
assert result['classification'] == classification
Running Tests
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Run all tests
pytest
# Run tests with verbose output
pytest -v
# Run specific test file
pytest tests/test_bmi_calculator.py
# Run specific test class
pytest tests/test_bmi_calculator.py::TestBMICalculation
# Run specific test
pytest tests/test_bmi_calculator.py::TestBMICalculation::test_calculate_bmi_normal_weight
# Run tests matching pattern
pytest -k "test_calculate_bmi"
# Run with code coverage
pytest --cov=bmi_calculator --cov-report=html
# Run tests in watch mode (requires pytest-watch)
ptw
# Run tests with detailed output
pytest -vv --tb=long
Sample Output
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
========== test session starts ==========
platform linux -- Python 3.10.0, pytest-7.2.0
collected 24 items
tests/test_bmi_calculator.py::TestBMICalculation::test_calculate_bmi_normal_weight PASSED
tests/test_bmi_calculator.py::TestBMICalculation::test_calculate_bmi_overweight PASSED
tests/test_bmi_calculator.py::TestBMICalculation::test_calculate_bmi_underweight PASSED
tests/test_bmi_calculator.py::TestBMICalculation::test_calculate_bmi_decimal_values PASSED
tests/test_bmi_calculator.py::TestBMICalculation::test_calculate_bmi_returns_float PASSED
tests/test_bmi_calculator.py::TestBMICalculation::test_calculate_bmi_precision PASSED
tests/test_bmi_calculator.py::TestBMICalculation::test_calculate_bmi_negative_weight PASSED
tests/test_bmi_calculator.py::TestBMICalculation::test_calculate_bmi_zero_weight PASSED
tests/test_bmi_calculator.py::TestBMICalculation::test_calculate_bmi_negative_height PASSED
tests/test_bmi_calculator.py::TestBMICalculation::test_calculate_bmi_zero_height PASSED
tests/test_bmi_calculator.py::TestBMICalculation::test_calculate_bmi_non_numeric_weight PASSED
tests/test_bmi_calculator.py::TestBMICalculation::test_calculate_bmi_non_numeric_height PASSED
tests/test_bmi_calculator.py::TestBMICalculation::test_calculate_bmi_unrealistic_weight PASSED
tests/test_bmi_calculator.py::TestBMICalculation::test_calculate_bmi_unrealistic_height PASSED
tests/test_bmi_calculator.py::TestBMIClassification::test_classify_bmi_underweight PASSED
tests/test_bmi_calculator.py::TestBMIClassification::test_classify_bmi_normal PASSED
tests/test_bmi_calculator.py::TestBMIClassification::test_classify_bmi_overweight PASSED
tests/test_bmi_calculator.py::TestBMIClassification::test_classify_bmi_obese PASSED
tests/test_bmi_calculator.py::TestBMIClassification::test_classify_bmi_boundary_underweight_normal PASSED
tests/test_bmi_calculator.py::TestBMIClassification::test_classify_bmi_boundary_normal_overweight PASSED
tests/test_bmi_calculator.py::TestBMIClassification::test_classify_bmi_boundary_overweight_obese PASSED
tests/test_bmi_calculator.py::TestBMIClassification::test_classify_bmi_negative PASSED
tests/test_bmi_calculator.py::TestBMIClassification::test_classify_bmi_non_numeric PASSED
tests/test_bmi_calculator.py::TestBMIAndClassificationCombined::test_get_bmi_and_classification_returns_dict PASSED
tests/test_bmi_calculator.py::TestBMIAndClassificationCombined::test_get_bmi_and_classification_correct_values PASSED
tests/test_bmi_calculator.py::TestBMIAndClassificationCombined::test_get_bmi_and_classification_parametrized[40-170-Underweight] PASSED
tests/test_bmi_calculator.py::TestBMIAndClassificationCombined::test_get_bmi_and_classification_parametrized[55-150-Normal] PASSED
tests/test_bmi_calculator.py::TestBMIAndClassificationCombined::test_get_bmi_and_classification_parametrized[85-180-Overweight] PASSED
tests/test_bmi_calculator.py::TestBMIAndClassificationCombined::test_get_bmi_and_classification_parametrized[100-170-Obese] PASSED
tests/test_bmi_calculator.py::TestIntegration::test_workflow_from_input_to_classification PASSED
========== 30 passed in 0.23s ==========
Coverage report:
Name Stmts Miss Cover
───────────────────────────────────────
bmi_calculator.py 45 0 100%
───────────────────────────────────────
TOTAL 45 0 100%
Best Practices
Unit Testing Best Practices
1. One Assertion Per Test (Usually)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ❌ BAD: Multiple assertions about different things
def test_calculate_bmi_bad():
bmi = calculator.calculate_bmi(55, 150)
assert bmi == 24.44
assert bmi > 0
classification = calculator.classify_bmi(bmi)
assert classification == "Normal"
# ✅ GOOD: One test per concern
def test_calculate_bmi_correct_value():
bmi = calculator.calculate_bmi(55, 150)
assert bmi == 24.44
def test_calculate_bmi_positive():
bmi = calculator.calculate_bmi(55, 150)
assert bmi > 0
def test_classify_calculated_bmi():
bmi = calculator.calculate_bmi(55, 150)
classification = calculator.classify_bmi(bmi)
assert classification == "Normal"
2. Descriptive Test Names
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ❌ BAD: Unclear what's being tested
def test1():
pass
def test_bmi():
pass
# ✅ GOOD: Clear test names
def test_calculate_bmi_with_normal_values():
pass
def test_calculate_bmi_raises_error_for_negative_weight():
pass
def test_classify_bmi_as_normal_for_bmi_22():
pass
3. Avoid Test Interdependencies
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ❌ BAD: Test order matters, tests depend on each other
global_bmi = None
def test_first():
global global_bmi
global_bmi = calculator.calculate_bmi(55, 150)
def test_second():
classification = calculator.classify_bmi(global_bmi) # Depends on test_first
assert classification == "Normal"
# ✅ GOOD: Each test is independent
def test_classify_normal_bmi():
bmi = 22.0
classification = calculator.classify_bmi(bmi)
assert classification == "Normal"
def test_classify_overweight_bmi():
bmi = 27.5
classification = calculator.classify_bmi(bmi)
assert classification == "Overweight"
4. Use Fixtures for Setup
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import pytest
# ❌ BAD: Repetitive setup
def test_one():
calculator = BMICalculator()
bmi = calculator.calculate_bmi(55, 150)
assert bmi > 0
def test_two():
calculator = BMICalculator()
bmi = calculator.calculate_bmi(70, 175)
assert bmi > 0
# ✅ GOOD: Use fixture
@pytest.fixture
def calculator():
return BMICalculator()
def test_one(calculator):
bmi = calculator.calculate_bmi(55, 150)
assert bmi > 0
def test_two(calculator):
bmi = calculator.calculate_bmi(70, 175)
assert bmi > 0
5. Test Edge Cases
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# ✅ Test boundary values
def test_boundary_values():
# Boundary between Underweight and Normal
assert calculator.classify_bmi(18.49) == "Underweight"
assert calculator.classify_bmi(18.50) == "Normal"
# Boundary between Normal and Overweight
assert calculator.classify_bmi(24.99) == "Normal"
assert classifier.classify_bmi(25.00) == "Overweight"
# ✅ Test exceptional cases
def test_exceptional_cases():
# Negative values
with pytest.raises(ValueError):
calculator.calculate_bmi(-55, 150)
# Invalid types
with pytest.raises(TypeError):
calculator.calculate_bmi("55", 150)
# Zero values
with pytest.raises(ValueError):
calculator.calculate_bmi(0, 150)
6. Keep Tests Fast
1
2
3
4
5
6
7
8
9
10
11
12
13
# ❌ BAD: Slow tests (database, API calls)
def test_user_creation_slow():
# Makes real API call
response = requests.post("https://api.example.com/users", ...)
assert response.status_code == 201
# ✅ GOOD: Fast tests (mocked dependencies)
@patch('requests.post')
def test_user_creation_fast(mock_post):
# Mock the API call
mock_post.return_value.status_code = 201
result = create_user(...)
assert result is not None
7. Use Parametrized Tests for Multiple Cases
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ❌ BAD: Repetitive code for similar tests
def test_classification_underweight():
assert calculator.classify_bmi(15.0) == "Underweight"
def test_classification_normal():
assert calculator.classify_bmi(22.0) == "Normal"
def test_classification_overweight():
assert calculator.classify_bmi(27.0) == "Overweight"
# ✅ GOOD: Parametrized test
@pytest.mark.parametrize("bmi,expected", [
(15.0, "Underweight"),
(22.0, "Normal"),
(27.0, "Overweight"),
(32.0, "Obese"),
])
def test_classification(calculator, bmi, expected):
assert calculator.classify_bmi(bmi) == expected
Kesimpulan
Key Takeaways
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
✅ UNIT TESTING BENEFITS:
✓ Early bug detection
✓ Improved code quality
✓ Regression prevention
✓ Living documentation
✓ Faster development
✓ Lower maintenance cost
✅ AAA PATTERN:
✓ Arrange: Setup test conditions
✓ Act: Execute the code
✓ Assert: Verify the result
✅ BEST PRACTICES:
✓ One assertion per test
✓ Descriptive test names
✓ Independent tests
✓ Use fixtures for setup
✓ Test edge cases
✓ Keep tests fast
✓ Use parametrized tests
✅ FRAMEWORKS:
✓ JUnit (Java) - Industry standard
✓ Jest (JavaScript) - Very easy setup
✓ Pytest (Python) - Most pythonic
✅ COVERAGE GOALS:
✓ Minimum 80% code coverage
✓ 100% coverage for critical paths
✓ Focus on functionality, not lines
✓ Quality over quantity
Next Steps
- Start Small: Begin with simple functions and methods
- Practice AAA Pattern: Make it a habit in all your tests
- Expand Coverage: Gradually increase test coverage
- Learn Mocking: Master mocking for external dependencies
- Test Automation: Integrate tests into CI/CD pipeline
- Continuous Improvement: Refactor and maintain tests
Resources
- Pytest Documentation
- JUnit Documentation
- Jest Documentation
- Testing Best Practices
- Clean Code: “Clean Code” by Robert C. Martin
Happy Testing! 🎉