Unit Testing for FTC Development

Introduction to Unit Testing in FTC

Unit testing is a critical practice for ensuring your FTC robot code works correctly and reliably. By writing tests for individual components and methods, you can catch bugs early, verify functionality, and make your code more maintainable. This lesson covers how to implement unit testing in Android Studio for FTC projects.

Why Unit Testing Matters for FTC

  • Catch bugs before they reach the robot hardware
  • Verify complex algorithms work correctly
  • Ensure code changes don't break existing functionality
  • Document expected behavior of your code
  • Speed up development by catching issues early

Setting Up Testing Framework

Android Studio uses JUnit for unit testing. You'll need to set up your project to include test dependencies and create test classes that can run independently of the robot hardware.

Project Testing Configuration - Dependencies

// In your app/build.gradle file, ensure you have:
dependencies {
    // Existing FTC dependencies...
    
    // Testing dependencies
    testImplementation 'junit:junit:4.13.2'
    testImplementation 'org.mockito:mockito-core:4.5.1'
    testImplementation 'org.mockito:mockito-inline:4.5.1'
    
    // For Android-specific testing
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test:runner:1.5.2'
    androidTestImplementation 'androidx.test:rules:1.5.0'
}

Understanding Testing Dependencies

The testing dependencies provide essential tools for unit testing. JUnit provides the testing framework, while Mockito allows you to create mock objects that simulate hardware components. This is crucial for testing robot code without requiring actual hardware.

Project Testing Configuration - Test Options

// In your app/build.gradle file, add test options:
android {
    // Existing configuration...
    
    testOptions {
        unitTests {
            includeAndroidResources = true
            returnDefaultValues = true
        }
    }
}

Test Configuration Explanation

The test options configure how your tests run. 'includeAndroidResources' allows tests to access Android resources, while 'returnDefaultValues' provides default return values for Android framework methods that aren't available in unit tests.

Creating Your First Test

Let's start with a simple test for a utility class that performs calculations. This demonstrates the basic structure of unit tests and shows how to test mathematical operations that are common in FTC programming.

Basic Utility Class - Encoder Conversions

// File: RobotUtils.java
public class RobotUtils {
    /**
     * Converts encoder ticks to distance in inches
     * @param ticks Number of encoder ticks
     * @param ticksPerRevolution Ticks per revolution of the motor
     * @param wheelDiameter Wheel diameter in inches
     * @return Distance in inches
     */
    public static double ticksToInches(int ticks, int ticksPerRevolution, double wheelDiameter) {
        if (ticksPerRevolution <= 0 || wheelDiameter <= 0) {
            throw new IllegalArgumentException("Ticks per revolution and wheel diameter must be positive");
        }
        
        double circumference = Math.PI * wheelDiameter;
        return (ticks * circumference) / ticksPerRevolution;
    }
    
    /**
     * Converts distance in inches to encoder ticks
     * @param inches Distance in inches
     * @param ticksPerRevolution Ticks per revolution of the motor
     * @param wheelDiameter Wheel diameter in inches
     * @return Number of encoder ticks
     */
    public static int inchesToTicks(double inches, int ticksPerRevolution, double wheelDiameter) {
        if (ticksPerRevolution <= 0 || wheelDiameter <= 0) {
            throw new IllegalArgumentException("Ticks per revolution and wheel diameter must be positive");
        }
        
        double circumference = Math.PI * wheelDiameter;
        return (int) Math.round((inches * ticksPerRevolution) / circumference);
    }

Basic Utility Class - Power Calculations

    /**
     * Calculates the power needed for a given distance and time
     * @param distance Distance to travel in inches
     * @param time Time to travel in seconds
     * @param maxPower Maximum power available (0.0 to 1.0)
     * @return Calculated power (0.0 to maxPower)
     */
    public static double calculatePowerForDistance(double distance, double time, double maxPower) {
        if (distance < 0 || time <= 0 || maxPower <= 0 || maxPower > 1.0) {
            throw new IllegalArgumentException("Invalid parameters for power calculation");
        }
        
        // Simple linear calculation (in reality, you'd use more complex physics)
        double requiredPower = distance / (time * 12.0); // Assuming 12 inches per second at full power
        return Math.min(requiredPower, maxPower);
    }
}

Understanding Power Calculations

Power calculations determine how much motor power is needed to achieve a desired movement. This method uses a simplified linear model, but in practice, you might use more complex physics calculations that account for robot mass, friction, and acceleration.

Writing Tests for Encoder Conversions

Now let's write tests for the encoder conversion methods. These tests verify that the mathematical calculations work correctly and handle edge cases properly.

Test Class Setup and Ticks to Inches Tests

// File: RobotUtilsTest.java (in test directory)
import org.junit.Test;
import static org.junit.Assert.*;

public class RobotUtilsTest {
    
    @Test
    public void testTicksToInches() {
        // Test case 1: Basic conversion
        double result = RobotUtils.ticksToInches(1000, 1440, 4.0);
        double expected = (1000 * Math.PI * 4.0) / 1440;
        assertEquals("Basic ticks to inches conversion", expected, result, 0.001);
        
        // Test case 2: Zero ticks
        result = RobotUtils.ticksToInches(0, 1440, 4.0);
        assertEquals("Zero ticks should return zero inches", 0.0, result, 0.001);
        
        // Test case 3: Large number of ticks
        result = RobotUtils.ticksToInches(10000, 1440, 4.0);
        expected = (10000 * Math.PI * 4.0) / 1440;
        assertEquals("Large number of ticks", expected, result, 0.001);
    }

Understanding Basic Conversion Tests

The basic conversion test verifies that the mathematical formula works correctly. It tests normal cases, edge cases (zero ticks), and large values to ensure the calculation remains accurate across different input ranges.

Inches to Ticks Tests

    @Test
    public void testInchesToTicks() {
        // Test case 1: Basic conversion
        int result = RobotUtils.inchesToTicks(10.0, 1440, 4.0);
        int expected = (int) Math.round((10.0 * 1440) / (Math.PI * 4.0));
        assertEquals("Basic inches to ticks conversion", expected, result);
        
        // Test case 2: Zero inches
        result = RobotUtils.inchesToTicks(0.0, 1440, 4.0);
        assertEquals("Zero inches should return zero ticks", 0, result);
        
        // Test case 3: Rounding behavior
        result = RobotUtils.inchesToTicks(5.5, 1440, 4.0);
        expected = (int) Math.round((5.5 * 1440) / (Math.PI * 4.0));
        assertEquals("Rounding behavior test", expected, result);
    }

Understanding Reverse Conversion Tests

The inches to ticks tests verify the reverse conversion works correctly. They test the rounding behavior since the result must be an integer, and ensure that zero distance returns zero ticks.

Power Calculation Tests

    @Test
    public void testCalculatePowerForDistance() {
        // Test case 1: Normal calculation
        double result = RobotUtils.calculatePowerForDistance(24.0, 2.0, 1.0);
        double expected = 24.0 / (2.0 * 12.0); // 1.0
        assertEquals("Normal power calculation", expected, result, 0.001);
        
        // Test case 2: Power limited by maxPower
        result = RobotUtils.calculatePowerForDistance(48.0, 2.0, 0.5);
        expected = 0.5; // Limited by maxPower
        assertEquals("Power limited by maxPower", expected, result, 0.001);
        
        // Test case 3: Zero distance
        result = RobotUtils.calculatePowerForDistance(0.0, 2.0, 1.0);
        assertEquals("Zero distance should return zero power", 0.0, result, 0.001);
    }

Understanding Power Calculation Tests

The power calculation tests verify that the method correctly calculates required power and respects the maximum power limit. They test normal cases, power limiting behavior, and edge cases like zero distance.

Error Handling Tests

    @Test(expected = IllegalArgumentException.class)
    public void testTicksToInchesInvalidParameters() {
        RobotUtils.ticksToInches(1000, 0, 4.0); // Invalid ticks per revolution
    }
    
    @Test(expected = IllegalArgumentException.class)
    public void testInchesToTicksInvalidParameters() {
        RobotUtils.inchesToTicks(10.0, 1440, -4.0); // Invalid wheel diameter
    }
    
    @Test(expected = IllegalArgumentException.class)
    public void testCalculatePowerInvalidParameters() {
        RobotUtils.calculatePowerForDistance(10.0, 0.0, 1.0); // Invalid time
    }
}

Understanding Error Handling Tests

Error handling tests use the @Test(expected = ...) annotation to verify that methods throw appropriate exceptions when given invalid parameters. This ensures your code fails gracefully with meaningful error messages.

Testing Robot Components with Mocking

Since you can't always test with real hardware, mocking allows you to simulate hardware components and test your control logic independently. Mocking is essential for testing robot controllers without requiring actual hardware.

Robot Controller Class - Basic Structure

// File: RobotController.java
public class RobotController {
    private DcMotor leftMotor;
    private DcMotor rightMotor;
    private ColorSensor colorSensor;
    
    public RobotController(DcMotor leftMotor, DcMotor rightMotor, ColorSensor colorSensor) {
        this.leftMotor = leftMotor;
        this.rightMotor = rightMotor;
        this.colorSensor = colorSensor;
    }
    
    public void driveForward(double power) {
        if (power < -1.0 || power > 1.0) {
            throw new IllegalArgumentException("Power must be between -1.0 and 1.0");
        }
        
        leftMotor.setPower(power);
        rightMotor.setPower(power);
    }

Understanding Robot Controller Design

The robot controller class encapsulates hardware interactions, making it easier to test. By accepting hardware components as constructor parameters, we can inject mock objects during testing instead of requiring real hardware.

Robot Controller Class - Additional Methods

    public void turn(double leftPower, double rightPower) {
        if (Math.abs(leftPower) > 1.0 || Math.abs(rightPower) > 1.0) {
            throw new IllegalArgumentException("Power values must be between -1.0 and 1.0");
        }
        
        leftMotor.setPower(leftPower);
        rightMotor.setPower(rightPower);
    }
    
    public boolean isRedBallDetected() {
        int red = colorSensor.red();
        int green = colorSensor.green();
        int blue = colorSensor.blue();
        
        // Simple red detection logic
        return red > green && red > blue && red > 100;
    }
    
    public void stop() {
        leftMotor.setPower(0.0);
        rightMotor.setPower(0.0);
    }
    
    public double getLeftMotorPower() {
        return leftMotor.getPower();
    }
    
    public double getRightMotorPower() {
        return rightMotor.getPower();
    }
}

Understanding Robot Controller Methods

The additional methods provide a complete interface for robot control. The turn method allows differential drive, the color detection method processes sensor data, and the getter methods allow testing to verify motor states.

Setting Up Mocked Hardware Tests

Now let's create tests for the RobotController using Mockito. These tests will verify that the controller properly interacts with the hardware components.

Test Setup and Drive Forward Tests

// File: RobotControllerTest.java
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;

@RunWith(MockitoJUnitRunner.class)
public class RobotControllerTest {
    
    @Mock
    private DcMotor leftMotor;
    
    @Mock
    private DcMotor rightMotor;
    
    @Mock
    private ColorSensor colorSensor;
    
    private RobotController robotController;
    
    @Before
    public void setUp() {
        robotController = new RobotController(leftMotor, rightMotor, colorSensor);
    }
    
    @Test
    public void testDriveForward() {
        // Test driving forward with positive power
        robotController.driveForward(0.5);
        
        verify(leftMotor).setPower(0.5);
        verify(rightMotor).setPower(0.5);
    }
    
    @Test
    public void testDriveForwardNegativePower() {
        // Test driving backward
        robotController.driveForward(-0.3);
        
        verify(leftMotor).setPower(-0.3);
        verify(rightMotor).setPower(-0.3);
    }
    
    @Test(expected = IllegalArgumentException.class)
    public void testDriveForwardInvalidPower() {
        robotController.driveForward(1.5); // Power > 1.0
    }

Understanding Mockito Test Setup

The @RunWith(MockitoJUnitRunner.class) annotation tells JUnit to use Mockito for test execution. The @Mock annotations create mock objects that simulate hardware components, and the @Before method sets up the test environment before each test.

Turn and Stop Tests

    @Test
    public void testTurn() {
        // Test turning left
        robotController.turn(-0.4, 0.4);
        
        verify(leftMotor).setPower(-0.4);
        verify(rightMotor).setPower(0.4);
    }
    
    @Test(expected = IllegalArgumentException.class)
    public void testTurnInvalidPower() {
        robotController.turn(1.2, 0.5); // Left power > 1.0
    }
    
    @Test
    public void testStop() {
        robotController.stop();
        
        verify(leftMotor).setPower(0.0);
        verify(rightMotor).setPower(0.0);
    }

Understanding Turn and Stop Tests

The turn test verifies that the robot controller correctly sets different powers for left and right motors to achieve turning. The stop test ensures both motors are set to zero power when stopping.

Color Sensor Tests

    @Test
    public void testIsRedBallDetected() {
        // Test case 1: Red ball detected
        when(colorSensor.red()).thenReturn(200);
        when(colorSensor.green()).thenReturn(50);
        when(colorSensor.blue()).thenReturn(30);
        
        assertTrue("Should detect red ball", robotController.isRedBallDetected());
        
        // Test case 2: No red ball (blue dominant)
        when(colorSensor.red()).thenReturn(50);
        when(colorSensor.green()).thenReturn(60);
        when(colorSensor.blue()).thenReturn(200);
        
        assertFalse("Should not detect red ball when blue is dominant", robotController.isRedBallDetected());
        
        // Test case 3: Red too low
        when(colorSensor.red()).thenReturn(80);
        when(colorSensor.green()).thenReturn(30);
        when(colorSensor.blue()).thenReturn(20);
        
        assertFalse("Should not detect red ball when red value is too low", robotController.isRedBallDetected());
    }

Understanding Color Sensor Tests

The color sensor tests use Mockito's when().thenReturn() to simulate different sensor readings. This allows testing the color detection logic with various scenarios without needing a real color sensor.

Motor Power Retrieval Tests

    @Test
    public void testGetMotorPowers() {
        when(leftMotor.getPower()).thenReturn(0.7);
        when(rightMotor.getPower()).thenReturn(0.3);
        
        assertEquals("Left motor power should match", 0.7, robotController.getLeftMotorPower(), 0.001);
        assertEquals("Right motor power should match", 0.3, robotController.getRightMotorPower(), 0.001);
    }
}

Understanding Motor Power Tests

The motor power retrieval tests verify that the controller correctly returns the current motor power values. These tests ensure that the getter methods properly delegate to the underlying motor objects.

Testing Autonomous Sequences

Autonomous sequences are complex and critical for competition success. Testing them thoroughly ensures they work reliably under various conditions. We'll create a testable autonomous sequence class and verify its behavior.

Autonomous Sequence Class

// File: AutonomousSequence.java
public class AutonomousSequence {
    private RobotController robotController;
    private double startTime;
    private boolean isComplete = false;
    
    public AutonomousSequence(RobotController robotController) {
        this.robotController = robotController;
    }
    
    public void start(double currentTime) {
        this.startTime = currentTime;
        this.isComplete = false;
    }
    
    public void update(double currentTime) {
        if (isComplete) return;
        
        double elapsedTime = currentTime - startTime;
        
        // Simple autonomous sequence: drive forward for 2 seconds, then stop
        if (elapsedTime < 2.0) {
            robotController.driveForward(0.5);
        } else {
            robotController.stop();
            isComplete = true;
        }
    }
    
    public boolean isComplete() {
        return isComplete;
    }
    
    public double getElapsedTime(double currentTime) {
        return currentTime - startTime;
    }
}

Understanding Autonomous Sequence Design

The autonomous sequence class encapsulates the timing and state management of an autonomous routine. It uses dependency injection to accept a robot controller, making it testable with mocked components.

Testing Autonomous Sequence Behavior

Now let's test the autonomous sequence to ensure it behaves correctly at different time points and properly manages its state.

Autonomous Sequence Test Setup and Start Tests

// File: AutonomousSequenceTest.java
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;

@RunWith(MockitoJUnitRunner.class)
public class AutonomousSequenceTest {
    
    @Mock
    private RobotController robotController;
    
    private AutonomousSequence autonomousSequence;
    
    @Before
    public void setUp() {
        autonomousSequence = new AutonomousSequence(robotController);
    }
    
    @Test
    public void testSequenceStart() {
        autonomousSequence.start(0.0);
        
        assertFalse("Sequence should not be complete at start", autonomousSequence.isComplete());
        assertEquals("Elapsed time should be 0 at start", 0.0, autonomousSequence.getElapsedTime(0.0), 0.001);
    }

Understanding Sequence Start Tests

The sequence start test verifies that the autonomous sequence initializes correctly. It checks that the sequence is not complete at the start and that elapsed time calculation works properly.

Driving Phase Tests

    @Test
    public void testSequenceDrivingPhase() {
        autonomousSequence.start(0.0);
        
        // Test during driving phase (first 2 seconds)
        autonomousSequence.update(1.0);
        
        verify(robotController).driveForward(0.5);
        assertFalse("Sequence should not be complete during driving phase", autonomousSequence.isComplete());
        
        // Test at 1.5 seconds
        autonomousSequence.update(1.5);
        
        verify(robotController, times(2)).driveForward(0.5);
        assertFalse("Sequence should not be complete at 1.5 seconds", autonomousSequence.isComplete());
    }

Understanding Driving Phase Tests

The driving phase tests verify that the sequence correctly drives forward during the first 2 seconds. The times(2) verification ensures that driveForward was called exactly twice during the test.

Completion Tests

    @Test
    public void testSequenceCompletion() {
        autonomousSequence.start(0.0);
        
        // Test at exactly 2.0 seconds
        autonomousSequence.update(2.0);
        
        verify(robotController).driveForward(0.5);
        verify(robotController).stop();
        assertTrue("Sequence should be complete at 2.0 seconds", autonomousSequence.isComplete());
        
        // Test that further updates don't change behavior
        autonomousSequence.update(3.0);
        
        // Should not call driveForward again since sequence is complete
        verify(robotController, times(1)).driveForward(0.5);
        verify(robotController, times(1)).stop();
    }
    
    @Test
    public void testSequenceAfterCompletion() {
        autonomousSequence.start(0.0);
        
        // Complete the sequence
        autonomousSequence.update(2.0);
        assertTrue("Sequence should be complete", autonomousSequence.isComplete());
        
        // Try to update again
        autonomousSequence.update(5.0);
        
        // Should not call any robot controller methods
        verify(robotController, times(1)).driveForward(0.5);
        verify(robotController, times(1)).stop();
        verifyNoMoreInteractions(robotController);
    }
    
    @Test
    public void testElapsedTimeCalculation() {
        autonomousSequence.start(10.0);
        
        assertEquals("Elapsed time calculation", 0.0, autonomousSequence.getElapsedTime(10.0), 0.001);
        assertEquals("Elapsed time calculation", 1.5, autonomousSequence.getElapsedTime(11.5), 0.001);
        assertEquals("Elapsed time calculation", 3.0, autonomousSequence.getElapsedTime(13.0), 0.001);
    }
}

Understanding Completion Tests

The completion tests verify that the sequence properly stops and marks itself as complete after 2 seconds. They also ensure that once complete, the sequence doesn't continue calling robot controller methods.

Integration Testing

Integration tests verify that multiple components work together correctly. These tests are more complex but catch issues that unit tests might miss. They test the interaction between different parts of your robot system.

Integration Test Setup and Complete Sequence Test

// File: RobotIntegrationTest.java
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;

@RunWith(MockitoJUnitRunner.class)
public class RobotIntegrationTest {
    
    @Mock
    private DcMotor leftMotor;
    
    @Mock
    private DcMotor rightMotor;
    
    @Mock
    private ColorSensor colorSensor;
    
    private RobotController robotController;
    private AutonomousSequence autonomousSequence;
    
    @Before
    public void setUp() {
        robotController = new RobotController(leftMotor, rightMotor, colorSensor);
        autonomousSequence = new AutonomousSequence(robotController);
    }
    
    @Test
    public void testCompleteAutonomousSequence() {
        // Set up color sensor to detect red ball
        when(colorSensor.red()).thenReturn(200);
        when(colorSensor.green()).thenReturn(50);
        when(colorSensor.blue()).thenReturn(30);
        
        // Start autonomous sequence
        autonomousSequence.start(0.0);
        
        // Simulate sequence execution
        for (double time = 0.1; time <= 2.5; time += 0.1) {
            autonomousSequence.update(time);
        }
        
        // Verify the complete sequence executed correctly
        assertTrue("Autonomous sequence should be complete", autonomousSequence.isComplete());
        
        // Verify motor interactions
        verify(leftMotor, atLeastOnce()).setPower(0.5);
        verify(rightMotor, atLeastOnce()).setPower(0.5);
        verify(leftMotor, atLeastOnce()).setPower(0.0);
        verify(rightMotor, atLeastOnce()).setPower(0.0);
        
        // Verify color sensor was used
        verify(colorSensor, atLeastOnce()).red();
        verify(colorSensor, atLeastOnce()).green();
        verify(colorSensor, atLeastOnce()).blue();
    }

Understanding Integration Test Setup

The integration test sets up a complete system with mocked hardware components. It tests the interaction between the autonomous sequence and the robot controller, ensuring they work together correctly.

Integration Test with Real Calculations

    @Test
    public void testRobotControllerWithRealCalculations() {
        // Test that utility functions work with robot controller
        double distance = 24.0; // 24 inches
        int ticksPerRev = 1440;
        double wheelDiameter = 4.0;
        
        // Calculate expected ticks
        int expectedTicks = RobotUtils.inchesToTicks(distance, ticksPerRev, wheelDiameter);
        
        // Verify calculation is reasonable
        assertTrue("Ticks should be positive", expectedTicks > 0);
        assertTrue("Ticks should be reasonable for 24 inches", expectedTicks < 10000);
        
        // Test power calculation
        double time = 3.0; // 3 seconds
        double maxPower = 1.0;
        double calculatedPower = RobotUtils.calculatePowerForDistance(distance, time, maxPower);
        
        assertTrue("Power should be positive", calculatedPower > 0);
        assertTrue("Power should not exceed maxPower", calculatedPower <= maxPower);
        
        // Use calculated power with robot controller
        robotController.driveForward(calculatedPower);
        
        verify(leftMotor).setPower(calculatedPower);
        verify(rightMotor).setPower(calculatedPower);
    }
}

Understanding Integration with Real Calculations

This integration test combines utility functions with robot controller operations, testing that calculated values are reasonable and that the robot controller correctly uses them.

Complete Unit Testing Example

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
import com.qualcomm.robotcore.hardware.DcMotor;
import com.qualcomm.robotcore.hardware.ColorSensor;

// RobotUtils.java
public class RobotUtils {
    public static double ticksToInches(int ticks, int ticksPerRevolution, double wheelDiameter) {
        if (ticksPerRevolution <= 0 || wheelDiameter <= 0) {
            throw new IllegalArgumentException("Ticks per revolution and wheel diameter must be positive");
        }
        
        double circumference = Math.PI * wheelDiameter;
        return (ticks * circumference) / ticksPerRevolution;
    }
    
    public static int inchesToTicks(double inches, int ticksPerRevolution, double wheelDiameter) {
        if (ticksPerRevolution <= 0 || wheelDiameter <= 0) {
            throw new IllegalArgumentException("Ticks per revolution and wheel diameter must be positive");
        }
        
        double circumference = Math.PI * wheelDiameter;
        return (int) Math.round((inches * ticksPerRevolution) / circumference);
    }
    
    public static double calculatePowerForDistance(double distance, double time, double maxPower) {
        if (distance < 0 || time <= 0 || maxPower <= 0 || maxPower > 1.0) {
            throw new IllegalArgumentException("Invalid parameters for power calculation");
        }
        
        double requiredPower = distance / (time * 12.0);
        return Math.min(requiredPower, maxPower);
    }
}

// RobotController.java
public class RobotController {
    private DcMotor leftMotor;
    private DcMotor rightMotor;
    private ColorSensor colorSensor;
    
    public RobotController(DcMotor leftMotor, DcMotor rightMotor, ColorSensor colorSensor) {
        this.leftMotor = leftMotor;
        this.rightMotor = rightMotor;
        this.colorSensor = colorSensor;
    }
    
    public void driveForward(double power) {
        if (power < -1.0 || power > 1.0) {
            throw new IllegalArgumentException("Power must be between -1.0 and 1.0");
        }
        
        leftMotor.setPower(power);
        rightMotor.setPower(power);
    }
    
    public void turn(double leftPower, double rightPower) {
        if (Math.abs(leftPower) > 1.0 || Math.abs(rightPower) > 1.0) {
            throw new IllegalArgumentException("Power values must be between -1.0 and 1.0");
        }
        
        leftMotor.setPower(leftPower);
        rightMotor.setPower(rightPower);
    }
    
    public boolean isRedBallDetected() {
        int red = colorSensor.red();
        int green = colorSensor.green();
        int blue = colorSensor.blue();
        
        return red > green && red > blue && red > 100;
    }
    
    public void stop() {
        leftMotor.setPower(0.0);
        rightMotor.setPower(0.0);
    }
    
    public double getLeftMotorPower() {
        return leftMotor.getPower();
    }
    
    public double getRightMotorPower() {
        return rightMotor.getPower();
    }
}

// AutonomousSequence.java
public class AutonomousSequence {
    private RobotController robotController;
    private double startTime;
    private boolean isComplete = false;
    
    public AutonomousSequence(RobotController robotController) {
        this.robotController = robotController;
    }
    
    public void start(double currentTime) {
        this.startTime = currentTime;
        this.isComplete = false;
    }
    
    public void update(double currentTime) {
        if (isComplete) return;
        
        double elapsedTime = currentTime - startTime;
        
        if (elapsedTime < 2.0) {
            robotController.driveForward(0.5);
        } else {
            robotController.stop();
            isComplete = true;
        }
    }
    
    public boolean isComplete() {
        return isComplete;
    }
    
    public double getElapsedTime(double currentTime) {
        return currentTime - startTime;
    }
}

// ComprehensiveRobotTest.java
@RunWith(MockitoJUnitRunner.class)
public class ComprehensiveRobotTest {
    
    @Mock
    private DcMotor leftMotor, rightMotor;
    
    @Mock
    private ColorSensor colorSensor;
    
    private RobotController robotController;
    private AutonomousSequence autonomousSequence;
    
    @Before
    public void setUp() {
        robotController = new RobotController(leftMotor, rightMotor, colorSensor);
        autonomousSequence = new AutonomousSequence(robotController);
    }
    
    @Test
    public void testTicksToInches() {
        double result = RobotUtils.ticksToInches(1000, 1440, 4.0);
        double expected = (1000 * Math.PI * 4.0) / 1440;
        assertEquals("Basic ticks to inches conversion", expected, result, 0.001);
    }
    
    @Test
    public void testInchesToTicks() {
        int result = RobotUtils.inchesToTicks(10.0, 1440, 4.0);
        int expected = (int) Math.round((10.0 * 1440) / (Math.PI * 4.0));
        assertEquals("Basic inches to ticks conversion", expected, result);
    }
    
    @Test
    public void testDriveForward() {
        robotController.driveForward(0.5);
        verify(leftMotor).setPower(0.5);
        verify(rightMotor).setPower(0.5);
    }
    
    @Test
    public void testTurn() {
        robotController.turn(-0.4, 0.4);
        verify(leftMotor).setPower(-0.4);
        verify(rightMotor).setPower(0.4);
    }
    
    @Test
    public void testIsRedBallDetected() {
        when(colorSensor.red()).thenReturn(200);
        when(colorSensor.green()).thenReturn(50);
        when(colorSensor.blue()).thenReturn(30);
        
        assertTrue("Should detect red ball", robotController.isRedBallDetected());
    }
    
    @Test
    public void testAutonomousSequence() {
        autonomousSequence.start(0.0);
        
        autonomousSequence.update(1.0);
        verify(robotController).driveForward(0.5);
        assertFalse("Sequence should not be complete", autonomousSequence.isComplete());
        
        autonomousSequence.update(2.0);
        verify(robotController).stop();
        assertTrue("Sequence should be complete", autonomousSequence.isComplete());
    }
    
    @Test
    public void testIntegrationWithRealCalculations() {
        double distance = 24.0;
        int ticksPerRev = 1440;
        double wheelDiameter = 4.0;
        
        int expectedTicks = RobotUtils.inchesToTicks(distance, ticksPerRev, wheelDiameter);
        assertTrue("Ticks should be positive", expectedTicks > 0);
        
        double time = 3.0;
        double maxPower = 1.0;
        double calculatedPower = RobotUtils.calculatePowerForDistance(distance, time, maxPower);
        
        assertTrue("Power should be positive", calculatedPower > 0);
        assertTrue("Power should not exceed maxPower", calculatedPower <= maxPower);
        
        robotController.driveForward(calculatedPower);
        verify(leftMotor).setPower(calculatedPower);
        verify(rightMotor).setPower(calculatedPower);
    }
}

Test Organization and Best Practices

Organizing your tests properly and following best practices makes them more maintainable and effective. Good test organization helps you find and fix issues quickly.

Unit Testing Best Practices

  • Test one thing at a time - each test should verify one specific behavior
  • Use descriptive test names that explain what is being tested
  • Follow the Arrange-Act-Assert pattern: set up, execute, verify
  • Test both normal cases and edge cases (boundary conditions)
  • Test error conditions and exception handling
  • Keep tests independent - they should not depend on each other
  • Use meaningful test data that represents real scenarios

Comprehensive Test Organization

Let's create a comprehensive test class that demonstrates proper test organization with clear naming conventions and thorough coverage.

Comprehensive Test Class - Setup and Drive Tests

// File: ComprehensiveRobotTest.java
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;

@RunWith(MockitoJUnitRunner.class)
public class ComprehensiveRobotTest {
    
    @Mock
    private DcMotor leftMotor, rightMotor;
    
    @Mock
    private ColorSensor colorSensor;
    
    private RobotController robotController;
    
    @Before
    public void setUp() {
        robotController = new RobotController(leftMotor, rightMotor, colorSensor);
    }
    
    // Group related tests together with descriptive names
    
    @Test
    public void driveForward_WithPositivePower_ShouldSetBothMotorsToSamePower() {
        // Arrange
        double power = 0.7;
        
        // Act
        robotController.driveForward(power);
        
        // Assert
        verify(leftMotor).setPower(power);
        verify(rightMotor).setPower(power);
    }
    
    @Test
    public void driveForward_WithNegativePower_ShouldDriveBackward() {
        // Arrange
        double power = -0.5;
        
        // Act
        robotController.driveForward(power);
        
        // Assert
        verify(leftMotor).setPower(power);
        verify(rightMotor).setPower(power);
    }

Comprehensive Test Class - Error Handling Tests

    @Test(expected = IllegalArgumentException.class)
    public void driveForward_WithPowerGreaterThanOne_ShouldThrowException() {
        // Arrange
        double invalidPower = 1.5;
        
        // Act & Assert
        robotController.driveForward(invalidPower);
    }
    
    @Test(expected = IllegalArgumentException.class)
    public void driveForward_WithPowerLessThanNegativeOne_ShouldThrowException() {
        // Arrange
        double invalidPower = -1.2;
        
        // Act & Assert
        robotController.driveForward(invalidPower);
    }
    
    @Test
    public void turn_WithDifferentLeftAndRightPowers_ShouldSetMotorsCorrectly() {
        // Arrange
        double leftPower = -0.3;
        double rightPower = 0.3;
        
        // Act
        robotController.turn(leftPower, rightPower);
        
        // Assert
        verify(leftMotor).setPower(leftPower);
        verify(rightMotor).setPower(rightPower);
    }

Comprehensive Test Class - Sensor Tests

    @Test
    public void isRedBallDetected_WithRedDominantColors_ShouldReturnTrue() {
        // Arrange
        when(colorSensor.red()).thenReturn(200);
        when(colorSensor.green()).thenReturn(50);
        when(colorSensor.blue()).thenReturn(30);
        
        // Act
        boolean result = robotController.isRedBallDetected();
        
        // Assert
        assertTrue("Should detect red ball when red is dominant", result);
    }
    
    @Test
    public void isRedBallDetected_WithBlueDominantColors_ShouldReturnFalse() {
        // Arrange
        when(colorSensor.red()).thenReturn(50);
        when(colorSensor.green()).thenReturn(60);
        when(colorSensor.blue()).thenReturn(200);
        
        // Act
        boolean result = robotController.isRedBallDetected();
        
        // Assert
        assertFalse("Should not detect red ball when blue is dominant", result);
    }
    
    @Test
    public void stop_ShouldSetBothMotorsToZero() {
        // Arrange - no setup needed
        
        // Act
        robotController.stop();
        
        // Assert
        verify(leftMotor).setPower(0.0);
        verify(rightMotor).setPower(0.0);
    }
}

Practice: Advanced Debugging

Try these debugging exercises to reinforce your understanding:

  • Create an OpMode with a state machine that controls a robot drivetrain
  • Intentionally introduce a bug (e.g., wrong state transition condition)
  • Set up conditional breakpoints to catch the bug
  • Use watch expressions to monitor state variables
  • Implement structured logging to track the bug
  • Use remote debugging to verify the fix works on hardware

Additional Resources

Open full interactive app