# Testing Guide: Mocking Hardware Without the Robot ## The Problem When you run `gradlew test`, Gradle tries to compile **all** your main source code, including files that depend on the FTC SDK (like `FtcMotorController` and `MotorCycleOpMode`). Since the FTC SDK isn't available on your Windows machine, compilation fails. ## The Solution: Source Set Exclusion Your `build.gradle.kts` now excludes FTC-dependent files from test compilation: ```kotlin sourceSets { main { java { exclude( "robot/hardware/FtcMotorController.java", "robot/opmodes/**/*.java" ) } } } ``` This means: - ✅ `MotorController.java` (interface) - Compiles for tests - ✅ `MotorCycler.java` (pure logic) - Compiles for tests - ✅ `MockMotorController.java` (test mock) - Compiles for tests - ❌ `FtcMotorController.java` (FTC SDK) - Skipped for tests - ❌ `MotorCycleOpMode.java` (FTC SDK) - Skipped for tests ## Running Tests ```bash # On Windows gradlew test # On Linux/Mac ./gradlew test ``` The tests run entirely on your Windows JRE - **no robot, no Android, no FTC SDK needed!** ## The Architecture Pattern ### 1. Interface (Hardware Abstraction) ```java public interface MotorController { void setPower(double power); double getPower(); } ``` ### 2. Real Implementation (FTC-dependent) ```java public class FtcMotorController implements MotorController { private final DcMotor motor; // FTC SDK class public FtcMotorController(DcMotor motor) { this.motor = motor; } @Override public void setPower(double power) { motor.setPower(power); } @Override public double getPower() { return motor.getPower(); } } ``` ### 3. Mock Implementation (Testing) ```java public class MockMotorController implements MotorController { private double power = 0.0; @Override public void setPower(double power) { this.power = power; } @Override public double getPower() { return power; } } ``` ### 4. Business Logic (Pure Java) ```java public class MotorCycler { private final MotorController motor; // Interface, not FTC class! public MotorCycler(MotorController motor, long onMs, long offMs) { this.motor = motor; // ... } public void update(long currentTimeMs) { // Time-based state machine // No FTC SDK dependencies! } } ``` ## Example: I2C Bump Sensor Here's how you'd implement the pattern for an I2C bump sensor: ### Interface ```java // src/main/java/robot/hardware/BumpSensor.java package robot.hardware; public interface BumpSensor { /** * Check if the sensor detects contact. * @return true if bumped, false otherwise */ boolean isBumped(); /** * Get the force reading (0.0 to 1.0). * @return force value */ double getForce(); } ``` ### FTC Implementation ```java // src/main/java/robot/hardware/FtcBumpSensor.java package robot.hardware; import com.qualcomm.robotcore.hardware.I2cDevice; import com.qualcomm.robotcore.hardware.I2cDeviceReader; public class FtcBumpSensor implements BumpSensor { private final I2cDevice sensor; private final I2cDeviceReader reader; private static final double BUMP_THRESHOLD = 0.5; public FtcBumpSensor(I2cDevice sensor) { this.sensor = sensor; this.reader = new I2cDeviceReader(sensor, 0x00, 2); } @Override public boolean isBumped() { return getForce() > BUMP_THRESHOLD; } @Override public double getForce() { byte[] data = reader.read(); // Convert I2C bytes to force value int raw = ((data[0] & 0xFF) << 8) | (data[1] & 0xFF); return raw / 65535.0; } } ``` ### Mock Implementation ```java // src/test/java/robot/hardware/MockBumpSensor.java package robot.hardware; public class MockBumpSensor implements BumpSensor { private double force = 0.0; /** * Simulate hitting a wall with given force. */ public void simulateImpact(double force) { this.force = Math.max(0.0, Math.min(1.0, force)); } /** * Simulate sensor returning to neutral. */ public void reset() { this.force = 0.0; } @Override public boolean isBumped() { return force > 0.5; } @Override public double getForce() { return force; } } ``` ### Business Logic Using Sensor ```java // src/main/java/robot/subsystems/CollisionDetector.java package robot.subsystems; import robot.hardware.BumpSensor; import robot.hardware.MotorController; public class CollisionDetector { private final BumpSensor sensor; private final MotorController motor; private boolean collisionDetected = false; public CollisionDetector(BumpSensor sensor, MotorController motor) { this.sensor = sensor; this.motor = motor; } public void update() { if (sensor.isBumped() && !collisionDetected) { // First detection - stop the motor motor.setPower(0.0); collisionDetected = true; } else if (!sensor.isBumped() && collisionDetected) { // Sensor cleared - reset flag collisionDetected = false; } } public boolean hasCollision() { return collisionDetected; } } ``` ### Test with Simulated Wall Hit ```java // src/test/java/robot/subsystems/CollisionDetectorTest.java package robot.subsystems; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import robot.hardware.MockBumpSensor; import robot.hardware.MockMotorController; import static org.junit.jupiter.api.Assertions.*; class CollisionDetectorTest { private MockBumpSensor sensor; private MockMotorController motor; private CollisionDetector detector; @BeforeEach void setUp() { sensor = new MockBumpSensor(); motor = new MockMotorController(); detector = new CollisionDetector(sensor, motor); } @Test void testWallImpactStopsMotor() { // Robot is driving motor.setPower(0.8); assertEquals(0.8, motor.getPower(), 0.001); // Simulate hitting a wall with high force sensor.simulateImpact(0.9); detector.update(); // Motor should stop assertEquals(0.0, motor.getPower(), 0.001); assertTrue(detector.hasCollision()); } @Test void testGentleContactDetected() { // Simulate gentle touch (above threshold) sensor.simulateImpact(0.6); detector.update(); assertTrue(detector.hasCollision()); } @Test void testBelowThresholdIgnored() { motor.setPower(0.5); // Simulate light vibration (below threshold) sensor.simulateImpact(0.3); detector.update(); // Should not register as collision assertFalse(detector.hasCollision()); assertEquals(0.5, motor.getPower(), 0.001); } @Test void testCollisionClearsWhenSensorReleased() { // Hit wall sensor.simulateImpact(0.8); detector.update(); assertTrue(detector.hasCollision()); // Back away from wall sensor.reset(); detector.update(); // Collision flag should clear assertFalse(detector.hasCollision()); } @Test void testMultipleImpacts() { // First impact sensor.simulateImpact(0.7); detector.update(); assertTrue(detector.hasCollision()); // Clear sensor.reset(); detector.update(); assertFalse(detector.hasCollision()); // Second impact sensor.simulateImpact(0.8); detector.update(); assertTrue(detector.hasCollision()); } } ``` ## Key Benefits ### 1. **Test Real Scenarios Without Hardware** ```java @Test void testRobotBouncesOffWall() { // Simulate approach motor.setPower(0.8); // Hit wall sensor.simulateImpact(0.9); detector.update(); // Verify emergency stop assertEquals(0.0, motor.getPower()); } ``` ### 2. **Test Edge Cases** ```java @Test void testSensorNoise() { // Simulate sensor flutter at threshold sensor.simulateImpact(0.49); detector.update(); assertFalse(detector.hasCollision()); sensor.simulateImpact(0.51); detector.update(); assertTrue(detector.hasCollision()); } ``` ### 3. **Test Timing Issues** ```java @Test void testRapidImpacts() { for (int i = 0; i < 10; i++) { sensor.simulateImpact(0.8); detector.update(); assertTrue(detector.hasCollision()); sensor.reset(); detector.update(); } } ``` ### 4. **Integration Tests** ```java @Test void testFullDriveSequence() { // Drive forward motor.setPower(0.5); for (int i = 0; i < 10; i++) { detector.update(); assertFalse(detector.hasCollision()); } // Hit obstacle sensor.simulateImpact(0.8); detector.update(); assertEquals(0.0, motor.getPower()); // Back up motor.setPower(-0.3); sensor.reset(); detector.update(); // Continue backing for (int i = 0; i < 5; i++) { detector.update(); assertEquals(-0.3, motor.getPower()); } } ``` ## File Organization ``` my-robot/ ├── src/main/java/robot/ │ ├── hardware/ # Abstractions │ │ ├── MotorController.java ✅ Tests compile │ │ ├── BumpSensor.java ✅ Tests compile │ │ ├── FtcMotorController.java ❌ Excluded from tests │ │ └── FtcBumpSensor.java ❌ Excluded from tests │ │ │ ├── subsystems/ # Business Logic │ │ ├── MotorCycler.java ✅ Tests compile │ │ └── CollisionDetector.java ✅ Tests compile │ │ │ └── opmodes/ # FTC Integration │ └── MotorCycleOpMode.java ❌ Excluded from tests │ └── src/test/java/robot/ ├── hardware/ # Mocks │ ├── MockMotorController.java │ └── MockBumpSensor.java │ └── subsystems/ # Tests ├── MotorCyclerTest.java └── CollisionDetectorTest.java ``` ## Build Configuration Rules In `build.gradle.kts`: ```kotlin sourceSets { main { java { // Exclude all FTC-dependent code from test compilation exclude( "robot/hardware/Ftc*.java", // All FTC implementations "robot/opmodes/**/*.java" // All OpModes ) } } } ``` This pattern ensures: - ✅ Interfaces and pure logic compile for tests - ✅ Mocks are available in test classpath - ✅ Tests run on Windows JRE instantly - ✅ FTC-dependent code is deployed and compiled on robot - ✅ Same logic runs in tests and on robot ## Advanced Mocking Patterns ### Stateful Mock (Servo with Position Memory) ```java public class MockServo implements ServoController { private double position = 0.5; private double speed = 1.0; // Instant by default public void setSpeed(double speed) { this.speed = speed; } @Override public void setPosition(double target) { // Simulate gradual movement double delta = target - position; position += delta * speed; position = Math.max(0.0, Math.min(1.0, position)); } @Override public double getPosition() { return position; } } ``` ### Mock with Latency ```java public class MockGyro implements GyroSensor { private double heading = 0.0; private long lastUpdateTime = 0; private double drift = 0.1; // Degrees per second drift public void simulateRotation(double degrees, long timeMs) { heading += degrees; heading = (heading + 360) % 360; lastUpdateTime = timeMs; } @Override public double getHeading(long currentTime) { // Simulate sensor drift over time long elapsed = currentTime - lastUpdateTime; double driftError = (elapsed / 1000.0) * drift; return (heading + driftError) % 360; } } ``` ### Mock with Failure Modes ```java public class MockDistanceSensor implements DistanceSensor { private double distance = 100.0; private boolean connected = true; private double noise = 0.0; public void simulateDisconnect() { connected = false; } public void setNoise(double stdDev) { this.noise = stdDev; } @Override public double getDistance() throws SensorException { if (!connected) { throw new SensorException("Sensor disconnected"); } // Add Gaussian noise double noisyDistance = distance + (Math.random() - 0.5) * noise; return Math.max(0, noisyDistance); } } ``` ## Why This Matters Traditional FTC development: - ❌ Can't test without robot - ❌ Long iteration cycles (code → deploy → test → repeat) - ❌ Hard to test edge cases - ❌ Integration issues found late Weevil + proper mocking: - ✅ Test instantly on PC - ✅ Rapid iteration (code → test → fix) - ✅ Comprehensive edge case coverage - ✅ Catch bugs before robot practice **You're not just testing motor values - you're simulating complete scenarios: wall collisions, sensor failures, timing issues, state machines, everything!** This is professional robotics software engineering.