Unit Testing

Unit testing is a method of testing code by dividing the code into the smallest «units» possible and testing each unit. In robot code, this can mean testing the code for each subsystem individually. There are many unit testing frameworks for most languages. Java robot projects have JUnit 4 available by default, and C++ robot projects have Google Test.

Escribir código para pruebas

Nota

Este ejemplo se puede adaptar fácilmente al paradigma basado en comandos haciendo que Intake herede de SubsystemBase.

Nuestro subsistema será un mecanismo Intake para Infinite Recharge que contiene un pistón y un motor: el pistón despliega/retrae la admisión y el motor arrastrará las Power Cells hacia adentro. No queremos que el motor funcione si el mecanismo de admisión no está desplegado porque no hará nada.

Para proporcionar una «pizarra limpia» para cada prueba, necesitamos tener una función para destruir el objeto y liberar todas las asignaciones de hardware. En Java, esto se hace implementando la interfaz AutoCloseable y su método .close(), destruyendo cada objeto de los miembros al llamar al método .close() del objeto - un objeto sin un método .close() probablemente no necesite ser cerrado. En C ++, el destructor predeterminado se llamará automáticamente cuando el objeto salga del alcance y llamará a los destructores de los objetos miembros.

Nota

Es posible que los proveedores no admitan el cierre de recursos de la misma manera que se muestra aquí. Consulte la documentación de su proveedor para obtener más información sobre qué admiten y cómo.

import edu.wpi.first.wpilibj.DoubleSolenoid;
import edu.wpi.first.wpilibj.PWMSparkMax;
import frc.robot.Constants.IntakeConstants;

public class Intake implements AutoCloseable {
  private PWMSparkMax motor;
  private DoubleSolenoid piston;

  public Intake() {
    motor = new PWMSparkMax(IntakeConstants.MOTOR_PORT);
    piston = new DoubleSolenoid(PneumaticsModuleType.CTREPCM, IntakeConstants.PISTON_FWD, IntakeConstants.PISTON_REV);
  }

  public void deploy() {
    piston.set(DoubleSolenoid.Value.kForward);
  }

  public void retract() {
    piston.set(DoubleSolenoid.Value.kReverse);
    motor.set(0); // turn off the motor
  }

  public void activate(double speed) {
    if (piston.get() == DoubleSolenoid.Value.kForward) {
      motor.set(speed);
    } else { // if piston isn't open, do nothing
      motor.set(0);
    }
  }

  @Override
  public void close() throws Exception {
    piston.close();
    motor.close();
  }
}

Writing Tests

Importante

Los tests se colocan dentro del conjunto de fuentes de test: /src/test/java/ y /src/test/cpp/ para tests de Java y C++ respectivamente. Los archivos fuera de esa raíz de origen no tienen acceso al marco de prueba; esto fallará en la compilación debido a referencias sin resolver.

En Java, cada clase de prueba contiene al menos un método de prueba marcado con @org.junit.Test, cada método representa un caso de prueba. Los métodos adicionales para abrir recursos (como nuestro objeto Intake) antes de cada prueba y cerrarlos después están marcados respectivamente con @org.junit.Before y @org.junit.After. En C++, las clases de test fixture que heredan de testing::Test contienen nuestros objetos de subsistema y hardware de simulación, y los métodos de prueba se escriben utilizando la macro TEST_F(testfixture, testname). Los métodos SetUp() y TearDown() pueden ser sobreescritos en la clase test fixture y se ejecutarán respectivamente antes y después de cada prueba.

Cada método de prueba debe contener al menos una aserción (assert*() en Java o EXPECT_*() en C++). Estas aserciones verifican una condición en tiempo de ejecución y fallan la prueba si la condición no se cumple. Si hay más de una aserción en un método de prueba, la primera aserción fallida bloqueará la prueba - la ejecución no llegará a las aserciones posteriores.

Tanto JUnit como GoogleTest tienen múltiples tipos de aserción, pero la más común es la de igualdad: assertEquals(expected, actual)/EXPECT_EQ(expected, actual)`. Cuando se comparan números, se puede dar un tercer parámetro: delta, el error aceptable. En JUnit (Java), estas aserciones son métodos estáticos y se pueden utilizar sin calificación añadiendo la estrella estática import import static org.junit.Asssert.*. En Google Test (C++), las aserciones son macros de la cabecera <gtest/gtest.h>.

Nota

La comparación de valores de punto flotante no es precisa, por lo que la comparación debe hacerse con un parámetro de error aceptable (DELTA).

import static org.junit.Assert.*;

import edu.wpi.first.hal.HAL;
import edu.wpi.first.wpilibj.DoubleSolenoid;
import edu.wpi.first.wpilibj.simulation.DoubleSolenoidSim;
import edu.wpi.first.wpilibj.simulation.PWMSim;
import frc.robot.Constants.IntakeConstants;
import org.junit.*;

public class IntakeTest {
  public static final double DELTA = 1e-2; // acceptable deviation range
  Intake intake;
  PWMSim simMotor;
  DoubleSolenoidSim simPiston;

  @Before // this method will run before each test
  public void setup() {
    assert HAL.initialize(500, 0); // initialize the HAL, crash if failed
    intake = new Intake(); // create our intake
    simMotor = new PWMSim(IntakeConstants.MOTOR_PORT); // create our simulation PWM motor controller
    simPiston = new DoubleSolenoidSim(PneumaticsModuleType.CTREPCM, IntakeConstants.PISTON_FWD, IntakeConstants.PISTON_REV); // create our simulation solenoid
  }

  @After // this method will run after each test
  public void shutdown() throws Exception {
    intake.close(); // destroy our intake object
  }

  @Test // marks this method as a test
  public void doesntWorkWhenClosed() {
    intake.retract(); // close the intake
    intake.activate(0.5); // try to activate the motor
    assertEquals(0.0, simMotor.getSpeed(), DELTA); // make sure that the value set to the motor is 0
  }

  @Test
  public void worksWhenOpen() {
    intake.deploy();
    intake.activate(0.5);
    assertEquals(0.5, simMotor.getSpeed(), DELTA);
  }

  @Test
  public void retractTest() {
    intake.retract();
    assertEquals(DoubleSolenoid.Value.kReverse, simPiston.get());
  }

  @Test
  public void deployTest() {
    intake.deploy();
    assertEquals(DoubleSolenoid.Value.kForward, simPiston.get());
  }
}

Para un uso más avanzado de JUnit y Google Test, consulte la documentación del framework.

Pruebas para correrlo

Nota

Las pruebas se ejecutarán siempre en simulación en su escritorio. Para conocer los requisitos previos y más información, consulte el documento introducción a la simulación.

Para que las pruebas Java se ejecuten, asegúrese de que su archivo build.gradle contenga el siguiente bloque:

test {
   useJUnit()
}

Utilice Test Robot Code de la paleta de comandos para ejecutar las pruebas. Los resultados serán reportados en la salida de la terminal, cada prueba tendrá una etiqueta FAILED o PASSED/OK junto al nombre de la prueba en la salida. JUnit (sólo en Java) generará un documento HTML en build/reports/tests/test/index.html con un resumen más detallado de los resultados; si hay pruebas fallidas se imprimirá en la salida de la terminal un enlace para renderizar el documento en el navegador.

Por defecto, Gradle ejecuta las pruebas cada vez que se construye el código del robot, incluyendo los despliegues. Esto aumentará el tiempo de despliegue, y las pruebas fallidas harán que la construcción y el despliegue fallen. Para evitar que esto ocurra, puedes utilizar Change Skip Tests On Deploy Setting de la paleta de comandos para configurar si se ejecutan las pruebas al desplegar.