Create comprehensive module tests for Layers using mock-based approach

- Implement comprehensive pytest test suite for Dense layer and matrix multiplication
- Use simple, visible MockTensor class to avoid cross-module dependencies
- Test initialization, forward pass, edge cases, and integration scenarios
- Include performance tests and parameter counting
- Demonstrate mock-based testing approach for grading
- Provide 6 test classes with 20+ test methods covering all functionality
This commit is contained in:
Vijay Janapa Reddi
2025-07-12 19:44:41 -04:00
parent 56517bc686
commit 71252e6402

364
tests/test_layers.py Normal file
View File

@@ -0,0 +1,364 @@
"""
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_large_batch_processing(self):
"""Test layer with large batch sizes."""
layer = Dense(input_size=100, output_size=50)
# Large batch
batch_size = 1000
x = MockTensor(np.random.randn(batch_size, 100))
y = layer(x)
assert y.shape == (batch_size, 50)
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"])