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();
}
}
#include <frc2/command/SubsystemBase.h>
#include <frc/DoubleSolenoid.h>
#include <frc/PWMSparkMax.h>
#include "Constants.h"
class Intake : public frc2::SubsystemBase {
public:
void Deploy();
void Retract();
void Activate(double speed);
private:
frc::PWMSparkMax motor{Constants::Intake::MOTOR_PORT};
frc::DoubleSolenoid piston{frc::PneumaticsModuleType::CTREPCM, Constants::Intake::PISTON_FWD, Constants::Intake::PISTON_REV};
};
#include "subsystems/Intake.h"
void Intake::Deploy() {
piston.Set(frc::DoubleSolenoid::Value::kForward);
}
void Intake::Retract() {
piston.Set(frc::DoubleSolenoid::Value::kReverse);
motor.Set(0); // turn off the motor
}
void Intake::Activate(double speed) {
if (piston.Get() == frc::DoubleSolenoid::Value::kForward) {
motor.Set(speed);
} else { // if piston isn't open, do nothing
motor.Set(0);
}
}
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());
}
}
#include <gtest/gtest.h>
#include <frc/DoubleSolenoid.h>
#include <frc/simulation/DoubleSolenoidSim.h>
#include <frc/simulation/PWMSim.h>
#include "subsystems/Intake.h"
#include "Constants.h"
class IntakeTest : public testing::Test {
protected:
Intake intake; // create our intake
frc::sim::PWMSim simMotor{Constants::Intake::MOTOR_PORT}; // create our simulation PWM
frc::sim::DoubleSolenoidSim simPiston{frc::PneumaticsModuleType::CTREPCM, Constants::Intake::PISTON_FWD, Constants::Intake::PISTON_REV}; // create our simulation solenoid
};
TEST_F(IntakeTest, DoesntWorkWhenClosed) {
intake.Retract(); // close the intake
intake.Activate(0.5); // try to activate the motor
EXPECT_DOUBLE_EQ(0.0, simMotor.GetSpeed()); // make sure that the value set to the motor is 0
}
TEST_F(IntakeTest, WorksWhenOpen) {
intake.Deploy();
intake.Activate(0.5);
EXPECT_DOUBLE_EQ(0.5, simMotor.GetSpeed());
}
TEST_F(IntakeTest, RetractTest) {
intake.Retract();
EXPECT_EQ(frc::DoubleSolenoid::Value::kReverse, simPiston.Get());
}
TEST_F(IntakeTest, DeployTest) {
intake.Deploy();
EXPECT_EQ(frc::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.