test_calculator.py

Download
python 543 lines 16.9 KB
  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""")