1"""
2Test Suite for Calculator Module
3
4Demonstrates comprehensive testing with pytest:
51. Basic unit tests
62. Parametrized tests
73. Fixtures
84. Mocking
95. Edge cases
106. Exception testing
117. Test organization
128. TDD style
13
14Run with: pytest test_calculator.py -v
15"""
16
17import pytest
18import math
19from unittest.mock import Mock, patch
20from calculator import (
21 Calculator,
22 ScientificCalculator,
23 StatisticsCalculator,
24 CalculatorMemory,
25 is_even,
26 is_prime,
27 fibonacci,
28 gcd
29)
30
31
32# =============================================================================
33# FIXTURES - Reusable test setup
34# =============================================================================
35
36@pytest.fixture
37def calculator():
38 """Fixture providing Calculator instance"""
39 return Calculator()
40
41
42@pytest.fixture
43def sci_calculator():
44 """Fixture providing ScientificCalculator instance"""
45 return ScientificCalculator()
46
47
48@pytest.fixture
49def stats_calculator():
50 """Fixture providing StatisticsCalculator instance"""
51 return StatisticsCalculator()
52
53
54@pytest.fixture
55def calculator_memory():
56 """Fixture providing CalculatorMemory instance"""
57 return CalculatorMemory()
58
59
60@pytest.fixture
61def sample_data():
62 """Fixture providing sample test data"""
63 return [1, 2, 3, 4, 5]
64
65
66@pytest.fixture
67def sample_data_with_duplicates():
68 """Fixture providing data with duplicates"""
69 return [1, 2, 2, 3, 3, 3, 4]
70
71
72# =============================================================================
73# BASIC CALCULATOR TESTS
74# =============================================================================
75
76class TestCalculatorBasicOperations:
77 """Test basic arithmetic operations"""
78
79 def test_add_positive_numbers(self, calculator):
80 """Test addition of positive numbers"""
81 result = calculator.add(5, 3)
82 assert result == 8
83
84 def test_add_negative_numbers(self, calculator):
85 """Test addition of negative numbers"""
86 result = calculator.add(-5, -3)
87 assert result == -8
88
89 def test_subtract(self, calculator):
90 """Test subtraction"""
91 result = calculator.subtract(10, 4)
92 assert result == 6
93
94 def test_multiply(self, calculator):
95 """Test multiplication"""
96 result = calculator.multiply(6, 7)
97 assert result == 42
98
99 def test_divide(self, calculator):
100 """Test division"""
101 result = calculator.divide(15, 3)
102 assert result == 5.0
103
104 def test_divide_by_zero_raises_error(self, calculator):
105 """Test that division by zero raises ValueError"""
106 with pytest.raises(ValueError, match="Cannot divide by zero"):
107 calculator.divide(10, 0)
108
109 def test_power(self, calculator):
110 """Test exponentiation"""
111 result = calculator.power(2, 8)
112 assert result == 256
113
114 def test_square_root(self, calculator):
115 """Test square root"""
116 result = calculator.square_root(16)
117 assert result == 4.0
118
119 def test_square_root_negative_raises_error(self, calculator):
120 """Test that square root of negative raises ValueError"""
121 with pytest.raises(ValueError, match="Cannot calculate square root"):
122 calculator.square_root(-1)
123
124 def test_modulo(self, calculator):
125 """Test modulo operation"""
126 result = calculator.modulo(10, 3)
127 assert result == 1
128
129 def test_modulo_zero_raises_error(self, calculator):
130 """Test that modulo by zero raises ValueError"""
131 with pytest.raises(ValueError, match="Cannot calculate modulo"):
132 calculator.modulo(10, 0)
133
134
135# =============================================================================
136# PARAMETRIZED TESTS - Test multiple inputs efficiently
137# =============================================================================
138
139class TestCalculatorParametrized:
140 """Parametrized tests for comprehensive coverage"""
141
142 @pytest.mark.parametrize("a, b, expected", [
143 (1, 1, 2),
144 (0, 0, 0),
145 (-1, 1, 0),
146 (100, 200, 300),
147 (0.1, 0.2, pytest.approx(0.3)), # Floating point comparison
148 ])
149 def test_add_parametrized(self, calculator, a, b, expected):
150 """Test addition with multiple input combinations"""
151 assert calculator.add(a, b) == expected
152
153 @pytest.mark.parametrize("a, b, expected", [
154 (10, 2, 5.0),
155 (7, 2, 3.5),
156 (-10, 2, -5.0),
157 (10, -2, -5.0),
158 ])
159 def test_divide_parametrized(self, calculator, a, b, expected):
160 """Test division with multiple inputs"""
161 assert calculator.divide(a, b) == expected
162
163 @pytest.mark.parametrize("base, exponent, expected", [
164 (2, 0, 1),
165 (2, 1, 2),
166 (2, 10, 1024),
167 (5, 2, 25),
168 (10, -1, 0.1),
169 ])
170 def test_power_parametrized(self, calculator, base, exponent, expected):
171 """Test power with various bases and exponents"""
172 assert calculator.power(base, exponent) == pytest.approx(expected)
173
174
175# =============================================================================
176# SCIENTIFIC CALCULATOR TESTS
177# =============================================================================
178
179class TestScientificCalculator:
180 """Test scientific calculator functions"""
181
182 def test_sin(self, sci_calculator):
183 """Test sine function"""
184 result = sci_calculator.sin(math.pi / 2)
185 assert result == pytest.approx(1.0)
186
187 def test_cos(self, sci_calculator):
188 """Test cosine function"""
189 result = sci_calculator.cos(0)
190 assert result == pytest.approx(1.0)
191
192 def test_tan(self, sci_calculator):
193 """Test tangent function"""
194 result = sci_calculator.tan(math.pi / 4)
195 assert result == pytest.approx(1.0)
196
197 @pytest.mark.parametrize("n, expected", [
198 (0, 1),
199 (1, 1),
200 (5, 120),
201 (10, 3628800),
202 ])
203 def test_factorial(self, sci_calculator, n, expected):
204 """Test factorial with various inputs"""
205 assert sci_calculator.factorial(n) == expected
206
207 def test_factorial_negative_raises_error(self, sci_calculator):
208 """Test that factorial of negative raises ValueError"""
209 with pytest.raises(ValueError, match="not defined for negative"):
210 sci_calculator.factorial(-1)
211
212 def test_factorial_non_integer_raises_error(self, sci_calculator):
213 """Test that factorial of non-integer raises TypeError"""
214 with pytest.raises(TypeError, match="requires integer"):
215 sci_calculator.factorial(3.5)
216
217 def test_log_natural(self, sci_calculator):
218 """Test natural logarithm"""
219 result = sci_calculator.log(math.e)
220 assert result == pytest.approx(1.0)
221
222 def test_log_base_10(self, sci_calculator):
223 """Test logarithm base 10"""
224 result = sci_calculator.log(100, 10)
225 assert result == pytest.approx(2.0)
226
227 def test_log_zero_raises_error(self, sci_calculator):
228 """Test that log(0) raises ValueError"""
229 with pytest.raises(ValueError, match="must be positive"):
230 sci_calculator.log(0)
231
232 def test_log_negative_raises_error(self, sci_calculator):
233 """Test that log of negative raises ValueError"""
234 with pytest.raises(ValueError, match="must be positive"):
235 sci_calculator.log(-1)
236
237
238# =============================================================================
239# STATISTICS CALCULATOR TESTS
240# =============================================================================
241
242class TestStatisticsCalculator:
243 """Test statistical functions"""
244
245 def test_mean(self, stats_calculator, sample_data):
246 """Test mean calculation"""
247 result = stats_calculator.mean(sample_data)
248 assert result == 3.0
249
250 def test_mean_empty_raises_error(self, stats_calculator):
251 """Test that mean of empty list raises ValueError"""
252 with pytest.raises(ValueError, match="empty list"):
253 stats_calculator.mean([])
254
255 def test_median_odd_length(self, stats_calculator):
256 """Test median with odd number of elements"""
257 result = stats_calculator.median([1, 2, 3, 4, 5])
258 assert result == 3
259
260 def test_median_even_length(self, stats_calculator):
261 """Test median with even number of elements"""
262 result = stats_calculator.median([1, 2, 3, 4])
263 assert result == 2.5
264
265 def test_median_unsorted(self, stats_calculator):
266 """Test median with unsorted data"""
267 result = stats_calculator.median([5, 1, 3, 2, 4])
268 assert result == 3
269
270 def test_mode(self, stats_calculator, sample_data_with_duplicates):
271 """Test mode calculation"""
272 result = stats_calculator.mode(sample_data_with_duplicates)
273 assert result == 3
274
275 def test_mode_no_unique_raises_error(self, stats_calculator):
276 """Test that mode with no unique value raises error"""
277 with pytest.raises(ValueError, match="No unique mode"):
278 stats_calculator.mode([1, 1, 2, 2])
279
280 def test_variance(self, stats_calculator, sample_data):
281 """Test variance calculation"""
282 result = stats_calculator.variance(sample_data)
283 assert result == pytest.approx(2.0)
284
285 def test_standard_deviation(self, stats_calculator, sample_data):
286 """Test standard deviation"""
287 result = stats_calculator.standard_deviation(sample_data)
288 assert result == pytest.approx(math.sqrt(2.0))
289
290
291# =============================================================================
292# STATEFUL TESTS - Testing objects with state
293# =============================================================================
294
295class TestCalculatorMemory:
296 """Test calculator with memory functionality"""
297
298 def test_store_and_recall(self, calculator_memory):
299 """Test storing and recalling values"""
300 calculator_memory.store(42)
301 assert calculator_memory.recall() == 42
302
303 def test_initial_memory_is_zero(self, calculator_memory):
304 """Test that initial memory is 0"""
305 assert calculator_memory.recall() == 0
306
307 def test_clear_memory(self, calculator_memory):
308 """Test clearing memory"""
309 calculator_memory.store(100)
310 calculator_memory.clear()
311 assert calculator_memory.recall() == 0
312
313 def test_add_to_memory(self, calculator_memory):
314 """Test adding to memory"""
315 calculator_memory.store(10)
316 calculator_memory.add_to_memory(5)
317 assert calculator_memory.recall() == 15
318
319 def test_history_tracking(self, calculator_memory):
320 """Test that history is tracked correctly"""
321 calculator_memory.store(10)
322 calculator_memory.store(20)
323 calculator_memory.add_to_memory(5)
324
325 history = calculator_memory.get_history()
326 assert history == [10, 20, 25]
327
328 def test_clear_history(self, calculator_memory):
329 """Test clearing history"""
330 calculator_memory.store(10)
331 calculator_memory.store(20)
332 calculator_memory.clear_history()
333
334 assert calculator_memory.get_history() == []
335
336
337# =============================================================================
338# PURE FUNCTION TESTS
339# =============================================================================
340
341class TestHelperFunctions:
342 """Test pure helper functions"""
343
344 @pytest.mark.parametrize("n, expected", [
345 (0, True),
346 (1, False),
347 (2, True),
348 (10, True),
349 (11, False),
350 ])
351 def test_is_even(self, n, expected):
352 """Test even number detection"""
353 assert is_even(n) == expected
354
355 @pytest.mark.parametrize("n, expected", [
356 (0, False),
357 (1, False),
358 (2, True),
359 (3, True),
360 (4, False),
361 (17, True),
362 (20, False),
363 (97, True),
364 ])
365 def test_is_prime(self, n, expected):
366 """Test prime number detection"""
367 assert is_prime(n) == expected
368
369 @pytest.mark.parametrize("n, expected", [
370 (0, 0),
371 (1, 1),
372 (5, 5),
373 (10, 55),
374 (15, 610),
375 ])
376 def test_fibonacci(self, n, expected):
377 """Test Fibonacci sequence"""
378 assert fibonacci(n) == expected
379
380 def test_fibonacci_negative_raises_error(self):
381 """Test that negative Fibonacci raises error"""
382 with pytest.raises(ValueError, match="not defined for negative"):
383 fibonacci(-1)
384
385 @pytest.mark.parametrize("a, b, expected", [
386 (48, 18, 6),
387 (100, 50, 50),
388 (17, 19, 1), # Coprime
389 (0, 5, 5),
390 ])
391 def test_gcd(self, a, b, expected):
392 """Test greatest common divisor"""
393 assert gcd(a, b) == expected
394
395
396# =============================================================================
397# EDGE CASES AND BOUNDARY CONDITIONS
398# =============================================================================
399
400class TestEdgeCases:
401 """Test edge cases and boundary conditions"""
402
403 def test_add_large_numbers(self, calculator):
404 """Test addition with very large numbers"""
405 result = calculator.add(1e15, 1e15)
406 assert result == 2e15
407
408 def test_divide_very_small_result(self, calculator):
409 """Test division resulting in very small number"""
410 result = calculator.divide(1, 1e10)
411 assert result == pytest.approx(1e-10)
412
413 def test_power_zero_exponent(self, calculator):
414 """Test that any number to power 0 equals 1"""
415 assert calculator.power(1000, 0) == 1
416 assert calculator.power(-5, 0) == 1
417
418 def test_square_root_zero(self, calculator):
419 """Test square root of zero"""
420 assert calculator.square_root(0) == 0
421
422 def test_mean_single_element(self, stats_calculator):
423 """Test mean of single element"""
424 assert stats_calculator.mean([42]) == 42
425
426 def test_median_single_element(self, stats_calculator):
427 """Test median of single element"""
428 assert stats_calculator.median([42]) == 42
429
430
431# =============================================================================
432# MOCKING EXAMPLE
433# =============================================================================
434
435class TestMocking:
436 """Demonstrate testing with mocks"""
437
438 def test_calculator_with_mock_dependency(self):
439 """Example of mocking external dependencies"""
440 # Create a mock for external API or service
441 mock_service = Mock()
442 mock_service.get_exchange_rate.return_value = 1.2
443
444 # Use the mock in calculation
445 euros = 100
446 rate = mock_service.get_exchange_rate("EUR", "USD")
447 dollars = euros * rate
448
449 assert dollars == 120
450 mock_service.get_exchange_rate.assert_called_once_with("EUR", "USD")
451
452 @patch('math.sqrt')
453 def test_square_root_with_patch(self, mock_sqrt, calculator):
454 """Example of patching built-in functions"""
455 mock_sqrt.return_value = 5.0
456
457 result = calculator.square_root(25)
458
459 assert result == 5.0
460 mock_sqrt.assert_called_once_with(25)
461
462
463# =============================================================================
464# TEST ORGANIZATION AND MARKERS
465# =============================================================================
466
467@pytest.mark.slow
468class TestSlowOperations:
469 """Tests that might be slow (can be skipped with -m "not slow")"""
470
471 def test_factorial_large_number(self, sci_calculator):
472 """Test factorial of large number"""
473 result = sci_calculator.factorial(100)
474 assert result > 0 # Just check it completes
475
476
477@pytest.mark.integration
478class TestIntegration:
479 """Integration tests (can be run separately with -m integration)"""
480
481 def test_complex_calculation_workflow(self):
482 """Test complete calculation workflow"""
483 calc = Calculator()
484 sci_calc = ScientificCalculator()
485 stats_calc = StatisticsCalculator()
486
487 # Multi-step calculation
488 step1 = calc.add(10, 5)
489 step2 = calc.multiply(step1, 2)
490 step3 = calc.divide(step2, 5)
491 step4 = sci_calc.power(step3, 2)
492
493 assert step4 == pytest.approx(36.0)
494
495
496# =============================================================================
497# RUN SUMMARY
498# =============================================================================
499
500if __name__ == "__main__":
501 print("=" * 70)
502 print("CALCULATOR TEST SUITE")
503 print("=" * 70)
504 print("""
505This test suite demonstrates:
506
5071. FIXTURES
508 ✓ Reusable test setup
509 ✓ Reduces code duplication
510 ✓ Clean test organization
511
5122. PARAMETRIZED TESTS
513 ✓ Test multiple inputs efficiently
514 ✓ Better coverage with less code
515 ✓ Clear test cases
516
5173. EXCEPTION TESTING
518 ✓ Verify error handling
519 ✓ Check error messages
520 ✓ Ensure robustness
521
5224. EDGE CASES
523 ✓ Boundary conditions
524 ✓ Zero, negative, large numbers
525 ✓ Empty collections
526
5275. MOCKING
528 ✓ Isolate code under test
529 ✓ Simulate external dependencies
530 ✓ Verify interactions
531
5326. TEST ORGANIZATION
533 ✓ Grouped by functionality
534 ✓ Clear naming
535 ✓ Test markers for categorization
536
537Run with:
538 pytest test_calculator.py -v # Verbose output
539 pytest test_calculator.py -v -m "not slow" # Skip slow tests
540 pytest test_calculator.py -k "mean" # Run specific tests
541 pytest test_calculator.py --cov # With coverage report
542""")