Hoy me gustaría hablar sobre las pruebas, porque cuanto más pruebas se cubren el código, mejor y más confiable se considera. No hablemos de pruebas unitarias, sino de pruebas de integración de bases de datos. ¿Cuál es exactamente la diferencia entre pruebas unitarias y pruebas de integración? Modular (unitario) es probar un programa a nivel de módulos, métodos o clases individuales, es decir, las pruebas son rápidas y sencillas y afectan las partes más divisibles de la funcionalidad. También se les conoce como “una prueba por método”. Los de integración son más lentos y pesados, y pueden constar de varios módulos y funcionalidades adicionales. ¿Por qué las pruebas para la integración de la capa dao (objeto de acceso a datos)? Porque para probar métodos con consultas a la base de datos, necesitamos crear una base de datos separada en la RAM, reemplazando la principal. La idea es que creemos las tablas que necesitamos, las llenemos con datos de prueba y verifiquemos la exactitud de los métodos de la clase del repositorio (después de todo, sabemos cuál debería ser el resultado final en un caso determinado). Vamos a empezar. Los temas sobre la conexión de una base de datos se han tratado ampliamente durante mucho tiempo y, por lo tanto, hoy no me gustaría detenerme en esto y consideraremos solo las partes del programa que nos interesan. De forma predeterminada, partiremos del hecho de que nuestra aplicación está basada en Spring Boot, para la capa dao Spring JDBC (para mayor claridad), nuestra base de datos principal es MySQL, y la reemplazaremos usando MariaDB (son máximamente compatibles y en consecuencia, los scripts MySQL nunca tendrán conflictos con el dialecto MariaDB, como los habrá con H2). También asumiremos condicionalmente que nuestro programa usa Liquibase para administrar y aplicar cambios al esquema de la base de datos y, en consecuencia, todos los scripts aplicados se almacenan en nuestra aplicación.
Estructura del proyecto
Solo se muestran las partes afectadas: Y sí, hoy crearemos robots)) Script para la tabla, cuyos métodos probaríamos hoy (create_table_robots.sql):CREATE TABLE `robots`
(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`name` CHAR(255) CHARACTER SET utf8 NOT NULL,
`cpu` CHAR(255) CHARACTER SET utf8 NOT NULL,
`producer` CHAR(255) CHARACTER SET utf8 NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
La entidad que representa esta tabla:
@Builder
@Data
public class Robot {
private Long id;
private String name;
private String cpu;
private String producer;
}
Interfaz para el repositorio probado:
public interface RobotDAO {
Robot findById(Long id);
Robot create(Robot robot);
List<Robot> findAll();
Robot update(Robot robot);
void delete(Long id);
}
En realidad, aquí hay operaciones CRUD estándar, sin exóticas, por lo que consideraremos la implementación no de todos los métodos (bueno, esto no sorprenderá a nadie), sino de algunos, para mayor brevedad:
@Repository
@AllArgsConstructor
public class RobotDAOImpl implements RobotDAO {
private static final String FIND_BY_ID = "SELECT id, name, cpu, producer FROM robots WHERE id = ?";
private static final String UPDATE_BY_ID = "UPDATE robots SET name = ?, cpu = ?, producer = ? WHERE id = ?";
@Autowired
private final JdbcTemplate jdbcTemplate;
@Override
public Robot findById(Long id) {
return jdbcTemplate.queryForObject(FIND_BY_ID, robotMapper(), id);
}
@Override
public Robot update(Robot robot) {
jdbcTemplate.update(UPDATE_BY_ID,
robot.getName(),
robot.getCpu(),
robot.getProducer(),
robot.getId());
return robot;
}
private RowMapper<Robot> robotMapper() {
return (rs, rowNum) ->
Robot.builder()
.id(rs.getLong("id"))
.name(rs.getString("name"))
.cpu(rs.getString("cpu"))
.producer(rs.getString("producer"))
.build();
}
Hagamos una pequeña digresión y veamos qué está pasando con nuestras dependencias (solo se presentan aquellas utilizadas para la parte demostrada de la aplicación):
<dependencies>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>2.5.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.craftercms.mariaDB4j</groupId>
<artifactId>mariaDB4j-springboot</artifactId>
<version>2.4.2.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.2.1.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
</dependencies>
4 - dependencia para la base de datos MariaDb en sí 10 - dependencia para conectarse con SpringBoot 16 - Lombok (bueno, creo que todos saben qué tipo de biblioteca es esta) 22 - iniciador para pruebas (donde está integrado el JUnit que necesitamos) 28 - iniciador para trabajando con springJdbc Echemos un vistazo al contenedor Spring con los beans necesarios para nuestras pruebas (en particular, el bean de creación MariaDB):
@Configuration
public class TestConfigDB {
@Bean
public MariaDB4jSpringService mariaDB4jSpringService() {
return new MariaDB4jSpringService();
}
@Bean
public DataSource dataSource(MariaDB4jSpringService mariaDB4jSpringService) {
try {
mariaDB4jSpringService.getDB().createDB("testDB");
} catch (ManagedProcessException e) {
e.printStackTrace();
}
DBConfigurationBuilder config = mariaDB4jSpringService.getConfiguration();
return DataSourceBuilder
.create()
.username("root")
.password("root")
.url(config.getURL("testDB"))
.driverClassName("org.mariadb.jdbc.Driver")
.build();
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
5 - el componente principal para generar MariaDB (para aplicaciones basadas en Spring Framework) 10 - definir un bean de base de datos 12 - establecer el nombre de la base de datos creada 17 - extraer las configuraciones para nuestro caso 19 - construir una base de datos usando el patrón Builder ( una buena descripción general del patrón ) Y finalmente, de lo que se trata todo este alboroto es del bean JdbcTemplate para la comunicación con la base de datos que se está generando. La idea es que tendremos una clase principal para las pruebas de Tao, de la cual heredarán todas las clases de pruebas de Tao, cuyas tareas incluyen:
- ejecutar algunos scripts utilizados en la base de datos principal (scripts para crear tablas, cambiar columnas y otros);
- lanzar scripts de prueba que llenan tablas con datos de prueba;
- eliminando tablas.
@SpringBootTest(classes = TestConfigDB.class)
public abstract class DataBaseIT {
@Autowired
private JdbcTemplate jdbcTemplate;
public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}
public void fillDataBase(String[] initList) {
for (String x : initList) {
try {
jdbcTemplate.update(IOUtils.resourceToString("/db.migrations/" + x, StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void cleanDataBase() {
getJdbcTemplate().update("DROP database testDB");
getJdbcTemplate().update("CREATE database testDB");
getJdbcTemplate().update("USE testDB");
}
public void fillTables(String[] fillList) {
for (String x : fillList) {
try {
Stream.of(
IOUtils.resourceToString("/fill_scripts/" + x, StandardCharsets.UTF_8))
.forEach(jdbcTemplate::update);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
1 - usando la anotación @SpringBootTest establecemos una configuración de prueba 11 - como argumento en este método pasamos los nombres de las tablas que necesitamos y él, como trabajador responsable, las cargará por nosotros (lo que nos da la oportunidad reutilizar este método tanto como nuestro corazón desee) 21 - usamos este método para limpiar, es decir, eliminar todas las tablas (y sus datos) de la base de datos 27 - el argumento en este método es una matriz de nombres de scripts con datos de prueba que se cargará para probar un método específico. Nuestro script con datos de prueba:
INSERT INTO robots(name, cpu, producer)
VALUES ('Rex', 'Intel Core i5-9400F', 'Vietnam'),
('Molly', 'AMD Ryzen 7 2700X', 'China'),
('Ross', 'Intel Core i7-9700K', 'Malaysia')
Y ahora lo que nos hemos reunido todos para hoy.
clase de prueba de tao
@RunWith(SpringRunner.class)
public class RobotDataBaseIT extends DataBaseIT {
private static RobotDAO countryDAO;
@Before
public void fillData() {
fillDataBase(new String[]{
"create_table_robots.sql"
});
countryDAO = new RobotDAOImpl(getJdbcTemplate());
}
@After
public void clean() {
cleanDataBase();
}
private RowMapper<Robot> robotMapper() {
return (rs, rowNum) ->
Robot.builder()
.id(rs.getLong("id"))
.name(rs.getString("name"))
.cpu(rs.getString("cpu"))
.producer(rs.getString("producer"))
.build();
}
2 - heredamos de la clase principal para nuestras pruebas 4 - nuestro repositorio probado 7 - un método que se lanzará antes de cada prueba 8 - usamos el método de la clase principal para cargar las tablas necesarias 11 - inicializamos nuestro dao 15 - un método que se lanzará después de cada prueba, limpiando nuestra base de datos 19 - implementación de nuestro RowMapper, análogo a la clase Tao Usamos @Before y @After, que se usan antes y después de un método de prueba, pero podríamos tomar alguna lib que nos permita utilizar anotaciones vinculadas al inicio de las pruebas de ejecución de esta clase y al final. Por ejemplo, éste , que aceleraría significativamente las pruebas, ya que las tablas tendrían que crearse y eliminarse por completo cada vez y una vez por clase. Pero no hacemos eso. ¿Porque preguntas? ¿Qué pasa si uno de los métodos cambia la estructura de la tabla? Por ejemplo, elimine una columna. En este caso, los métodos restantes pueden fallar o deben responder como se esperaba (por ejemplo, crear una columna posterior). Tenemos que admitir que esto nos da una conexión (dependencia) innecesaria de las pruebas entre sí, lo cual no nos sirve de nada. Pero estoy divagando, sigamos...
Probando el método findById
@Test
public void findByIdTest() {
fillTables(new String[]{"fill_table_robots.sql"});
Long id = getJdbcTemplate().queryForObject("SELECT id FROM robots WHERE name = 'Molly'", Long.class);
Robot robot = countryDAO.findById(id);
assertThat(robot).isNotNull();
assertThat(robot.getId()).isEqualTo(id);
assertThat(robot.getName()).isEqualTo("Molly");
assertThat(robot.getCpu()).isEqualTo("AMD Ryzen 7 2700X");
assertThat(robot.getProducer()).isEqualTo("China");
}
3 - complete la tabla con datos de prueba 5 - obtenga la identificación de la entidad que necesitamos 6 - use el método que se está probando 8...12 - compare los datos recibidos con los esperados
Prueba del método de actualización
@Test
public void updateTest() {
fillTables(new String[]{"fill_table_robots.sql"});
Long robotId = getJdbcTemplate().queryForObject("SELECT id FROM robots WHERE name = 'Rex'", Long.class);
Robot updateRobot = Robot.builder()
.id(robotId)
.name("Aslan")
.cpu("Intel Core i5-3470")
.producer("Narnia")
.build();
Robot responseRobot = countryDAO.update(updateRobot);
Robot updatedRobot = getJdbcTemplate().queryForObject(
"SELECT id, name, cpu, producer FROM robots WHERE id = ?",
robotMapper(),
robotId);
assertThat(updatedRobot).isNotNull();
assertThat(updateRobot.getName()).isEqualTo(responseRobot.getName());
assertThat(updateRobot.getName()).isEqualTo(updatedRobot.getName());
assertThat(updateRobot.getCpu()).isEqualTo(responseRobot.getCpu());
assertThat(updateRobot.getCpu()).isEqualTo(updatedRobot.getCpu());
assertThat(updateRobot.getProducer()).isEqualTo(responseRobot.getProducer());
assertThat(updateRobot.getProducer()).isEqualTo(updatedRobot.getProducer());
assertThat(responseRobot.getId()).isEqualTo(updatedRobot.getId());
assertThat(updateRobot.getId()).isEqualTo(updatedRobot.getId());
}
3 - complete la tabla con datos de prueba 5 - obtenga la identificación de la entidad que se está actualizando 7 - cree la entidad actualizada 14 - use el método que se está probando 15 - obtenga la entidad actualizada para verificación 20...28 - compare los datos recibidos con los esperados Probar el método de actualización es similar a crear. Al menos para mi. Puedes torcer las conciliaciones tanto como quieras: nunca puede haber demasiados controles. También me gustaría señalar que las pruebas no garantizan la funcionalidad completa ni la ausencia de errores. Las pruebas sólo garantizan que el resultado real del programa (su fragmento) corresponda al esperado. En este caso, solo se verifican aquellas partes para las cuales se escribieron las pruebas.
Empecemos una clase con pruebas...
Victoria)) Vamos a preparar té y comprar galletas: nos lo merecemos))Enlaces útiles
- Un buen artículo sobre la parte no examinada de esta pila de tecnología.
- Una interesante serie de artículos sobre Maven, Spring, MySQL, Hibernate.
- Refresquemos un poco la memoria sobre las pruebas unitarias.
- Un ejemplo interesante de pruebas de integración, pero con sustitución de PostgreSQL.
- Mucha gente, y no sólo en las pruebas, utiliza MariaDB en lugar de MySql.
*música épica de Star Wars*
GO TO FULL VERSION