Reorganize tests: Remove mocks, add real integration tests

REMOVED (Mock-based tests that duplicate inline tests):
• test_activations.py - Used MockTensor instead of real Tensor
• test_layers.py - Used MockTensor instead of real Tensor
• test_networks.py - Used MockTensor/MockLayer instead of real components
• test_cnn.py - Used MockTensor instead of real Tensor
• test_dataloader.py - Used MockTensor/MockDataset instead of real components

ADDED (Real integration tests with actual TinyTorch components):
• integration/test_tensor_activations.py - Tests real Tensor ↔ Activations integration
• integration/test_layers_networks.py - Tests real Dense ↔ Sequential/MLP integration
• e2e/ directory structure for end-to-end tests

RESULT:
• Reduced test count from 209 → 70 (removed 139 redundant mock-based tests)
• All 70 remaining tests use real components for true integration testing
• Clear separation: inline tests (component validation) vs integration tests (cross-module)
• Better QA structure following proper testing pyramid

This follows QA best practices: since all modules are working and building on each
other, integration tests should use real components, not mocks. Mocks were preventing
us from catching actual integration issues.
This commit is contained in:
Vijay Janapa Reddi
2025-07-13 23:10:14 -04:00
parent ee16f59323
commit 4a1bc7c7f4
8 changed files with 542 additions and 2474 deletions

6
tests/e2e/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
"""
End-to-End tests for TinyTorch.
These tests verify complete workflows and real-world scenarios
using actual TinyTorch components throughout the entire pipeline.
"""

View File

@@ -0,0 +1,306 @@
"""
Integration Tests - Layers and Networks
Tests real integration between Dense layers and Network architectures.
Uses actual TinyTorch components to verify they work together correctly.
"""
import pytest
import numpy as np
import sys
from pathlib import Path
# Add the project root to the path
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))
# Import REAL TinyTorch components
try:
from tinytorch.core.tensor import Tensor
from tinytorch.core.activations import ReLU, Sigmoid, Tanh
from tinytorch.core.layers import Dense
from tinytorch.core.networks import Sequential, MLP
except ImportError:
# Fallback for development
sys.path.append(str(project_root / "modules" / "source" / "01_tensor"))
sys.path.append(str(project_root / "modules" / "source" / "02_activations"))
sys.path.append(str(project_root / "modules" / "source" / "03_layers"))
sys.path.append(str(project_root / "modules" / "source" / "04_networks"))
from tensor_dev import Tensor
from activations_dev import ReLU, Sigmoid, Tanh
from layers_dev import Dense
from networks_dev import Sequential, MLP
class TestLayerNetworkIntegration:
"""Test real integration between Dense layers and Networks."""
def test_dense_layer_with_real_tensors(self):
"""Test Dense layer works with real Tensor objects."""
# Create real layer and tensor
layer = Dense(input_size=3, output_size=2)
x = Tensor([[1.0, 2.0, 3.0]])
# Forward pass
result = layer(x)
# Verify real integration
assert isinstance(result, Tensor)
assert result.shape == (1, 2)
assert hasattr(result, 'data')
assert not np.any(np.isnan(result.data))
def test_sequential_with_real_components(self):
"""Test Sequential network with real Dense layers and activations."""
# Create network with REAL components
network = Sequential([
Dense(input_size=4, output_size=8),
ReLU(),
Dense(input_size=8, output_size=4),
Sigmoid(),
Dense(input_size=4, output_size=2)
])
# Test with real tensor
x = Tensor([[1.0, 2.0, 3.0, 4.0]])
result = network(x)
# Verify real integration
assert isinstance(result, Tensor)
assert result.shape == (1, 2)
assert not np.any(np.isnan(result.data))
assert not np.any(np.isinf(result.data))
def test_mlp_with_real_components(self):
"""Test MLP network with real components."""
# Create MLP with real components
mlp = MLP(input_size=5, hidden_size=10, output_size=3)
# Test with real tensor
x = Tensor([[1.0, 2.0, 3.0, 4.0, 5.0]])
result = mlp(x)
# Verify real integration
assert isinstance(result, Tensor)
assert result.shape == (1, 3)
assert hasattr(mlp, 'network')
assert isinstance(mlp.network, Sequential)
def test_deep_network_integration(self):
"""Test deep network with multiple real layers."""
# Create deep network
deep_network = Sequential([
Dense(input_size=6, output_size=12),
ReLU(),
Dense(input_size=12, output_size=8),
Tanh(),
Dense(input_size=8, output_size=4),
ReLU(),
Dense(input_size=4, output_size=2),
Sigmoid()
])
# Test with real tensor
x = Tensor([[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]])
result = deep_network(x)
# Verify deep integration
assert isinstance(result, Tensor)
assert result.shape == (1, 2)
# Verify final activation worked (sigmoid bounds)
assert np.all(result.data >= 0.0)
assert np.all(result.data <= 1.0)
def test_batch_processing_integration(self):
"""Test network works with batched real tensors."""
network = Sequential([
Dense(input_size=3, output_size=6),
ReLU(),
Dense(input_size=6, output_size=2)
])
# Create batch of real tensors
batch_x = Tensor([
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0]
])
result = network(batch_x)
# Verify batch integration
assert isinstance(result, Tensor)
assert result.shape == (3, 2)
assert not np.any(np.isnan(result.data))
def test_layer_parameter_sharing(self):
"""Test that layers maintain consistent parameters."""
layer = Dense(input_size=4, output_size=3)
# Store original parameters
original_weights = np.copy(layer.weights.data)
original_bias = np.copy(layer.bias.data) if layer.bias is not None else None
# Multiple forward passes
x1 = Tensor([[1.0, 2.0, 3.0, 4.0]])
x2 = Tensor([[5.0, 6.0, 7.0, 8.0]])
result1 = layer(x1)
result2 = layer(x2)
# Parameters should be unchanged
np.testing.assert_array_equal(layer.weights.data, original_weights)
if original_bias is not None:
np.testing.assert_array_equal(layer.bias.data, original_bias)
# Results should be different for different inputs
assert not np.allclose(result1.data, result2.data)
def test_network_composition_integration(self):
"""Test composing networks with real components."""
# Create encoder
encoder = Sequential([
Dense(input_size=8, output_size=4),
ReLU(),
Dense(input_size=4, output_size=2)
])
# Create decoder
decoder = Sequential([
Dense(input_size=2, output_size=4),
ReLU(),
Dense(input_size=4, output_size=8)
])
# Compose autoencoder
autoencoder = Sequential([encoder, decoder])
# Test with real tensor
x = Tensor([[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]])
result = autoencoder(x)
# Verify composition
assert isinstance(result, Tensor)
assert result.shape == x.shape # Should reconstruct input size
class TestMLClassificationPipeline:
"""Test realistic ML classification pipelines."""
def test_binary_classifier_integration(self):
"""Test binary classification with real components."""
# Create binary classifier
classifier = Sequential([
Dense(input_size=4, output_size=8),
ReLU(),
Dense(input_size=8, output_size=4),
ReLU(),
Dense(input_size=4, output_size=1),
Sigmoid() # Binary classification
])
# Test with sample data
x = Tensor([[0.5, 1.5, -0.5, 2.0]])
prediction = classifier(x)
# Verify binary classification
assert isinstance(prediction, Tensor)
assert prediction.shape == (1, 1)
assert 0.0 <= prediction.data[0, 0] <= 1.0 # Probability
def test_multiclass_classifier_integration(self):
"""Test multi-class classification with real components."""
# Create multi-class classifier (3 classes)
classifier = MLP(
input_size=6,
hidden_size=12,
output_size=3,
activation=ReLU,
output_activation=None # Will add softmax manually
)
# Add softmax for multi-class
from tinytorch.core.activations import Softmax
softmax = Softmax()
# Test prediction
x = Tensor([[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]])
logits = classifier(x)
probabilities = softmax(logits)
# Verify multi-class classification
assert isinstance(probabilities, Tensor)
assert probabilities.shape == (1, 3)
assert np.isclose(np.sum(probabilities.data), 1.0, atol=1e-6)
assert np.all(probabilities.data >= 0.0)
def test_feature_extraction_pipeline(self):
"""Test feature extraction with real components."""
# Feature extractor (encoder part)
feature_extractor = Sequential([
Dense(input_size=10, output_size=16),
ReLU(),
Dense(input_size=16, output_size=8),
ReLU(),
Dense(input_size=8, output_size=4) # Feature representation
])
# Classifier head
classifier = Sequential([
Dense(input_size=4, output_size=8),
ReLU(),
Dense(input_size=8, output_size=2)
])
# Full pipeline
x = Tensor([[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]])
# Extract features
features = feature_extractor(x)
assert isinstance(features, Tensor)
assert features.shape == (1, 4)
# Classify features
predictions = classifier(features)
assert isinstance(predictions, Tensor)
assert predictions.shape == (1, 2)
class TestErrorHandlingIntegration:
"""Test error handling in real integration scenarios."""
def test_shape_mismatch_detection(self):
"""Test that shape mismatches are properly detected."""
layer = Dense(input_size=3, output_size=2)
# Wrong input size should raise error
wrong_x = Tensor([[1.0, 2.0]]) # Should be size 3
with pytest.raises(Exception):
layer(wrong_x)
def test_network_layer_compatibility(self):
"""Test network layer compatibility checking."""
# Create network with incompatible layers
try:
incompatible_network = Sequential([
Dense(input_size=4, output_size=3),
Dense(input_size=5, output_size=2) # Expects 5, gets 3
])
x = Tensor([[1.0, 2.0, 3.0, 4.0]])
result = incompatible_network(x)
# If no error raised, should handle gracefully
assert isinstance(result, Tensor)
except Exception:
# It's acceptable to raise an error for incompatible layers
pass
if __name__ == "__main__":
# Run integration tests
pytest.main([__file__, "-v"])

View File

@@ -0,0 +1,230 @@
"""
Integration Tests - Tensor and Activations
Tests real integration between Tensor and Activation modules.
Uses actual TinyTorch components to verify they work together correctly.
"""
import pytest
import numpy as np
import sys
from pathlib import Path
# Add the project root to the path
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))
# Import REAL TinyTorch components
try:
from tinytorch.core.tensor import Tensor
from tinytorch.core.activations import ReLU, Sigmoid, Tanh, Softmax
except ImportError:
# Fallback for development
sys.path.append(str(project_root / "modules" / "source" / "01_tensor"))
sys.path.append(str(project_root / "modules" / "source" / "02_activations"))
from tensor_dev import Tensor
from activations_dev import ReLU, Sigmoid, Tanh, Softmax
class TestTensorActivationIntegration:
"""Test real integration between Tensor and Activation modules."""
def test_relu_with_real_tensors(self):
"""Test ReLU activation with real Tensor objects."""
relu = ReLU()
# Test with negative, zero, and positive values
x = Tensor([[-2.0, -1.0, 0.0, 1.0, 2.0]])
result = relu(x)
# Verify it returns a real Tensor
assert isinstance(result, Tensor)
assert result.shape == x.shape
# Verify ReLU behavior: max(0, x)
expected = np.array([[0.0, 0.0, 0.0, 1.0, 2.0]])
np.testing.assert_allclose(result.data, expected)
def test_sigmoid_with_real_tensors(self):
"""Test Sigmoid activation with real Tensor objects."""
sigmoid = Sigmoid()
# Test with various inputs
x = Tensor([[-5.0, -1.0, 0.0, 1.0, 5.0]])
result = sigmoid(x)
# Verify it returns a real Tensor
assert isinstance(result, Tensor)
assert result.shape == x.shape
# Verify sigmoid properties
assert np.all(result.data > 0.0) # All positive
assert np.all(result.data < 1.0) # All less than 1
assert np.isclose(result.data[0, 2], 0.5, atol=1e-6) # sigmoid(0) = 0.5
def test_tanh_with_real_tensors(self):
"""Test Tanh activation with real Tensor objects."""
tanh = Tanh()
x = Tensor([[-2.0, -1.0, 0.0, 1.0, 2.0]])
result = tanh(x)
# Verify it returns a real Tensor
assert isinstance(result, Tensor)
assert result.shape == x.shape
# Verify tanh properties
assert np.all(result.data > -1.0) # All greater than -1
assert np.all(result.data < 1.0) # All less than 1
assert np.isclose(result.data[0, 2], 0.0, atol=1e-6) # tanh(0) = 0
def test_softmax_with_real_tensors(self):
"""Test Softmax activation with real Tensor objects."""
softmax = Softmax()
# Test with logits
x = Tensor([[1.0, 2.0, 3.0]])
result = softmax(x)
# Verify it returns a real Tensor
assert isinstance(result, Tensor)
assert result.shape == x.shape
# Verify softmax properties
assert np.all(result.data > 0.0) # All positive
assert np.all(result.data < 1.0) # All less than 1
assert np.isclose(np.sum(result.data), 1.0, atol=1e-6) # Sums to 1
def test_activation_chaining_with_real_tensors(self):
"""Test chaining activations with real Tensors."""
relu = ReLU()
sigmoid = Sigmoid()
# Start with mixed positive/negative values
x = Tensor([[-1.0, 0.0, 1.0, 2.0]])
# Apply ReLU first: negative values become 0
relu_result = relu(x)
expected_after_relu = np.array([[0.0, 0.0, 1.0, 2.0]])
np.testing.assert_allclose(relu_result.data, expected_after_relu)
# Apply Sigmoid to ReLU output
final_result = sigmoid(relu_result)
# Verify final result properties
assert isinstance(final_result, Tensor)
assert np.all(final_result.data > 0.0)
assert np.all(final_result.data < 1.0)
# First two should be sigmoid(0) = 0.5
assert np.isclose(final_result.data[0, 0], 0.5, atol=1e-6)
assert np.isclose(final_result.data[0, 1], 0.5, atol=1e-6)
def test_batch_processing_integration(self):
"""Test activation functions work with batched tensors."""
activations = [ReLU(), Sigmoid(), Tanh()]
# Create batch of samples
batch_x = Tensor([
[-2.0, -1.0, 0.0, 1.0, 2.0],
[0.5, 1.5, -0.5, -1.5, 0.0],
[3.0, -3.0, 1.0, -1.0, 0.0]
])
for activation in activations:
result = activation(batch_x)
# Verify batch processing preserves shape
assert isinstance(result, Tensor)
assert result.shape == batch_x.shape
assert not np.any(np.isnan(result.data))
assert not np.any(np.isinf(result.data))
def test_softmax_batch_integration(self):
"""Test Softmax works correctly with batched tensors."""
softmax = Softmax()
# Create batch of logits
batch_x = Tensor([
[1.0, 2.0, 3.0],
[0.0, 0.0, 0.0],
[10.0, 20.0, 30.0]
])
result = softmax(batch_x)
# Verify batch processing
assert isinstance(result, Tensor)
assert result.shape == batch_x.shape
# Each row should sum to 1
for i in range(batch_x.shape[0]):
row_sum = np.sum(result.data[i])
assert np.isclose(row_sum, 1.0, atol=1e-6)
def test_tensor_type_preservation(self):
"""Test that activations preserve tensor type and properties."""
activations = [ReLU(), Sigmoid(), Tanh(), Softmax()]
# Test with different tensor shapes
test_tensors = [
Tensor([5.0]), # Scalar
Tensor([1.0, 2.0, 3.0]), # 1D
Tensor([[1.0, 2.0], [3.0, 4.0]]), # 2D
]
for tensor in test_tensors:
for activation in activations:
result = activation(tensor)
# Verify type preservation
assert isinstance(result, Tensor)
assert result.shape == tensor.shape
assert hasattr(result, 'data')
assert hasattr(result, 'shape')
def test_numerical_stability_integration(self):
"""Test numerical stability when using real tensors with activations."""
# Test with extreme values
extreme_tensor = Tensor([[-1000.0, 1000.0, 0.0]])
# ReLU should handle extreme values
relu = ReLU()
relu_result = relu(extreme_tensor)
assert np.all(np.isfinite(relu_result.data))
# Sigmoid should handle extreme values
sigmoid = Sigmoid()
sigmoid_result = sigmoid(extreme_tensor)
assert np.all(np.isfinite(sigmoid_result.data))
assert np.all(sigmoid_result.data >= 0.0)
assert np.all(sigmoid_result.data <= 1.0)
# Tanh should handle extreme values
tanh = Tanh()
tanh_result = tanh(extreme_tensor)
assert np.all(np.isfinite(tanh_result.data))
assert np.all(tanh_result.data >= -1.0)
assert np.all(tanh_result.data <= 1.0)
class TestActivationPolymorphism:
"""Test that activations work with different tensor-like objects."""
def test_activation_type_preservation(self):
"""Test that activations preserve input type."""
relu = ReLU()
# Test with real Tensor
tensor_input = Tensor([[1.0, -1.0, 2.0]])
tensor_result = relu(tensor_input)
# Should return same type as input
assert type(tensor_result) == type(tensor_input)
assert isinstance(tensor_result, Tensor)
if __name__ == "__main__":
# Run integration tests
pytest.main([__file__, "-v"])

View File

@@ -1,514 +0,0 @@
"""
Mock-based module tests for Activations module.
This test file uses simple mocks to avoid cross-module dependencies while thoroughly
testing the Activations module functionality. The MockTensor class provides a minimal
interface that matches the expected Tensor behavior without requiring the actual
Tensor implementation.
Test Philosophy:
- Use simple, visible mocks instead of complex mocking frameworks
- Test interface contracts and behavior, not implementation details
- Avoid dependency cascade where activations tests fail due to tensor bugs
- Focus on the activation functions' mathematical correctness
- Ensure educational value with clear test structure
"""
import pytest
import numpy as np
import sys
import os
# Add the module source directory to the path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'modules', 'source', '02_activations'))
from activations_dev import ReLU, Sigmoid, Tanh, Softmax
class MockTensor:
"""
Simple mock tensor for testing activations without tensor dependencies.
This mock provides just enough functionality to test activation functions
without requiring the full Tensor implementation. It's intentionally simple
and visible to make test behavior clear.
"""
def __init__(self, data):
"""Initialize with numpy array data."""
if isinstance(data, (list, tuple)):
self.data = np.array(data, dtype=np.float32)
elif isinstance(data, np.ndarray):
self.data = data.astype(np.float32)
else:
self.data = np.array([data], dtype=np.float32)
@property
def shape(self):
"""Return shape of the underlying data."""
return self.data.shape
def __repr__(self):
return f"MockTensor({self.data})"
def __eq__(self, other):
"""Check equality with another MockTensor."""
if isinstance(other, MockTensor):
return np.allclose(self.data, other.data)
return False
class TestReLUActivation:
"""Test ReLU activation function with mock tensors."""
def test_relu_initialization(self):
"""Test ReLU can be initialized without parameters."""
relu = ReLU()
assert relu is not None
assert hasattr(relu, 'forward') or hasattr(relu, '__call__')
def test_relu_positive_values(self):
"""Test ReLU preserves positive values."""
relu = ReLU()
# Test single positive value
input_tensor = MockTensor([5.0])
output = relu(input_tensor)
assert isinstance(output, MockTensor)
assert np.allclose(output.data, [5.0])
# Test multiple positive values
input_tensor = MockTensor([1.0, 2.5, 10.0])
output = relu(input_tensor)
assert np.allclose(output.data, [1.0, 2.5, 10.0])
def test_relu_negative_values(self):
"""Test ReLU zeros out negative values."""
relu = ReLU()
# Test single negative value
input_tensor = MockTensor([-3.0])
output = relu(input_tensor)
assert np.allclose(output.data, [0.0])
# Test multiple negative values
input_tensor = MockTensor([-1.0, -2.5, -10.0])
output = relu(input_tensor)
assert np.allclose(output.data, [0.0, 0.0, 0.0])
def test_relu_mixed_values(self):
"""Test ReLU with mixed positive and negative values."""
relu = ReLU()
input_tensor = MockTensor([-2.0, 0.0, 3.0, -1.5, 4.5])
output = relu(input_tensor)
expected = [0.0, 0.0, 3.0, 0.0, 4.5]
assert np.allclose(output.data, expected)
def test_relu_zero_value(self):
"""Test ReLU behavior at zero (should return zero)."""
relu = ReLU()
input_tensor = MockTensor([0.0])
output = relu(input_tensor)
assert np.allclose(output.data, [0.0])
def test_relu_2d_input(self):
"""Test ReLU with 2D input (matrices)."""
relu = ReLU()
input_data = np.array([[-1.0, 2.0], [3.0, -4.0]])
input_tensor = MockTensor(input_data)
output = relu(input_tensor)
expected = np.array([[0.0, 2.0], [3.0, 0.0]])
assert np.allclose(output.data, expected)
def test_relu_large_values(self):
"""Test ReLU with very large values."""
relu = ReLU()
input_tensor = MockTensor([1000.0, -1000.0])
output = relu(input_tensor)
expected = [1000.0, 0.0]
assert np.allclose(output.data, expected)
class TestSigmoidActivation:
"""Test Sigmoid activation function with mock tensors."""
def test_sigmoid_initialization(self):
"""Test Sigmoid can be initialized without parameters."""
sigmoid = Sigmoid()
assert sigmoid is not None
assert hasattr(sigmoid, 'forward') or hasattr(sigmoid, '__call__')
def test_sigmoid_zero_input(self):
"""Test Sigmoid at zero (should return 0.5)."""
sigmoid = Sigmoid()
input_tensor = MockTensor([0.0])
output = sigmoid(input_tensor)
assert np.allclose(output.data, [0.5], atol=1e-6)
def test_sigmoid_positive_values(self):
"""Test Sigmoid with positive values (should be > 0.5)."""
sigmoid = Sigmoid()
input_tensor = MockTensor([1.0, 2.0, 5.0])
output = sigmoid(input_tensor)
# All outputs should be > 0.5
assert np.all(output.data > 0.5)
# All outputs should be < 1.0
assert np.all(output.data < 1.0)
# Larger inputs should give larger outputs
assert output.data[0] < output.data[1] < output.data[2]
def test_sigmoid_negative_values(self):
"""Test Sigmoid with negative values (should be < 0.5)."""
sigmoid = Sigmoid()
input_tensor = MockTensor([-1.0, -2.0, -5.0])
output = sigmoid(input_tensor)
# All outputs should be < 0.5
assert np.all(output.data < 0.5)
# All outputs should be > 0.0
assert np.all(output.data > 0.0)
# More negative inputs should give smaller outputs
assert output.data[0] > output.data[1] > output.data[2]
def test_sigmoid_symmetry(self):
"""Test Sigmoid symmetry: sigmoid(x) + sigmoid(-x) = 1."""
sigmoid = Sigmoid()
x_values = [1.0, 2.0, 3.0]
pos_input = MockTensor(x_values)
neg_input = MockTensor([-x for x in x_values])
pos_output = sigmoid(pos_input)
neg_output = sigmoid(neg_input)
# sigmoid(x) + sigmoid(-x) should equal 1
sum_output = pos_output.data + neg_output.data
assert np.allclose(sum_output, [1.0, 1.0, 1.0], atol=1e-6)
def test_sigmoid_extreme_values(self):
"""Test Sigmoid with extreme values."""
sigmoid = Sigmoid()
# Very large positive value should approach 1
large_pos = MockTensor([100.0])
output_pos = sigmoid(large_pos)
assert np.allclose(output_pos.data, [1.0], atol=1e-6)
# Very large negative value should approach 0
large_neg = MockTensor([-100.0])
output_neg = sigmoid(large_neg)
assert np.allclose(output_neg.data, [0.0], atol=1e-6)
def test_sigmoid_2d_input(self):
"""Test Sigmoid with 2D input."""
sigmoid = Sigmoid()
input_data = np.array([[0.0, 1.0], [-1.0, 2.0]])
input_tensor = MockTensor(input_data)
output = sigmoid(input_tensor)
# Check that output has correct shape
assert output.shape == (2, 2)
# Check that all values are in (0, 1)
assert np.all(output.data > 0.0)
assert np.all(output.data < 1.0)
class TestTanhActivation:
"""Test Tanh activation function with mock tensors."""
def test_tanh_initialization(self):
"""Test Tanh can be initialized without parameters."""
tanh = Tanh()
assert tanh is not None
assert hasattr(tanh, 'forward') or hasattr(tanh, '__call__')
def test_tanh_zero_input(self):
"""Test Tanh at zero (should return 0)."""
tanh = Tanh()
input_tensor = MockTensor([0.0])
output = tanh(input_tensor)
assert np.allclose(output.data, [0.0], atol=1e-6)
def test_tanh_positive_values(self):
"""Test Tanh with positive values (should be in (0, 1))."""
tanh = Tanh()
input_tensor = MockTensor([0.5, 1.0, 2.0])
output = tanh(input_tensor)
# All outputs should be > 0
assert np.all(output.data > 0.0)
# All outputs should be < 1
assert np.all(output.data < 1.0)
# Larger inputs should give larger outputs
assert output.data[0] < output.data[1] < output.data[2]
def test_tanh_negative_values(self):
"""Test Tanh with negative values (should be in (-1, 0))."""
tanh = Tanh()
input_tensor = MockTensor([-0.5, -1.0, -2.0])
output = tanh(input_tensor)
# All outputs should be < 0
assert np.all(output.data < 0.0)
# All outputs should be > -1
assert np.all(output.data > -1.0)
# More negative inputs should give more negative outputs
assert output.data[0] > output.data[1] > output.data[2]
def test_tanh_antisymmetry(self):
"""Test Tanh antisymmetry: tanh(-x) = -tanh(x)."""
tanh = Tanh()
x_values = [1.0, 2.0, 3.0]
pos_input = MockTensor(x_values)
neg_input = MockTensor([-x for x in x_values])
pos_output = tanh(pos_input)
neg_output = tanh(neg_input)
# tanh(-x) should equal -tanh(x)
assert np.allclose(neg_output.data, -pos_output.data, atol=1e-6)
def test_tanh_extreme_values(self):
"""Test Tanh with extreme values."""
tanh = Tanh()
# Very large positive value should approach 1
large_pos = MockTensor([100.0])
output_pos = tanh(large_pos)
assert np.allclose(output_pos.data, [1.0], atol=1e-6)
# Very large negative value should approach -1
large_neg = MockTensor([-100.0])
output_neg = tanh(large_neg)
assert np.allclose(output_neg.data, [-1.0], atol=1e-6)
def test_tanh_2d_input(self):
"""Test Tanh with 2D input."""
tanh = Tanh()
input_data = np.array([[0.0, 1.0], [-1.0, 2.0]])
input_tensor = MockTensor(input_data)
output = tanh(input_tensor)
# Check that output has correct shape
assert output.shape == (2, 2)
# Check that all values are in (-1, 1)
assert np.all(output.data > -1.0)
assert np.all(output.data < 1.0)
class TestSoftmaxActivation:
"""Test Softmax activation function with mock tensors."""
def test_softmax_initialization(self):
"""Test Softmax can be initialized without parameters."""
softmax = Softmax()
assert softmax is not None
assert hasattr(softmax, 'forward') or hasattr(softmax, '__call__')
def test_softmax_probability_distribution(self):
"""Test Softmax produces valid probability distribution."""
softmax = Softmax()
input_tensor = MockTensor([1.0, 2.0, 3.0])
output = softmax(input_tensor)
# All outputs should be positive
assert np.all(output.data > 0.0)
# All outputs should be less than 1
assert np.all(output.data < 1.0)
# Outputs should sum to 1
assert np.allclose(np.sum(output.data), 1.0, atol=1e-6)
def test_softmax_uniform_input(self):
"""Test Softmax with uniform input (should give uniform distribution)."""
softmax = Softmax()
input_tensor = MockTensor([2.0, 2.0, 2.0])
output = softmax(input_tensor)
# Should be uniform distribution
expected = 1.0 / 3.0
assert np.allclose(output.data, [expected, expected, expected], atol=1e-6)
def test_softmax_max_element(self):
"""Test Softmax emphasizes maximum element."""
softmax = Softmax()
input_tensor = MockTensor([1.0, 5.0, 2.0])
output = softmax(input_tensor)
# Maximum input should correspond to maximum output
max_input_idx = np.argmax([1.0, 5.0, 2.0])
max_output_idx = np.argmax(output.data)
assert max_input_idx == max_output_idx
# Maximum output should be significantly larger than others
assert output.data[max_output_idx] > 0.8 # Should be dominant
def test_softmax_numerical_stability(self):
"""Test Softmax numerical stability with large values."""
softmax = Softmax()
# Large values that could cause overflow in naive implementation
input_tensor = MockTensor([1000.0, 1001.0, 999.0])
output = softmax(input_tensor)
# Should still be valid probability distribution
assert np.all(output.data > 0.0)
assert np.all(output.data < 1.0)
assert np.allclose(np.sum(output.data), 1.0, atol=1e-6)
# Maximum element should dominate
max_idx = np.argmax(output.data)
assert max_idx == 1 # 1001.0 is the maximum
def test_softmax_2d_input(self):
"""Test Softmax with 2D input (batch processing)."""
softmax = Softmax()
# Test with 2D input where each row is a sample
input_data = np.array([[1.0, 2.0, 3.0], [3.0, 1.0, 2.0]])
input_tensor = MockTensor(input_data)
output = softmax(input_tensor)
# Check that output has correct shape
assert output.shape == (2, 3)
# Each row should sum to 1
row_sums = np.sum(output.data, axis=1)
assert np.allclose(row_sums, [1.0, 1.0], atol=1e-6)
# All values should be positive
assert np.all(output.data > 0.0)
def test_softmax_single_element(self):
"""Test Softmax with single element (should return 1.0)."""
softmax = Softmax()
input_tensor = MockTensor([5.0])
output = softmax(input_tensor)
# Single element should have probability 1.0
assert np.allclose(output.data, [1.0], atol=1e-6)
class TestActivationIntegration:
"""Test integration between different activation functions."""
def test_activation_chaining(self):
"""Test chaining different activation functions."""
# Create activations
relu = ReLU()
sigmoid = Sigmoid()
tanh = Tanh()
# Test chaining: input -> ReLU -> Sigmoid
input_tensor = MockTensor([-1.0, 0.0, 1.0, 2.0])
# Apply ReLU first
relu_output = relu(input_tensor)
expected_relu = [0.0, 0.0, 1.0, 2.0]
assert np.allclose(relu_output.data, expected_relu)
# Apply Sigmoid to ReLU output
sigmoid_output = sigmoid(relu_output)
# Should be valid sigmoid values
assert np.all(sigmoid_output.data > 0.0)
assert np.all(sigmoid_output.data < 1.0)
# First two should be 0.5 (sigmoid of 0)
assert np.allclose(sigmoid_output.data[:2], [0.5, 0.5], atol=1e-6)
def test_activation_consistency(self):
"""Test that activations are consistent across calls."""
activations = [ReLU(), Sigmoid(), Tanh(), Softmax()]
input_tensor = MockTensor([1.0, 2.0, 3.0])
for activation in activations:
# Apply activation twice
output1 = activation(input_tensor)
output2 = activation(input_tensor)
# Should get identical results
assert np.allclose(output1.data, output2.data, atol=1e-10)
def test_activation_shapes(self):
"""Test that all activations preserve input shapes."""
activations = [ReLU(), Sigmoid(), Tanh(), Softmax()]
test_shapes = [
[5], # 1D
[3, 4], # 2D
[2, 3, 4], # 3D
]
for shape in test_shapes:
input_data = np.random.randn(*shape)
input_tensor = MockTensor(input_data)
for activation in activations:
output = activation(input_tensor)
assert output.shape == input_tensor.shape, f"Shape mismatch for {activation.__class__.__name__}"
class TestActivationEdgeCases:
"""Test edge cases and error conditions for activation functions."""
def test_empty_input(self):
"""Test activations with empty input."""
activations = [ReLU(), Sigmoid(), Tanh(), Softmax()]
empty_tensor = MockTensor([])
for activation in activations:
output = activation(empty_tensor)
assert output.shape == (0,), f"Empty input failed for {activation.__class__.__name__}"
def test_very_small_values(self):
"""Test activations with very small values."""
activations = [ReLU(), Sigmoid(), Tanh()] # Exclude Softmax for this test
small_tensor = MockTensor([1e-10, -1e-10, 1e-15])
for activation in activations:
output = activation(small_tensor)
# Should not crash and should produce finite values
assert np.all(np.isfinite(output.data)), f"Small values failed for {activation.__class__.__name__}"
def test_inf_and_nan_handling(self):
"""Test activation behavior with inf and nan values."""
activations = [ReLU(), Sigmoid(), Tanh(), Softmax()]
# Test with inf values
inf_tensor = MockTensor([np.inf, -np.inf, 0.0])
for activation in activations:
try:
output = activation(inf_tensor)
# Should either handle gracefully or raise appropriate exception
# At minimum, should not crash the program
assert output is not None
except (ValueError, RuntimeWarning):
# Acceptable to raise warnings/errors for inf values
pass
if __name__ == "__main__":
# Run tests if executed directly
pytest.main([__file__, "-v"])

View File

@@ -1,467 +0,0 @@
"""
Mock-based module tests for CNN module.
This test file uses simple mocks to avoid cross-module dependencies while thoroughly
testing the CNN module functionality. The MockTensor class provides a minimal
interface that matches expected behavior without requiring actual implementations.
Test Philosophy:
- Use simple, visible mocks instead of complex mocking frameworks
- Test interface contracts and behavior, not implementation details
- Avoid dependency cascade where CNN tests fail due to tensor bugs
- Focus on convolution operations, Conv2D layers, and flatten functionality
- Ensure educational value with clear test structure
"""
import pytest
import numpy as np
import sys
import os
# Add the module source directory to the path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'modules', 'source', '05_cnn'))
from cnn_dev import Conv2D, conv2d_naive, flatten
class MockTensor:
"""
Simple mock tensor for testing CNN operations without tensor dependencies.
This mock provides just enough functionality to test CNN operations
without requiring the full Tensor implementation.
"""
def __init__(self, data):
"""Initialize with numpy array data."""
if isinstance(data, (list, tuple)):
self.data = np.array(data, dtype=np.float32)
elif isinstance(data, np.ndarray):
self.data = data.astype(np.float32)
else:
self.data = np.array([data], dtype=np.float32)
@property
def shape(self):
"""Return shape of the underlying data."""
return self.data.shape
def __repr__(self):
return f"MockTensor({self.data})"
def __eq__(self, other):
"""Check equality with another MockTensor."""
if isinstance(other, MockTensor):
return np.allclose(self.data, other.data)
return False
class TestConv2DNaive:
"""Test conv2d_naive function with numpy arrays."""
def test_conv2d_naive_basic(self):
"""Test basic convolution operation."""
# Simple 3x3 input with 2x2 kernel
input_array = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.float32)
kernel_array = np.array([[1, 0], [0, 1]], dtype=np.float32)
result = conv2d_naive(input_array, kernel_array)
expected = np.array([[6, 8], [12, 14]], dtype=np.float32)
assert np.allclose(result, expected)
assert result.shape == (2, 2)
def test_conv2d_naive_edge_detection(self):
"""Test convolution with edge detection kernel."""
# Create a simple edge pattern
input_array = np.array([[0, 0, 1], [0, 0, 1], [0, 0, 1]], dtype=np.float32)
edge_kernel = np.array([[-1, 1], [-1, 1]], dtype=np.float32)
result = conv2d_naive(input_array, edge_kernel)
# Should detect vertical edge
assert result.shape == (2, 2)
assert result[0, 0] == 0 # No edge at left
assert result[0, 1] > 0 # Edge detected at right
def test_conv2d_naive_different_sizes(self):
"""Test convolution with different kernel sizes."""
# Test with 5x5 input and 3x3 kernel
input_5x5 = np.random.randn(5, 5).astype(np.float32)
kernel_3x3 = np.random.randn(3, 3).astype(np.float32)
result = conv2d_naive(input_5x5, kernel_3x3)
expected_shape = (3, 3) # 5-3+1 = 3
assert result.shape == expected_shape
assert result.dtype == np.float32
def test_conv2d_naive_identity_kernel(self):
"""Test convolution with identity-like kernel."""
input_array = np.array([[1, 2], [3, 4]], dtype=np.float32)
identity_kernel = np.array([[1]], dtype=np.float32)
result = conv2d_naive(input_array, identity_kernel)
expected = np.array([[1, 2], [3, 4]], dtype=np.float32)
assert np.allclose(result, expected)
def test_conv2d_naive_zero_kernel(self):
"""Test convolution with zero kernel."""
input_array = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32)
zero_kernel = np.array([[0, 0], [0, 0]], dtype=np.float32)
result = conv2d_naive(input_array, zero_kernel)
expected = np.zeros((1, 2), dtype=np.float32)
assert np.allclose(result, expected)
def test_conv2d_naive_large_kernel(self):
"""Test convolution with large kernel."""
input_array = np.ones((10, 10), dtype=np.float32)
large_kernel = np.ones((5, 5), dtype=np.float32)
result = conv2d_naive(input_array, large_kernel)
expected_shape = (6, 6) # 10-5+1 = 6
assert result.shape == expected_shape
# All ones input with all ones kernel should give 25 (5*5) everywhere
assert np.allclose(result, 25.0)
class TestConv2DLayer:
"""Test Conv2D layer class with mock tensors."""
def test_conv2d_layer_initialization(self):
"""Test Conv2D layer initialization."""
layer = Conv2D(kernel_size=(3, 3))
assert layer.kernel_size == (3, 3)
assert layer.kernel.shape == (3, 3)
assert layer.kernel.dtype == np.float32
assert not np.allclose(layer.kernel, 0) # Should be randomly initialized
def test_conv2d_layer_different_sizes(self):
"""Test Conv2D layer with different kernel sizes."""
sizes = [(2, 2), (3, 3), (5, 5), (1, 1)]
for size in sizes:
layer = Conv2D(kernel_size=size)
assert layer.kernel_size == size
assert layer.kernel.shape == size
def test_conv2d_layer_forward_pass(self):
"""Test Conv2D layer forward pass."""
layer = Conv2D(kernel_size=(2, 2))
# Test with 3x3 input
input_tensor = MockTensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
output = layer(input_tensor)
assert isinstance(output, MockTensor)
assert output.shape == (2, 2) # 3-2+1 = 2
def test_conv2d_layer_batch_processing(self):
"""Test Conv2D layer with batch input."""
layer = Conv2D(kernel_size=(2, 2))
# Note: Current implementation might not support batches
# This test checks if it handles single images correctly
input_tensor = MockTensor(np.random.randn(5, 5))
output = layer(input_tensor)
assert isinstance(output, MockTensor)
assert output.shape == (4, 4) # 5-2+1 = 4
def test_conv2d_layer_kernel_consistency(self):
"""Test that Conv2D layer uses consistent kernel."""
layer = Conv2D(kernel_size=(2, 2))
# Store original kernel
original_kernel = layer.kernel.copy()
# Forward pass shouldn't change kernel
input_tensor = MockTensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
output = layer(input_tensor)
assert np.allclose(layer.kernel, original_kernel)
def test_conv2d_layer_different_inputs(self):
"""Test Conv2D layer with different input sizes."""
layer = Conv2D(kernel_size=(3, 3))
input_sizes = [(5, 5), (8, 8), (10, 10)]
for h, w in input_sizes:
input_tensor = MockTensor(np.random.randn(h, w))
output = layer(input_tensor)
expected_h, expected_w = h - 3 + 1, w - 3 + 1
assert output.shape == (expected_h, expected_w)
def test_conv2d_layer_callable(self):
"""Test that Conv2D layer is callable."""
layer = Conv2D(kernel_size=(2, 2))
# Should be callable both ways
input_tensor = MockTensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
output1 = layer(input_tensor)
output2 = layer.forward(input_tensor)
# Both should work and produce same output
assert isinstance(output1, MockTensor)
assert isinstance(output2, MockTensor)
assert output1.shape == output2.shape
class TestFlattenFunction:
"""Test flatten function with mock tensors."""
def test_flatten_2d_tensor(self):
"""Test flattening 2D tensor."""
input_tensor = MockTensor([[1, 2], [3, 4]])
flattened = flatten(input_tensor)
assert isinstance(flattened, MockTensor)
assert flattened.shape == (1, 4)
expected = np.array([[1, 2, 3, 4]], dtype=np.float32)
assert np.allclose(flattened.data, expected)
def test_flatten_3x3_tensor(self):
"""Test flattening 3x3 tensor."""
input_tensor = MockTensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
flattened = flatten(input_tensor)
assert flattened.shape == (1, 9)
expected = np.array([[1, 2, 3, 4, 5, 6, 7, 8, 9]], dtype=np.float32)
assert np.allclose(flattened.data, expected)
def test_flatten_different_shapes(self):
"""Test flatten with different tensor shapes."""
test_shapes = [
(2, 3), # 2x3 -> (1, 6)
(4, 4), # 4x4 -> (1, 16)
(1, 5), # 1x5 -> (1, 5)
(6, 1), # 6x1 -> (1, 6)
]
for h, w in test_shapes:
input_data = np.random.randn(h, w)
input_tensor = MockTensor(input_data)
flattened = flatten(input_tensor)
assert flattened.shape == (1, h * w)
assert np.allclose(flattened.data.flatten(), input_data.flatten())
def test_flatten_preserves_order(self):
"""Test that flatten preserves row-major order."""
input_tensor = MockTensor([[1, 2, 3], [4, 5, 6]])
flattened = flatten(input_tensor)
expected = np.array([[1, 2, 3, 4, 5, 6]], dtype=np.float32)
assert np.allclose(flattened.data, expected)
def test_flatten_single_element(self):
"""Test flatten with single element tensor."""
input_tensor = MockTensor([[5]])
flattened = flatten(input_tensor)
assert flattened.shape == (1, 1)
assert np.allclose(flattened.data, [[5]])
def test_flatten_batch_dimension(self):
"""Test that flatten adds batch dimension correctly."""
input_tensor = MockTensor([[1, 2], [3, 4]])
flattened = flatten(input_tensor)
# Should have batch dimension of 1
assert flattened.shape[0] == 1
assert len(flattened.shape) == 2
class TestCNNIntegration:
"""Test integration between CNN components."""
def test_conv2d_to_flatten_pipeline(self):
"""Test pipeline from Conv2D to flatten."""
# Create Conv2D layer
conv_layer = Conv2D(kernel_size=(2, 2))
# Apply convolution
input_tensor = MockTensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
conv_output = conv_layer(input_tensor)
# Flatten the result
flattened = flatten(conv_output)
# Should have correct shapes
assert conv_output.shape == (2, 2)
assert flattened.shape == (1, 4)
def test_multiple_conv2d_layers(self):
"""Test multiple Conv2D layers in sequence."""
# Create two Conv2D layers
conv1 = Conv2D(kernel_size=(2, 2))
conv2 = Conv2D(kernel_size=(2, 2))
# Apply them in sequence
input_tensor = MockTensor(np.random.randn(5, 5))
# First convolution: 5x5 -> 4x4
h1 = conv1(input_tensor)
assert h1.shape == (4, 4)
# Second convolution: 4x4 -> 3x3
h2 = conv2(h1)
assert h2.shape == (3, 3)
def test_conv2d_with_different_kernels(self):
"""Test Conv2D layers with different kernel sizes."""
input_tensor = MockTensor(np.random.randn(10, 10))
# Test different kernel sizes
kernel_sizes = [(3, 3), (5, 5), (7, 7)]
for kernel_size in kernel_sizes:
conv_layer = Conv2D(kernel_size=kernel_size)
output = conv_layer(input_tensor)
expected_h = 10 - kernel_size[0] + 1
expected_w = 10 - kernel_size[1] + 1
assert output.shape == (expected_h, expected_w)
def test_cnn_feature_extraction_pipeline(self):
"""Test complete CNN feature extraction pipeline."""
# Simulate image classification pipeline
input_image = MockTensor(np.random.randn(8, 8))
# Feature extraction
conv1 = Conv2D(kernel_size=(3, 3)) # 8x8 -> 6x6
features1 = conv1(input_image)
conv2 = Conv2D(kernel_size=(2, 2)) # 6x6 -> 5x5
features2 = conv2(features1)
# Flatten for dense layer
flattened = flatten(features2) # 5x5 -> (1, 25)
# Verify pipeline
assert features1.shape == (6, 6)
assert features2.shape == (5, 5)
assert flattened.shape == (1, 25)
class TestCNNEdgeCases:
"""Test edge cases and error conditions for CNN operations."""
def test_conv2d_minimal_input(self):
"""Test Conv2D with minimal input size."""
# Test with input same size as kernel
layer = Conv2D(kernel_size=(2, 2))
input_tensor = MockTensor([[1, 2], [3, 4]])
output = layer(input_tensor)
assert output.shape == (1, 1) # 2-2+1 = 1
def test_conv2d_large_kernel(self):
"""Test Conv2D with large kernel relative to input."""
layer = Conv2D(kernel_size=(3, 3))
input_tensor = MockTensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
output = layer(input_tensor)
assert output.shape == (1, 1) # 3-3+1 = 1
def test_flatten_edge_shapes(self):
"""Test flatten with edge case shapes."""
# Very wide tensor
wide_tensor = MockTensor([[1, 2, 3, 4, 5, 6, 7, 8]])
flattened = flatten(wide_tensor)
assert flattened.shape == (1, 8)
# Very tall tensor
tall_tensor = MockTensor([[1], [2], [3], [4]])
flattened = flatten(tall_tensor)
assert flattened.shape == (1, 4)
def test_conv2d_numerical_stability(self):
"""Test Conv2D with extreme values."""
layer = Conv2D(kernel_size=(2, 2))
# Test with very large values
large_input = MockTensor([[1000, 2000], [3000, 4000]])
output = layer(large_input)
assert np.all(np.isfinite(output.data))
# Test with very small values
small_input = MockTensor([[1e-6, 2e-6], [3e-6, 4e-6]])
output = layer(small_input)
assert np.all(np.isfinite(output.data))
def test_conv2d_zero_input(self):
"""Test Conv2D with zero input."""
layer = Conv2D(kernel_size=(2, 2))
zero_input = MockTensor([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
output = layer(zero_input)
assert output.shape == (2, 2)
# Output should be finite (kernel is random, not zero)
assert np.all(np.isfinite(output.data))
class TestCNNPerformance:
"""Test CNN performance characteristics."""
def test_conv2d_consistency(self):
"""Test Conv2D produces consistent results."""
layer = Conv2D(kernel_size=(3, 3))
input_tensor = MockTensor(np.random.randn(5, 5))
# Multiple forward passes should be consistent
output1 = layer(input_tensor)
output2 = layer(input_tensor)
# Should be identical (deterministic)
assert np.allclose(output1.data, output2.data)
def test_conv2d_different_instances(self):
"""Test different Conv2D instances have different kernels."""
layer1 = Conv2D(kernel_size=(3, 3))
layer2 = Conv2D(kernel_size=(3, 3))
# Should have different random kernels
assert not np.allclose(layer1.kernel, layer2.kernel)
def test_flatten_efficiency(self):
"""Test flatten operation efficiency."""
# Test with different sizes
sizes = [(5, 5), (10, 10), (20, 20)]
for h, w in sizes:
input_tensor = MockTensor(np.random.randn(h, w))
flattened = flatten(input_tensor)
# Should preserve all data
assert flattened.shape == (1, h * w)
assert np.allclose(flattened.data.flatten(), input_tensor.data.flatten())
def test_conv2d_scalability(self):
"""Test Conv2D with different scales."""
kernel_sizes = [(2, 2), (3, 3), (5, 5)]
input_sizes = [(10, 10), (20, 20), (50, 50)]
for kernel_size in kernel_sizes:
for input_size in input_sizes:
if input_size[0] >= kernel_size[0] and input_size[1] >= kernel_size[1]:
layer = Conv2D(kernel_size=kernel_size)
input_tensor = MockTensor(np.random.randn(*input_size))
output = layer(input_tensor)
expected_h = input_size[0] - kernel_size[0] + 1
expected_w = input_size[1] - kernel_size[1] + 1
assert output.shape == (expected_h, expected_w)
if __name__ == "__main__":
# Run tests if executed directly
pytest.main([__file__, "-v"])

View File

@@ -1,577 +0,0 @@
"""
Mock-based module tests for DataLoader module.
This test file uses simple mocks to avoid cross-module dependencies while thoroughly
testing the DataLoader module functionality. The MockTensor class provides a minimal
interface that matches expected behavior without requiring actual implementations.
Test Philosophy:
- Use simple, visible mocks instead of complex mocking frameworks
- Test interface contracts and behavior, not implementation details
- Avoid dependency cascade where dataloader tests fail due to tensor bugs
- Focus on Dataset interface, DataLoader functionality, and data pipeline patterns
- Ensure educational value with clear test structure
"""
import pytest
import numpy as np
import sys
import os
# Add the module source directory to the path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'modules', 'source', '06_dataloader'))
from dataloader_dev import Dataset, DataLoader, SimpleDataset
class MockTensor:
"""
Simple mock tensor for testing dataloader operations without tensor dependencies.
This mock provides just enough functionality to test data loading operations
without requiring the full Tensor implementation.
"""
def __init__(self, data):
"""Initialize with numpy array data."""
if isinstance(data, (list, tuple)):
self.data = np.array(data, dtype=np.float32)
elif isinstance(data, np.ndarray):
self.data = data.astype(np.float32)
else:
self.data = np.array(data, dtype=np.float32)
@property
def shape(self):
"""Return shape of the underlying data."""
return self.data.shape
def __repr__(self):
return f"MockTensor({self.data})"
def __eq__(self, other):
"""Check equality with another MockTensor."""
if isinstance(other, MockTensor):
return np.allclose(self.data, other.data)
return False
class MockDataset(Dataset):
"""
Simple mock dataset for testing without cross-module dependencies.
This mock implements the Dataset interface with predictable, testable behavior.
"""
def __init__(self, size=10, num_features=3, num_classes=2):
"""Initialize mock dataset with synthetic data."""
self.size = size
self.num_features = num_features
self.num_classes = num_classes
# Generate predictable synthetic data
np.random.seed(42) # For reproducible tests
self.data = np.random.randn(size, num_features).astype(np.float32)
self.labels = np.random.randint(0, num_classes, size=size)
def __getitem__(self, index):
"""Get item by index."""
if index < 0 or index >= self.size:
raise IndexError(f"Index {index} out of range for dataset of size {self.size}")
data = MockTensor(self.data[index])
label = MockTensor(self.labels[index])
return data, label
def __len__(self):
"""Get dataset size."""
return self.size
def get_num_classes(self):
"""Get number of classes."""
return self.num_classes
class TestDatasetInterface:
"""Test Dataset abstract base class and interface."""
def test_dataset_abstract_methods(self):
"""Test that Dataset abstract methods raise NotImplementedError."""
dataset = Dataset()
with pytest.raises(NotImplementedError):
dataset[0]
with pytest.raises(NotImplementedError):
len(dataset)
with pytest.raises(NotImplementedError):
dataset.get_num_classes()
def test_dataset_get_sample_shape(self):
"""Test Dataset get_sample_shape method."""
mock_dataset = MockDataset(size=5, num_features=4, num_classes=3)
sample_shape = mock_dataset.get_sample_shape()
assert sample_shape == (4,) # Should match num_features
def test_mock_dataset_basic_functionality(self):
"""Test MockDataset basic functionality."""
dataset = MockDataset(size=10, num_features=5, num_classes=3)
# Test length
assert len(dataset) == 10
# Test get_num_classes
assert dataset.get_num_classes() == 3
# Test item access
data, label = dataset[0]
assert isinstance(data, MockTensor)
assert isinstance(label, MockTensor)
assert data.shape == (5,)
assert label.shape == ()
def test_mock_dataset_index_bounds(self):
"""Test MockDataset index bounds checking."""
dataset = MockDataset(size=5)
# Valid indices should work
for i in range(5):
data, label = dataset[i]
assert isinstance(data, MockTensor)
assert isinstance(label, MockTensor)
# Invalid indices should raise IndexError
with pytest.raises(IndexError):
dataset[5]
with pytest.raises(IndexError):
dataset[-1] # Negative indices not supported
def test_mock_dataset_consistency(self):
"""Test MockDataset produces consistent results."""
dataset = MockDataset(size=5, num_features=3, num_classes=2)
# Multiple accesses should return same data
data1, label1 = dataset[0]
data2, label2 = dataset[0]
assert np.allclose(data1.data, data2.data)
assert np.allclose(label1.data, label2.data)
def test_mock_dataset_different_configurations(self):
"""Test MockDataset with different configurations."""
configs = [
(5, 2, 2), # Small dataset
(100, 10, 5), # Medium dataset
(1000, 50, 10) # Large dataset
]
for size, num_features, num_classes in configs:
dataset = MockDataset(size=size, num_features=num_features, num_classes=num_classes)
assert len(dataset) == size
assert dataset.get_num_classes() == num_classes
data, label = dataset[0]
assert data.shape == (num_features,)
class TestDataLoaderBasic:
"""Test DataLoader basic functionality."""
def test_dataloader_initialization(self):
"""Test DataLoader initialization."""
dataset = MockDataset(size=10)
dataloader = DataLoader(dataset, batch_size=4, shuffle=True)
assert dataloader.dataset is dataset
assert dataloader.batch_size == 4
assert dataloader.shuffle == True
def test_dataloader_default_parameters(self):
"""Test DataLoader with default parameters."""
dataset = MockDataset(size=10)
dataloader = DataLoader(dataset)
assert dataloader.batch_size == 32 # Default batch size
assert dataloader.shuffle == True # Default shuffle
def test_dataloader_length_calculation(self):
"""Test DataLoader length calculation (number of batches)."""
dataset = MockDataset(size=10)
# Test different batch sizes
test_cases = [
(10, 2, 5), # 10 samples, batch size 2 -> 5 batches
(10, 3, 4), # 10 samples, batch size 3 -> 4 batches (ceiling division)
(10, 5, 2), # 10 samples, batch size 5 -> 2 batches
(10, 10, 1), # 10 samples, batch size 10 -> 1 batch
(10, 15, 1), # 10 samples, batch size 15 -> 1 batch
]
for dataset_size, batch_size, expected_batches in test_cases:
dataset = MockDataset(size=dataset_size)
dataloader = DataLoader(dataset, batch_size=batch_size)
assert len(dataloader) == expected_batches
def test_dataloader_iteration_basic(self):
"""Test basic DataLoader iteration."""
dataset = MockDataset(size=8, num_features=3, num_classes=2)
dataloader = DataLoader(dataset, batch_size=3, shuffle=False)
batches = list(dataloader)
# Should have 3 batches: [3, 3, 2] samples
assert len(batches) == 3
# Check batch shapes
batch_data, batch_labels = batches[0]
assert batch_data.shape == (3, 3) # 3 samples, 3 features each
assert batch_labels.shape == (3,) # 3 labels
# Check last batch (partial)
batch_data, batch_labels = batches[2]
assert batch_data.shape == (2, 3) # 2 samples, 3 features each
assert batch_labels.shape == (2,) # 2 labels
def test_dataloader_iteration_complete(self):
"""Test that DataLoader iteration covers all samples."""
dataset = MockDataset(size=10, num_features=4, num_classes=3)
dataloader = DataLoader(dataset, batch_size=3, shuffle=False)
total_samples = 0
all_batch_data = []
all_batch_labels = []
for batch_data, batch_labels in dataloader:
batch_size = batch_data.shape[0]
total_samples += batch_size
# Collect all data
all_batch_data.append(batch_data.data)
all_batch_labels.append(batch_labels.data)
# Should process all samples
assert total_samples == 10
# Should have 4 batches: [3, 3, 3, 1]
assert len(all_batch_data) == 4
assert all_batch_data[0].shape == (3, 4)
assert all_batch_data[1].shape == (3, 4)
assert all_batch_data[2].shape == (3, 4)
assert all_batch_data[3].shape == (1, 4)
class TestDataLoaderShuffling:
"""Test DataLoader shuffling functionality."""
def test_dataloader_no_shuffle(self):
"""Test DataLoader without shuffling."""
dataset = MockDataset(size=6, num_features=2, num_classes=2)
dataloader = DataLoader(dataset, batch_size=2, shuffle=False)
# Get first batch
batch_data, batch_labels = next(iter(dataloader))
# Should be samples 0 and 1
expected_data_0, expected_label_0 = dataset[0]
expected_data_1, expected_label_1 = dataset[1]
assert np.allclose(batch_data.data[0], expected_data_0.data)
assert np.allclose(batch_data.data[1], expected_data_1.data)
def test_dataloader_with_shuffle(self):
"""Test DataLoader with shuffling."""
dataset = MockDataset(size=10, num_features=3, num_classes=2)
# Create two dataloaders with different shuffle states
dataloader1 = DataLoader(dataset, batch_size=5, shuffle=True)
dataloader2 = DataLoader(dataset, batch_size=5, shuffle=True)
# Get first batches
batch1 = next(iter(dataloader1))
batch2 = next(iter(dataloader2))
# Should have same shapes
assert batch1[0].shape == batch2[0].shape
assert batch1[1].shape == batch2[1].shape
# Note: Due to randomness, batches might be different
# This is a basic test that shuffling doesn't break functionality
def test_dataloader_shuffle_consistency(self):
"""Test that DataLoader shuffling is consistent within an epoch."""
dataset = MockDataset(size=8, num_features=2, num_classes=2)
dataloader = DataLoader(dataset, batch_size=4, shuffle=True)
# Collect all data from one epoch
epoch_data = []
epoch_labels = []
for batch_data, batch_labels in dataloader:
epoch_data.append(batch_data.data)
epoch_labels.append(batch_labels.data)
# Should have processed all samples
total_samples = sum(data.shape[0] for data in epoch_data)
assert total_samples == 8
# All data should be accounted for
assert len(epoch_data) == 2 # 8 samples / 4 batch_size = 2 batches
class TestDataLoaderEdgeCases:
"""Test DataLoader edge cases and error conditions."""
def test_dataloader_empty_dataset(self):
"""Test DataLoader with empty dataset."""
dataset = MockDataset(size=0)
dataloader = DataLoader(dataset, batch_size=4)
# Should have 0 batches
assert len(dataloader) == 0
# Iteration should produce no batches
batches = list(dataloader)
assert len(batches) == 0
def test_dataloader_single_sample(self):
"""Test DataLoader with single sample."""
dataset = MockDataset(size=1, num_features=3, num_classes=2)
dataloader = DataLoader(dataset, batch_size=4)
# Should have 1 batch
assert len(dataloader) == 1
# Single batch should contain the one sample
batch_data, batch_labels = next(iter(dataloader))
assert batch_data.shape == (1, 3)
assert batch_labels.shape == (1,)
def test_dataloader_batch_size_larger_than_dataset(self):
"""Test DataLoader with batch size larger than dataset."""
dataset = MockDataset(size=5, num_features=4, num_classes=3)
dataloader = DataLoader(dataset, batch_size=10)
# Should have 1 batch
assert len(dataloader) == 1
# Batch should contain all samples
batch_data, batch_labels = next(iter(dataloader))
assert batch_data.shape == (5, 4)
assert batch_labels.shape == (5,)
def test_dataloader_batch_size_one(self):
"""Test DataLoader with batch size of 1."""
dataset = MockDataset(size=5, num_features=2, num_classes=2)
dataloader = DataLoader(dataset, batch_size=1)
# Should have 5 batches
assert len(dataloader) == 5
# Each batch should have 1 sample
for batch_data, batch_labels in dataloader:
assert batch_data.shape == (1, 2)
assert batch_labels.shape == (1,)
def test_dataloader_multiple_epochs(self):
"""Test DataLoader across multiple epochs."""
dataset = MockDataset(size=6, num_features=3, num_classes=2)
dataloader = DataLoader(dataset, batch_size=2, shuffle=False)
# Test 3 epochs
for epoch in range(3):
epoch_samples = 0
batch_count = 0
for batch_data, batch_labels in dataloader:
batch_count += 1
epoch_samples += batch_data.shape[0]
# Each epoch should process all samples
assert epoch_samples == 6
assert batch_count == 3 # 6 samples / 2 batch_size = 3 batches
class TestDataLoaderIntegration:
"""Test DataLoader integration with different dataset types."""
def test_dataloader_with_simple_dataset(self):
"""Test DataLoader with SimpleDataset."""
# Note: This test assumes SimpleDataset exists and works
try:
dataset = SimpleDataset(size=20, num_features=5, num_classes=3)
dataloader = DataLoader(dataset, batch_size=4, shuffle=True)
# Test basic functionality
assert len(dataloader) == 5 # 20 / 4 = 5 batches
# Test iteration
total_samples = 0
for batch_data, batch_labels in dataloader:
total_samples += batch_data.shape[0]
assert batch_data.shape[1] == 5 # num_features
assert total_samples == 20
except (ImportError, NameError):
# SimpleDataset might not be available in all test environments
pytest.skip("SimpleDataset not available")
def test_dataloader_with_custom_dataset(self):
"""Test DataLoader with custom dataset implementation."""
class CustomDataset(Dataset):
def __init__(self):
self.data = [(i, i % 2) for i in range(10)]
def __getitem__(self, index):
value, label = self.data[index]
return MockTensor([value]), MockTensor([label])
def __len__(self):
return len(self.data)
def get_num_classes(self):
return 2
dataset = CustomDataset()
dataloader = DataLoader(dataset, batch_size=3, shuffle=False)
# Test that it works with custom dataset
batches = list(dataloader)
assert len(batches) == 4 # 10 / 3 = 4 batches (ceiling division)
# Check first batch
batch_data, batch_labels = batches[0]
assert batch_data.shape == (3, 1)
assert batch_labels.shape == (3, 1)
def test_dataloader_different_data_types(self):
"""Test DataLoader with different data types."""
class MultiTypeDataset(Dataset):
def __init__(self):
self.samples = [
(np.array([1.0, 2.0]), 0),
(np.array([3.0, 4.0]), 1),
(np.array([5.0, 6.0]), 0),
(np.array([7.0, 8.0]), 1),
]
def __getitem__(self, index):
data, label = self.samples[index]
return MockTensor(data), MockTensor(label)
def __len__(self):
return len(self.samples)
def get_num_classes(self):
return 2
dataset = MultiTypeDataset()
dataloader = DataLoader(dataset, batch_size=2, shuffle=False)
# Test batching different data types
batch_data, batch_labels = next(iter(dataloader))
assert batch_data.shape == (2, 2)
assert batch_labels.shape == (2,)
class TestDataLoaderPerformance:
"""Test DataLoader performance characteristics."""
def test_dataloader_functionality(self):
"""Test DataLoader functionality with realistic datasets."""
# Create reasonable dataset for educational context
dataset = MockDataset(size=100, num_features=10, num_classes=5)
dataloader = DataLoader(dataset, batch_size=16, shuffle=True)
# Should iterate correctly
batch_count = 0
for batch_data, batch_labels in dataloader:
batch_count += 1
assert batch_data.shape[1] == 10
assert batch_labels.shape[0] <= 16
# Process first few batches
if batch_count >= 3:
break
assert batch_count == 3
def test_dataloader_iteration_completeness(self):
"""Test DataLoader processes all samples correctly."""
dataset = MockDataset(size=100, num_features=10, num_classes=5)
dataloader = DataLoader(dataset, batch_size=10, shuffle=False)
# Should process all samples
total_samples = 0
for batch_data, batch_labels in dataloader:
total_samples += batch_data.shape[0]
# Should process all samples
assert total_samples == 100
def test_dataloader_different_scales(self):
"""Test DataLoader with different reasonable scales."""
sizes = [10, 50, 100]
batch_sizes = [1, 8, 16]
for size in sizes:
for batch_size in batch_sizes:
dataset = MockDataset(size=size, num_features=5, num_classes=3)
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
# Should handle different scales
expected_batches = (size + batch_size - 1) // batch_size
assert len(dataloader) == expected_batches
# Should iterate correctly
total_samples = 0
for batch_data, batch_labels in dataloader:
total_samples += batch_data.shape[0]
assert total_samples == size
class TestDataLoaderRobustness:
"""Test DataLoader robustness and error handling."""
def test_dataloader_with_invalid_batch_size(self):
"""Test DataLoader with invalid batch sizes."""
dataset = MockDataset(size=10)
# Zero batch size should raise error
with pytest.raises((ValueError, AssertionError)):
DataLoader(dataset, batch_size=0)
# Negative batch size should raise error
with pytest.raises((ValueError, AssertionError)):
DataLoader(dataset, batch_size=-1)
def test_dataloader_with_none_dataset(self):
"""Test DataLoader with None dataset."""
with pytest.raises((TypeError, AttributeError)):
DataLoader(None, batch_size=4)
def test_dataloader_iteration_consistency(self):
"""Test DataLoader iteration consistency."""
dataset = MockDataset(size=12, num_features=3, num_classes=2)
dataloader = DataLoader(dataset, batch_size=5, shuffle=False)
# Multiple iterations should be consistent
batches1 = list(dataloader)
batches2 = list(dataloader)
assert len(batches1) == len(batches2)
# Without shuffle, should be identical
for (batch1_data, batch1_labels), (batch2_data, batch2_labels) in zip(batches1, batches2):
assert np.allclose(batch1_data.data, batch2_data.data)
assert np.allclose(batch1_labels.data, batch2_labels.data)
if __name__ == "__main__":
# Run tests if executed directly
pytest.main([__file__, "-v"])

View File

@@ -1,364 +0,0 @@
"""
Comprehensive Layers Module Tests
Tests Dense layer functionality using simple mock objects.
Used for instructor grading and comprehensive validation.
This file demonstrates the mock-based testing approach where we use
simple, visible mocks instead of depending on other TinyTorch modules.
"""
import numpy as np
import pytest
# Simple Mock Objects - Visible and Educational
class MockTensor:
"""
Simple mock tensor for testing layers.
Shows exactly what interface the Dense layer expects:
- .data (numpy array): The actual numerical data
- .shape (tuple): Dimensions of the data
"""
def __init__(self, data):
self.data = np.array(data, dtype=np.float32)
self.shape = self.data.shape
def __repr__(self):
return f"MockTensor(shape={self.shape})"
# Import the student's implementation
# Note: In a real setup, this would import from the student's module
try:
from modules.source.layers.layers_dev import Dense, matmul_naive
except ImportError:
# Fallback for different import paths
try:
from tinytorch.core.layers import Dense, matmul_naive
except ImportError:
# Skip tests if module not found
pytest.skip("Layers module not found", allow_module_level=True)
class TestMatrixMultiplication:
"""Comprehensive tests for matrix multiplication implementation."""
def test_basic_2x2_multiplication(self):
"""Test basic 2x2 matrix multiplication."""
A = np.array([[1, 2], [3, 4]], dtype=np.float32)
B = np.array([[5, 6], [7, 8]], dtype=np.float32)
result = matmul_naive(A, B)
expected = np.array([[19, 22], [43, 50]], dtype=np.float32)
np.testing.assert_array_almost_equal(result, expected)
assert result.shape == (2, 2)
def test_different_shapes(self):
"""Test matrix multiplication with different shapes."""
# Test 1x3 × 3x1 = 1x1
A = np.array([[1, 2, 3]], dtype=np.float32)
B = np.array([[4], [5], [6]], dtype=np.float32)
result = matmul_naive(A, B)
expected = np.array([[32]], dtype=np.float32) # 1*4 + 2*5 + 3*6 = 32
np.testing.assert_array_almost_equal(result, expected)
assert result.shape == (1, 1)
# Test 3x2 × 2x4 = 3x4
A2 = np.array([[1, 2], [3, 4], [5, 6]], dtype=np.float32)
B2 = np.array([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=np.float32)
result2 = matmul_naive(A2, B2)
expected2 = A2 @ B2 # Use NumPy for verification
np.testing.assert_array_almost_equal(result2, expected2)
assert result2.shape == (3, 4)
def test_edge_cases(self):
"""Test matrix multiplication edge cases."""
# Test with zeros
A_zero = np.zeros((2, 3), dtype=np.float32)
B_zero = np.zeros((3, 2), dtype=np.float32)
result_zero = matmul_naive(A_zero, B_zero)
expected_zero = np.zeros((2, 2), dtype=np.float32)
np.testing.assert_array_almost_equal(result_zero, expected_zero)
# Test with identity
A_id = np.array([[1, 2]], dtype=np.float32)
B_id = np.array([[1, 0], [0, 1]], dtype=np.float32)
result_id = matmul_naive(A_id, B_id)
expected_id = np.array([[1, 2]], dtype=np.float32)
np.testing.assert_array_almost_equal(result_id, expected_id)
def test_comparison_with_numpy(self):
"""Test that our implementation matches NumPy."""
# Random test cases
np.random.seed(42)
for _ in range(5):
m, n, k = np.random.randint(1, 10, 3)
A = np.random.randn(m, n).astype(np.float32)
B = np.random.randn(n, k).astype(np.float32)
our_result = matmul_naive(A, B)
numpy_result = A @ B
np.testing.assert_array_almost_equal(our_result, numpy_result, decimal=5)
class TestDenseLayerInitialization:
"""Test Dense layer initialization and parameter setup."""
def test_initialization_with_bias(self):
"""Test Dense layer initialization with bias."""
layer = Dense(input_size=3, output_size=2, use_bias=True)
# Check shapes
assert layer.weights.shape == (3, 2)
assert layer.bias is not None
assert layer.bias.shape == (2,)
# Check initialization
assert not np.allclose(layer.weights, 0), "Weights should not be all zeros"
assert np.allclose(layer.bias, 0), "Bias should be initialized to zeros"
def test_initialization_without_bias(self):
"""Test Dense layer initialization without bias."""
layer = Dense(input_size=4, output_size=3, use_bias=False)
# Check shapes
assert layer.weights.shape == (4, 3)
assert layer.bias is None
# Check weight initialization
assert not np.allclose(layer.weights, 0), "Weights should not be all zeros"
def test_different_sizes(self):
"""Test Dense layer with different input/output sizes."""
test_configs = [
(1, 1),
(10, 5),
(100, 50),
(784, 128) # MNIST-like
]
for input_size, output_size in test_configs:
layer = Dense(input_size=input_size, output_size=output_size)
assert layer.weights.shape == (input_size, output_size)
if layer.bias is not None:
assert layer.bias.shape == (output_size,)
class TestDenseLayerForward:
"""Test Dense layer forward pass computation."""
def test_single_sample_forward(self):
"""Test forward pass with single sample."""
layer = Dense(input_size=3, output_size=2, use_bias=True)
# Use mock tensor
x = MockTensor([[1, 2, 3]])
y = layer(x)
# Check output shape
assert y.shape == (1, 2)
# Verify computation manually
expected = np.dot(x.data, layer.weights) + layer.bias
np.testing.assert_array_almost_equal(y.data, expected)
def test_batch_forward(self):
"""Test forward pass with batch of samples."""
layer = Dense(input_size=3, output_size=2, use_bias=True)
# Use mock tensor with batch
x = MockTensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
y = layer(x)
# Check output shape
assert y.shape == (3, 2)
# Verify computation manually
expected = np.dot(x.data, layer.weights) + layer.bias
np.testing.assert_array_almost_equal(y.data, expected)
def test_forward_without_bias(self):
"""Test forward pass without bias."""
layer = Dense(input_size=2, output_size=3, use_bias=False)
x = MockTensor([[1, 2]])
y = layer(x)
# Check output shape
assert y.shape == (1, 3)
# Verify computation (should be just matrix multiplication)
expected = np.dot(x.data, layer.weights)
np.testing.assert_array_almost_equal(y.data, expected)
def test_naive_vs_optimized_matmul(self):
"""Test that naive and optimized matrix multiplication give same results."""
layer_naive = Dense(input_size=2, output_size=2, use_naive_matmul=True)
layer_optimized = Dense(input_size=2, output_size=2, use_naive_matmul=False)
# Set same weights for comparison
layer_optimized.weights = layer_naive.weights.copy()
if layer_naive.bias is not None:
layer_optimized.bias = layer_naive.bias.copy()
x = MockTensor([[1, 2]])
y_naive = layer_naive(x)
y_optimized = layer_optimized(x)
# Both should give same results
np.testing.assert_array_almost_equal(y_naive.data, y_optimized.data)
class TestDenseLayerEdgeCases:
"""Test Dense layer edge cases and robustness."""
def test_zero_input(self):
"""Test layer with zero input."""
layer = Dense(input_size=3, output_size=2, use_bias=True)
x = MockTensor([[0, 0, 0]])
y = layer(x)
# Output should be just the bias
expected = layer.bias
np.testing.assert_array_almost_equal(y.data.flatten(), expected)
def test_large_values(self):
"""Test layer with large input values."""
layer = Dense(input_size=2, output_size=2)
x = MockTensor([[1000, -1000]])
y = layer(x)
# Should not produce NaN or Inf
assert not np.any(np.isnan(y.data))
assert not np.any(np.isinf(y.data))
assert y.shape == (1, 2)
def test_negative_values(self):
"""Test layer with negative input values."""
layer = Dense(input_size=3, output_size=2)
x = MockTensor([[-1, -2, -3]])
y = layer(x)
# Should handle negative values correctly
assert y.shape == (1, 2)
assert not np.any(np.isnan(y.data))
def test_single_neuron(self):
"""Test layer with single input and output neuron."""
layer = Dense(input_size=1, output_size=1)
x = MockTensor([[5]])
y = layer(x)
assert y.shape == (1, 1)
# Manual verification
expected = x.data * layer.weights + (layer.bias if layer.bias is not None else 0)
np.testing.assert_array_almost_equal(y.data, expected)
class TestDenseLayerIntegration:
"""Test Dense layer integration scenarios."""
def test_layer_chaining(self):
"""Test chaining multiple Dense layers."""
layer1 = Dense(input_size=4, output_size=3)
layer2 = Dense(input_size=3, output_size=2)
layer3 = Dense(input_size=2, output_size=1)
x = MockTensor([[1, 2, 3, 4]])
# Chain layers
h1 = layer1(x)
h2 = layer2(h1)
h3 = layer3(h2)
# Check shapes
assert h1.shape == (1, 3)
assert h2.shape == (1, 2)
assert h3.shape == (1, 1)
def test_parameter_counting(self):
"""Test parameter counting for different layer configurations."""
# Layer with bias
layer_bias = Dense(input_size=10, output_size=5, use_bias=True)
expected_params_bias = 10 * 5 + 5 # weights + bias
actual_params_bias = layer_bias.weights.size + (layer_bias.bias.size if layer_bias.bias is not None else 0)
assert actual_params_bias == expected_params_bias
# Layer without bias
layer_no_bias = Dense(input_size=10, output_size=5, use_bias=False)
expected_params_no_bias = 10 * 5 # only weights
actual_params_no_bias = layer_no_bias.weights.size
assert actual_params_no_bias == expected_params_no_bias
def test_batch_consistency(self):
"""Test that batch processing is consistent with single sample processing."""
layer = Dense(input_size=3, output_size=2)
# Single samples
x1 = MockTensor([[1, 2, 3]])
x2 = MockTensor([[4, 5, 6]])
y1 = layer(x1)
y2 = layer(x2)
# Batch processing
x_batch = MockTensor([[1, 2, 3], [4, 5, 6]])
y_batch = layer(x_batch)
# Results should be consistent
np.testing.assert_array_almost_equal(y_batch.data[0], y1.data[0])
np.testing.assert_array_almost_equal(y_batch.data[1], y2.data[0])
class TestDenseLayerPerformance:
"""Test Dense layer performance characteristics."""
def test_batch_processing(self):
"""Test layer with reasonable batch sizes."""
layer = Dense(input_size=10, output_size=5)
# Realistic batch size for educational context
batch_size = 32
x = MockTensor(np.random.randn(batch_size, 10))
y = layer(x)
assert y.shape == (batch_size, 5)
assert not np.any(np.isnan(y.data))
assert not np.any(np.isinf(y.data))
def test_memory_efficiency(self):
"""Test that layer doesn't create unnecessary copies."""
layer = Dense(input_size=5, output_size=3)
x = MockTensor([[1, 2, 3, 4, 5]])
original_weights = layer.weights.copy()
original_bias = layer.bias.copy() if layer.bias is not None else None
# Forward pass shouldn't modify weights
y = layer(x)
np.testing.assert_array_equal(layer.weights, original_weights)
if original_bias is not None:
np.testing.assert_array_equal(layer.bias, original_bias)
# Test runner for command line execution
if __name__ == "__main__":
pytest.main([__file__, "-v"])

View File

@@ -1,552 +0,0 @@
"""
Mock-based module tests for Networks module.
This test file uses simple mocks to avoid cross-module dependencies while thoroughly
testing the Networks module functionality. The MockTensor and MockLayer classes provide
minimal interfaces that match expected behavior without requiring actual implementations.
Test Philosophy:
- Use simple, visible mocks instead of complex mocking frameworks
- Test interface contracts and behavior, not implementation details
- Avoid dependency cascade where networks tests fail due to layer/tensor bugs
- Focus on the Sequential network architecture and MLP functionality
- Ensure educational value with clear test structure
"""
import pytest
import numpy as np
import sys
import os
# Add the module source directory to the path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'modules', 'source', '04_networks'))
from networks_dev import Sequential, MLP
class MockTensor:
"""
Simple mock tensor for testing networks without tensor dependencies.
This mock provides just enough functionality to test network architectures
without requiring the full Tensor implementation.
"""
def __init__(self, data):
"""Initialize with numpy array data."""
if isinstance(data, (list, tuple)):
self.data = np.array(data, dtype=np.float32)
elif isinstance(data, np.ndarray):
self.data = data.astype(np.float32)
else:
self.data = np.array([data], dtype=np.float32)
@property
def shape(self):
"""Return shape of the underlying data."""
return self.data.shape
def __repr__(self):
return f"MockTensor({self.data})"
def __eq__(self, other):
"""Check equality with another MockTensor."""
if isinstance(other, MockTensor):
return np.allclose(self.data, other.data)
return False
class MockLayer:
"""
Simple mock layer for testing networks without layer dependencies.
This mock simulates a layer that transforms input dimensions in a predictable way.
"""
def __init__(self, input_size, output_size, name="MockLayer"):
"""Initialize mock layer with input/output sizes."""
self.input_size = input_size
self.output_size = output_size
self.name = name
self.call_count = 0
def forward(self, x):
"""Mock forward pass that transforms input shape."""
self.call_count += 1
# Simulate layer computation: transform input to output size
if hasattr(x, 'data'):
input_data = x.data
else:
input_data = x
# Create output with correct shape
if len(input_data.shape) == 1:
# 1D input -> 1D output
output_data = np.random.randn(self.output_size).astype(np.float32)
else:
# 2D input (batch) -> 2D output
batch_size = input_data.shape[0]
output_data = np.random.randn(batch_size, self.output_size).astype(np.float32)
return MockTensor(output_data)
def __call__(self, x):
"""Make layer callable."""
return self.forward(x)
def __repr__(self):
return f"{self.name}({self.input_size} -> {self.output_size})"
class MockActivation:
"""
Simple mock activation function for testing networks.
"""
def __init__(self, name="MockActivation"):
"""Initialize mock activation."""
self.name = name
self.call_count = 0
def forward(self, x):
"""Mock activation that preserves input shape."""
self.call_count += 1
# Simple activation: just add small noise to simulate processing
if hasattr(x, 'data'):
input_data = x.data
else:
input_data = x
# Preserve shape, add small transformation
output_data = input_data + 0.01 * np.random.randn(*input_data.shape).astype(np.float32)
return MockTensor(output_data)
def __call__(self, x):
"""Make activation callable."""
return self.forward(x)
def __repr__(self):
return f"{self.name}()"
class TestSequentialNetwork:
"""Test Sequential network architecture with mock layers."""
def test_sequential_initialization_empty(self):
"""Test Sequential can be initialized without layers."""
seq = Sequential()
assert seq is not None
assert hasattr(seq, 'layers')
assert len(seq.layers) == 0
def test_sequential_initialization_with_layers(self):
"""Test Sequential can be initialized with layers."""
layer1 = MockLayer(10, 5, "Layer1")
layer2 = MockLayer(5, 2, "Layer2")
seq = Sequential([layer1, layer2])
assert len(seq.layers) == 2
assert seq.layers[0] is layer1
assert seq.layers[1] is layer2
def test_sequential_add_layer(self):
"""Test adding layers to Sequential network."""
seq = Sequential()
layer1 = MockLayer(10, 5, "Layer1")
layer2 = MockLayer(5, 2, "Layer2")
seq.add(layer1)
assert len(seq.layers) == 1
assert seq.layers[0] is layer1
seq.add(layer2)
assert len(seq.layers) == 2
assert seq.layers[1] is layer2
def test_sequential_forward_single_layer(self):
"""Test Sequential forward pass with single layer."""
layer = MockLayer(5, 3, "TestLayer")
seq = Sequential([layer])
input_tensor = MockTensor([1.0, 2.0, 3.0, 4.0, 5.0])
output = seq(input_tensor)
assert isinstance(output, MockTensor)
assert output.shape == (3,) # Should match layer output size
assert layer.call_count == 1 # Layer should be called once
def test_sequential_forward_multiple_layers(self):
"""Test Sequential forward pass with multiple layers."""
layer1 = MockLayer(4, 6, "Layer1")
layer2 = MockLayer(6, 3, "Layer2")
layer3 = MockLayer(3, 2, "Layer3")
seq = Sequential([layer1, layer2, layer3])
input_tensor = MockTensor([1.0, 2.0, 3.0, 4.0])
output = seq(input_tensor)
assert isinstance(output, MockTensor)
assert output.shape == (2,) # Should match final layer output size
# All layers should be called once
assert layer1.call_count == 1
assert layer2.call_count == 1
assert layer3.call_count == 1
def test_sequential_forward_with_activations(self):
"""Test Sequential forward pass with layers and activations."""
layer1 = MockLayer(3, 4, "Layer1")
activation1 = MockActivation("ReLU")
layer2 = MockLayer(4, 2, "Layer2")
activation2 = MockActivation("Sigmoid")
seq = Sequential([layer1, activation1, layer2, activation2])
input_tensor = MockTensor([1.0, 2.0, 3.0])
output = seq(input_tensor)
assert isinstance(output, MockTensor)
assert output.shape == (2,)
# All components should be called
assert layer1.call_count == 1
assert activation1.call_count == 1
assert layer2.call_count == 1
assert activation2.call_count == 1
def test_sequential_batch_processing(self):
"""Test Sequential with batch input."""
layer1 = MockLayer(3, 5, "Layer1")
layer2 = MockLayer(5, 2, "Layer2")
seq = Sequential([layer1, layer2])
# Batch input: 4 samples, 3 features each
batch_input = MockTensor(np.random.randn(4, 3))
output = seq(batch_input)
assert isinstance(output, MockTensor)
assert output.shape == (4, 2) # Batch size preserved, features transformed
def test_sequential_empty_network(self):
"""Test Sequential with no layers (identity function)."""
seq = Sequential()
input_tensor = MockTensor([1.0, 2.0, 3.0])
output = seq(input_tensor)
# Should return input unchanged
assert isinstance(output, MockTensor)
assert np.allclose(output.data, input_tensor.data)
def test_sequential_layer_order(self):
"""Test that Sequential processes layers in correct order."""
# Create layers that modify data in a traceable way
class TrackedLayer:
def __init__(self, multiplier, name):
self.multiplier = multiplier
self.name = name
self.call_count = 0
def forward(self, x):
self.call_count += 1
return MockTensor(x.data * self.multiplier)
def __call__(self, x):
return self.forward(x)
layer1 = TrackedLayer(2.0, "Double")
layer2 = TrackedLayer(3.0, "Triple")
seq = Sequential([layer1, layer2])
input_tensor = MockTensor([1.0])
output = seq(input_tensor)
# Should be: 1.0 * 2.0 * 3.0 = 6.0
assert np.allclose(output.data, [6.0])
assert layer1.call_count == 1
assert layer2.call_count == 1
class TestMLPNetwork:
"""Test MLP (Multi-Layer Perceptron) network with mock components."""
def test_mlp_initialization_basic(self):
"""Test MLP can be initialized with basic parameters."""
mlp = MLP(input_size=10, hidden_size=20, output_size=5)
assert mlp is not None
assert hasattr(mlp, 'network')
assert isinstance(mlp.network, Sequential)
def test_mlp_initialization_parameters(self):
"""Test MLP stores initialization parameters."""
mlp = MLP(input_size=8, hidden_size=16, output_size=3)
# Should have stored parameters
assert mlp.input_size == 8
assert mlp.hidden_size == 16
assert mlp.output_size == 3
def test_mlp_network_structure(self):
"""Test MLP creates correct network structure."""
mlp = MLP(input_size=5, hidden_size=10, output_size=2)
# Should have 3 layers: input->hidden, activation, hidden->output
assert len(mlp.network.layers) == 3
# Check layer types and sizes
hidden_layer = mlp.network.layers[0]
activation = mlp.network.layers[1]
output_layer = mlp.network.layers[2]
# Verify layer properties (if available)
if hasattr(hidden_layer, 'input_size'):
assert hidden_layer.input_size == 5
assert hidden_layer.output_size == 10
if hasattr(output_layer, 'input_size'):
assert output_layer.input_size == 10
assert output_layer.output_size == 2
def test_mlp_forward_pass(self):
"""Test MLP forward pass."""
mlp = MLP(input_size=4, hidden_size=8, output_size=3)
input_tensor = MockTensor([1.0, 2.0, 3.0, 4.0])
output = mlp(input_tensor)
assert isinstance(output, MockTensor)
assert output.shape == (3,) # Should match output_size
def test_mlp_batch_processing(self):
"""Test MLP with batch input."""
mlp = MLP(input_size=3, hidden_size=6, output_size=2)
# Batch input: 5 samples, 3 features each
batch_input = MockTensor(np.random.randn(5, 3))
output = mlp(batch_input)
assert isinstance(output, MockTensor)
assert output.shape == (5, 2) # Batch size preserved
def test_mlp_different_sizes(self):
"""Test MLP with different size configurations."""
configurations = [
(2, 4, 1), # Small network
(10, 20, 5), # Medium network
(100, 50, 10) # Large network
]
for input_size, hidden_size, output_size in configurations:
mlp = MLP(input_size=input_size, hidden_size=hidden_size, output_size=output_size)
# Test with appropriate input
input_tensor = MockTensor(np.random.randn(input_size))
output = mlp(input_tensor)
assert output.shape == (output_size,)
def test_mlp_consistency(self):
"""Test MLP produces consistent outputs for same input."""
mlp = MLP(input_size=5, hidden_size=10, output_size=3)
# Note: This test might be flaky with random mock layers
# In real implementation, should be deterministic
input_tensor = MockTensor([1.0, 2.0, 3.0, 4.0, 5.0])
# Test that MLP can be called multiple times
output1 = mlp(input_tensor)
output2 = mlp(input_tensor)
assert isinstance(output1, MockTensor)
assert isinstance(output2, MockTensor)
assert output1.shape == output2.shape
class TestNetworkIntegration:
"""Test integration between Sequential and MLP networks."""
def test_sequential_as_mlp_component(self):
"""Test using Sequential as a component in larger networks."""
# Create a sub-network
sub_network = Sequential([
MockLayer(5, 8, "SubLayer1"),
MockActivation("SubReLU"),
MockLayer(8, 3, "SubLayer2")
])
# Use it in a larger Sequential
main_network = Sequential([
MockLayer(10, 5, "MainLayer1"),
sub_network,
MockLayer(3, 2, "MainLayer2")
])
input_tensor = MockTensor(np.random.randn(10))
output = main_network(input_tensor)
assert isinstance(output, MockTensor)
assert output.shape == (2,)
def test_mlp_vs_sequential_equivalence(self):
"""Test that MLP and equivalent Sequential produce similar structure."""
# Create MLP
mlp = MLP(input_size=4, hidden_size=6, output_size=2)
# Create equivalent Sequential
seq = Sequential([
MockLayer(4, 6, "Hidden"),
MockActivation("ReLU"),
MockLayer(6, 2, "Output")
])
# Both should have same number of layers
assert len(mlp.network.layers) == len(seq.layers)
# Both should handle same input/output shapes
input_tensor = MockTensor([1.0, 2.0, 3.0, 4.0])
mlp_output = mlp(input_tensor)
seq_output = seq(input_tensor)
assert mlp_output.shape == seq_output.shape
def test_network_composition(self):
"""Test composing multiple networks."""
# Create encoder network
encoder = Sequential([
MockLayer(10, 6, "Encoder1"),
MockActivation("ReLU"),
MockLayer(6, 3, "Encoder2")
])
# Create decoder network
decoder = Sequential([
MockLayer(3, 6, "Decoder1"),
MockActivation("ReLU"),
MockLayer(6, 10, "Decoder2")
])
# Compose them
autoencoder = Sequential([encoder, decoder])
input_tensor = MockTensor(np.random.randn(10))
output = autoencoder(input_tensor)
assert isinstance(output, MockTensor)
assert output.shape == (10,) # Should reconstruct input size
class TestNetworkEdgeCases:
"""Test edge cases and error conditions for networks."""
def test_sequential_with_incompatible_layers(self):
"""Test Sequential behavior with dimension mismatches."""
# Create layers with incompatible dimensions
layer1 = MockLayer(5, 3, "Layer1")
layer2 = MockLayer(10, 2, "Layer2") # Expects 10 inputs, gets 3
seq = Sequential([layer1, layer2])
input_tensor = MockTensor(np.random.randn(5))
# This should either work (if mocks are flexible) or raise appropriate error
try:
output = seq(input_tensor)
# If it works, output should have expected shape
assert isinstance(output, MockTensor)
except (ValueError, AssertionError):
# Acceptable to raise error for incompatible dimensions
pass
def test_mlp_edge_sizes(self):
"""Test MLP with edge case sizes."""
# Very small network
mlp_small = MLP(input_size=1, hidden_size=1, output_size=1)
input_small = MockTensor([1.0])
output_small = mlp_small(input_small)
assert output_small.shape == (1,)
# Network with large hidden layer
mlp_large = MLP(input_size=2, hidden_size=100, output_size=1)
input_large = MockTensor([1.0, 2.0])
output_large = mlp_large(input_large)
assert output_large.shape == (1,)
def test_empty_sequential_behavior(self):
"""Test Sequential with various empty states."""
# Empty Sequential
empty_seq = Sequential()
# Should handle empty input
empty_input = MockTensor([])
output = empty_seq(empty_input)
assert np.array_equal(output.data, empty_input.data)
# Should handle normal input (identity function)
normal_input = MockTensor([1.0, 2.0, 3.0])
output = empty_seq(normal_input)
assert np.array_equal(output.data, normal_input.data)
def test_network_with_none_layers(self):
"""Test network robustness with None layers."""
# Sequential should handle None layers gracefully
try:
seq = Sequential([None, MockLayer(5, 3, "Layer"), None])
# Should either filter out None or raise appropriate error
assert True # If we get here, it handled None gracefully
except (ValueError, TypeError):
# Acceptable to raise error for None layers
pass
class TestNetworkPerformance:
"""Test network performance characteristics."""
def test_sequential_call_efficiency(self):
"""Test that Sequential doesn't add excessive overhead."""
layers = [MockLayer(10, 10, f"Layer{i}") for i in range(5)]
seq = Sequential(layers)
input_tensor = MockTensor(np.random.randn(10))
# Multiple calls should work efficiently
for _ in range(10):
output = seq(input_tensor)
assert isinstance(output, MockTensor)
assert output.shape == (10,)
# Each layer should be called the expected number of times
for layer in layers:
assert layer.call_count == 10
def test_mlp_scalability(self):
"""Test MLP with different scales."""
scales = [
(5, 10, 2),
(20, 50, 10),
(100, 200, 50)
]
for input_size, hidden_size, output_size in scales:
mlp = MLP(input_size=input_size, hidden_size=hidden_size, output_size=output_size)
# Test single sample
single_input = MockTensor(np.random.randn(input_size))
single_output = mlp(single_input)
assert single_output.shape == (output_size,)
# Test batch
batch_input = MockTensor(np.random.randn(10, input_size))
batch_output = mlp(batch_input)
assert batch_output.shape == (10, output_size)
if __name__ == "__main__":
# Run tests if executed directly
pytest.main([__file__, "-v"])