Testes de Integração no Quarkus usando TestContainers
- Testes de Integração
- 23 de outubro de 2022
Quase toda aplicação utiliza algum recurso externo, como banco de dados (MySQL, PostgreSQL, Oracle), mensageria (ActiveMQ Artemis, Apache Kafka), cache (Redis), serviço de autenticação (KeyCloak), entre outros. Essas dependências acabam adicionando uma complexidade extra na hora de realizar testes de integração, pois além de rodar os testes, teria que ter esses serviços externos rodando, ou serem substituídos por dependências mais simples.
É comum por exemplo, ao realizar um teste de integração em uma aplicação que utiliza banco de dados, utilizar um banco de dados em memória (como H2 por exemplo), para não depender de uma dependência externa. O problema é que nem sempre o banco de dados em memória vai ter todas as funcionalidades utilizadas na aplicação, ou mesmo vai se comportar exatamente da mesma forma.
Uma alternativa bem interessante é utilizar a biblioteca Testcontainers, que permite subir instâncias de banco de dados ou qualquer serviço que rode em um container Docker durante os testes.
Info
A partir do Quarkus 1.11, foi introduzido o Dev Services, recurso que automatiza a inicialização de dependências como PostgreSQL, Kafka e Redis durante o desenvolvimento e execução de testes. Ele cobre a maioria das necessidades de testes de integração sem a necessidade de configurações adicionais. Por baixo, o Dev Services utiliza Testcontainers para provisionar os serviços necessários.
Mesmo com a chegada do Dev Services, o uso direto do Testcontainers continua sendo uma opção importante em cenários mais avançados, como quando é necessário controlar versões específicas dos serviços, utilizar imagens customizadas, orquestrar múltiplos containers ou reproduzir ambientes mais próximos da produção.
De modo geral, recomendo a utilização do Dev Services sempre que possível, por sua simplicidade e integração nativa com o Quarkus. O uso direto do Testcontainers deve ser considerado quando houver requisitos de customização que extrapolem os recursos oferecidos pelo Dev Services.
Criando o projeto Quarkus
Vamos criar o projeto utilizando o Quarkus CLI:
- Java + Maven
- Kotlin + Gradle
quarkus create app com.renanwillian:quarkus-testcontainers --extension=resteasy,resteasy-jackson,flyway,hibernate-orm-panache,jdbc-postgresqlquarkus create app com.renanwillian:quarkus-testcontainers --extension=kotlin,resteasy,resteasy-jackson,flyway,hibernate-orm-panache-kotlin,jdbc-postgresql --gradle-kotlin-dslquarkus create app com.renanwillian:quarkus-testcontainers --extension=resteasy,resteasy-jackson,flyway,hibernate-orm-panache,jdbc-postgresqlquarkus create app com.renanwillian:quarkus-testcontainers --extension=kotlin,resteasy,resteasy-jackson,flyway,hibernate-orm-panache-kotlin,jdbc-postgresql --gradle-kotlin-dslConfiguração do banco + flyway
Arquivo: /resources/application.properties
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=postgres
quarkus.datasource.password=password
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5435/postgres
quarkus.flyway.locations=db/migration
quarkus.flyway.clean-at-start=true
quarkus.flyway.migrate-at-start=true
/resources/db/migration/V1__init.sql
CREATE TABLE car (
id BIGSERIAL NOT NULL,
make VARCHAR(255) NOT NULL,
model VARCHAR(255) NOT NULL,
year INTEGER NOT NULL
);
ALTER TABLE car ADD CONSTRAINT pk_car PRIMARY KEY (id);
INSERT INTO car (make, model, year) VALUES ('Mercedes-Benz', '500 SL', 1992);
Classes
- Java
- Kotlin
Domain:
@Entity
public class Car {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String make;
private String model;
private Integer year;
// getters / setters
}DTO:
public class CarRequest implements Serializable {
private static final long serialVersionUID = 7937264281409749414L;
private String make;
private String model;
private Integer year;
public CarRequest() {}
public CarRequest(String make, String model, Integer year) {
this.make = make;
this.model = model;
this.year = year;
}
public Car toDomain() {
Car car = new Car();
car.setMake(make);
car.setModel(model);
car.setYear(year);
return car;
}
// getters / setters
}
public class CarResponse implements Serializable {
private static final long serialVersionUID = 2348428287037849372L;
private Long id;
private String make;
private String model;
private Integer year;
public CarResponse(Car car) {
this.id = car.getId();
this.make = car.getMake();
this.model = car.getModel();
this.year = car.getYear();
}
// getters / setters
}
Repository:
@ApplicationScoped
public class CarRepository implements PanacheRepository<Car> {
public Long countByMakeModelYear(String make, String model, Integer year) {
return count("lower(make) = lower(?1) AND lower(model) = lower(?2) AND year = ?3", make, model, year)
}
}Service:
@ApplicationScoped
@Transactional
public class CarService {
@Inject
CarRepository carRepository;
public List<CarResponse> list() {
return carRepository.listAll().stream().map(CarResponse::new).collect(Collectors.toList());
}
public CarResponse create(CarRequest carRequest) {
if (carRepository.countByMakeModelYear(carRequest.getMake(), carRequest.getModel(), carRequest.getYear()) > 0) {
throw new BadRequestException();
}
Car car = carRequest.toDomain();
carRepository.persist(car);
return new CarResponse(car);
}
public CarResponse find(Long id) {
return new CarResponse(findById(id));
}
public CarResponse update(Long id, CarRequest carRequest) {
Car car = findById(id);
car.setMake(carRequest.getMake());
car.setModel(carRequest.getModel());
car.setYear(carRequest.getYear());
carRepository.persist(car);
return new CarResponse(car);
}
public void delete(Long id) {
carRepository.delete(findById(id));
}
public Car findById(Long id) {
return carRepository.findByIdOptional(id).orElseThrow(NotFoundException::new);
}
}Resources:
@Path("/cars")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class CarResources {
@Inject
CarService carService;
@GET
public Response list() {
return Response.ok(carService.list()).build();
}
@POST
public Response create(CarRequest carRequest) {
return Response.status(Response.Status.CREATED.getStatusCode())
.entity(carService.create(carRequest))
.build();
}
@GET
@Path("/{id}")
public Response find(@PathParam("id") Long id) {
return Response.ok(carService.find(id)).build();
}
@PUT
@Path("/{id}")
public Response update(@PathParam("id") Long id, CarRequest carRequest) {
return Response.ok(carService.update(id, carRequest)).build();
}
@DELETE
@Path("/{id}")
public Response delete(@PathParam("id") Long id) {
carService.delete(id);
return Response.noContent().build();
}
}Domain:
@Entity
data class Car(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long?,
var make: String,
var model: String,
var year: Int
)DTO:
data class CarRequest(
val make: String,
val model: String,
val year: Int
)
data class CarResponse(
val id: Long,
val make: String,
val model: String,
val year: Int
)
fun CarRequest.toDomain(): Car {
return Car(
id = null,
make = make,
model = model,
year = year
)
}
fun Car.toResponse(): CarResponse {
return CarResponse(
id = id!!,
make = make,
model = model,
year = year,
)
}Repository:
@ApplicationScoped
class CarRepository: PanacheRepository<Car> {
fun countByMakeModelYear(make: String, model: String, year: Int): Long {
return count("lower(make) = lower(?1) AND lower(model) = lower(?2) AND year = ?3", make, model, year)
}
}Service:
@ApplicationScoped
@Transactional
class CarService {
@Inject
private lateinit var carRepository: CarRepository
fun list(): List<CarResponse> {
return carRepository.listAll().map { it.toResponse() }
}
fun create(carRequest: CarRequest): CarResponse {
if (carRepository.countByMakeModelYear(carRequest.make, carRequest.model, carRequest.year) > 0) throw BadRequestException()
val car = carRequest.toDomain()
carRepository.persist(car)
return car.toResponse()
}
fun find(id: Long): CarResponse {
return findById(id).toResponse()
}
fun update(id: Long, carRequest: CarRequest): CarResponse {
val car = findById(id)
car.make = carRequest.make
car.model = carRequest.model
car.year = carRequest.year
carRepository.persist(car)
return car.toResponse()
}
fun delete(id: Long) {
carRepository.delete(findById(id))
}
private fun findById(id: Long): Car {
return carRepository.findById(id) ?: throw NotFoundException()
}
}Resources:
@Path("/cars")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
class CarResources {
@Inject
private lateinit var carService: CarService
@GET
fun list(): Response {
return Response.ok(carService.list()).build()
}
@POST
fun create(carRequest: CarRequest): Response {
return Response.status(Status.CREATED.statusCode)
.entity(carService.create(carRequest))
.build()
}
@GET
@Path("/{id}")
fun find(@PathParam("id") id: Long): Response {
return Response.ok(carService.find(id)).build()
}
@PUT
@Path("/{id}")
fun update(@PathParam("id") id: Long, carRequest: CarRequest): Response {
return Response.ok(carService.update(id, carRequest)).build()
}
@DELETE
@Path("/{id}")
fun delete(@PathParam("id") id: Long): Response {
carService.delete(id)
return Response.noContent().build()
}
}Domain:
@Entity
public class Car {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String make;
private String model;
private Integer year;
// getters / setters
}DTO:
public class CarRequest implements Serializable {
private static final long serialVersionUID = 7937264281409749414L;
private String make;
private String model;
private Integer year;
public CarRequest() {}
public CarRequest(String make, String model, Integer year) {
this.make = make;
this.model = model;
this.year = year;
}
public Car toDomain() {
Car car = new Car();
car.setMake(make);
car.setModel(model);
car.setYear(year);
return car;
}
// getters / setters
}
public class CarResponse implements Serializable {
private static final long serialVersionUID = 2348428287037849372L;
private Long id;
private String make;
private String model;
private Integer year;
public CarResponse(Car car) {
this.id = car.getId();
this.make = car.getMake();
this.model = car.getModel();
this.year = car.getYear();
}
// getters / setters
}
Repository:
@ApplicationScoped
public class CarRepository implements PanacheRepository<Car> {
public Long countByMakeModelYear(String make, String model, Integer year) {
return count("lower(make) = lower(?1) AND lower(model) = lower(?2) AND year = ?3", make, model, year)
}
}Service:
@ApplicationScoped
@Transactional
public class CarService {
@Inject
CarRepository carRepository;
public List<CarResponse> list() {
return carRepository.listAll().stream().map(CarResponse::new).collect(Collectors.toList());
}
public CarResponse create(CarRequest carRequest) {
if (carRepository.countByMakeModelYear(carRequest.getMake(), carRequest.getModel(), carRequest.getYear()) > 0) {
throw new BadRequestException();
}
Car car = carRequest.toDomain();
carRepository.persist(car);
return new CarResponse(car);
}
public CarResponse find(Long id) {
return new CarResponse(findById(id));
}
public CarResponse update(Long id, CarRequest carRequest) {
Car car = findById(id);
car.setMake(carRequest.getMake());
car.setModel(carRequest.getModel());
car.setYear(carRequest.getYear());
carRepository.persist(car);
return new CarResponse(car);
}
public void delete(Long id) {
carRepository.delete(findById(id));
}
public Car findById(Long id) {
return carRepository.findByIdOptional(id).orElseThrow(NotFoundException::new);
}
}Resources:
@Path("/cars")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class CarResources {
@Inject
CarService carService;
@GET
public Response list() {
return Response.ok(carService.list()).build();
}
@POST
public Response create(CarRequest carRequest) {
return Response.status(Response.Status.CREATED.getStatusCode())
.entity(carService.create(carRequest))
.build();
}
@GET
@Path("/{id}")
public Response find(@PathParam("id") Long id) {
return Response.ok(carService.find(id)).build();
}
@PUT
@Path("/{id}")
public Response update(@PathParam("id") Long id, CarRequest carRequest) {
return Response.ok(carService.update(id, carRequest)).build();
}
@DELETE
@Path("/{id}")
public Response delete(@PathParam("id") Long id) {
carService.delete(id);
return Response.noContent().build();
}
}Domain:
@Entity
data class Car(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long?,
var make: String,
var model: String,
var year: Int
)DTO:
data class CarRequest(
val make: String,
val model: String,
val year: Int
)
data class CarResponse(
val id: Long,
val make: String,
val model: String,
val year: Int
)
fun CarRequest.toDomain(): Car {
return Car(
id = null,
make = make,
model = model,
year = year
)
}
fun Car.toResponse(): CarResponse {
return CarResponse(
id = id!!,
make = make,
model = model,
year = year,
)
}Repository:
@ApplicationScoped
class CarRepository: PanacheRepository<Car> {
fun countByMakeModelYear(make: String, model: String, year: Int): Long {
return count("lower(make) = lower(?1) AND lower(model) = lower(?2) AND year = ?3", make, model, year)
}
}Service:
@ApplicationScoped
@Transactional
class CarService {
@Inject
private lateinit var carRepository: CarRepository
fun list(): List<CarResponse> {
return carRepository.listAll().map { it.toResponse() }
}
fun create(carRequest: CarRequest): CarResponse {
if (carRepository.countByMakeModelYear(carRequest.make, carRequest.model, carRequest.year) > 0) throw BadRequestException()
val car = carRequest.toDomain()
carRepository.persist(car)
return car.toResponse()
}
fun find(id: Long): CarResponse {
return findById(id).toResponse()
}
fun update(id: Long, carRequest: CarRequest): CarResponse {
val car = findById(id)
car.make = carRequest.make
car.model = carRequest.model
car.year = carRequest.year
carRepository.persist(car)
return car.toResponse()
}
fun delete(id: Long) {
carRepository.delete(findById(id))
}
private fun findById(id: Long): Car {
return carRepository.findById(id) ?: throw NotFoundException()
}
}Resources:
@Path("/cars")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
class CarResources {
@Inject
private lateinit var carService: CarService
@GET
fun list(): Response {
return Response.ok(carService.list()).build()
}
@POST
fun create(carRequest: CarRequest): Response {
return Response.status(Status.CREATED.statusCode)
.entity(carService.create(carRequest))
.build()
}
@GET
@Path("/{id}")
fun find(@PathParam("id") id: Long): Response {
return Response.ok(carService.find(id)).build()
}
@PUT
@Path("/{id}")
fun update(@PathParam("id") id: Long, carRequest: CarRequest): Response {
return Response.ok(carService.update(id, carRequest)).build()
}
@DELETE
@Path("/{id}")
fun delete(@PathParam("id") id: Long): Response {
carService.delete(id)
return Response.noContent().build()
}
}Adicionando o Testcontainers
Para utilizar essa biblioteca, é necessário que o docker esteja instalado nas máquinas que forem rodar os testes. O primeiro passo é adicionar a dependência do testcontainers e do container postgresql:
- pom.xml
- build.gradle
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.17.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.17.3</version>
<scope>test</scope>
</dependency>testImplementation("org.testcontainers:testcontainers:1.17.3")
testImplementation("org.testcontainers:postgresql:1.17.3")<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.17.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.17.3</version>
<scope>test</scope>
</dependency>testImplementation("org.testcontainers:testcontainers:1.17.3")
testImplementation("org.testcontainers:postgresql:1.17.3")Depois é necessário adicionar um LifecycleManager com a configuração do container Postgres:
- Java
- Kotlin
public class PostgresTestLifecycleManager implements QuarkusTestResourceLifecycleManager {
public static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:12.2");
@Override
public Map<String, String> start() {
POSTGRES.start();
Map<String, String> properties = new HashMap<>();
properties.put("quarkus.datasource.jdbc.url", POSTGRES.getJdbcUrl());
properties.put("quarkus.datasource.username", POSTGRES.getUsername());
properties.put("quarkus.datasource.password", POSTGRES.getPassword());
return properties;
}
@Override
public void stop() {
if (POSTGRES.isRunning()) {
POSTGRES.stop();
}
}
}class PostgresTestLifecycleManager: QuarkusTestResourceLifecycleManager {
companion object {
val db: PostgreSQLContainer<Nothing> = PostgreSQLContainer("postgres:12.2")
}
override fun start(): MutableMap<String, String> {
db.start()
val properties = hashMapOf<String, String>()
properties["quarkus.datasource.jdbc.url"] = db.jdbcUrl
properties["quarkus.datasource.username"] = db.username
properties["quarkus.datasource.password"] = db.password
return properties
}
override fun stop() {
if (db.isRunning) {
db.stop()
}
}
}public class PostgresTestLifecycleManager implements QuarkusTestResourceLifecycleManager {
public static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:12.2");
@Override
public Map<String, String> start() {
POSTGRES.start();
Map<String, String> properties = new HashMap<>();
properties.put("quarkus.datasource.jdbc.url", POSTGRES.getJdbcUrl());
properties.put("quarkus.datasource.username", POSTGRES.getUsername());
properties.put("quarkus.datasource.password", POSTGRES.getPassword());
return properties;
}
@Override
public void stop() {
if (POSTGRES.isRunning()) {
POSTGRES.stop();
}
}
}class PostgresTestLifecycleManager: QuarkusTestResourceLifecycleManager {
companion object {
val db: PostgreSQLContainer<Nothing> = PostgreSQLContainer("postgres:12.2")
}
override fun start(): MutableMap<String, String> {
db.start()
val properties = hashMapOf<String, String>()
properties["quarkus.datasource.jdbc.url"] = db.jdbcUrl
properties["quarkus.datasource.username"] = db.username
properties["quarkus.datasource.password"] = db.password
return properties
}
override fun stop() {
if (db.isRunning) {
db.stop()
}
}
}Ali em properties estamos sobrescrevendo as propriedades de conexão do banco de dados com os valores do container postgres.
Agora todos os testes de integração que precisarem utilizar o container configurado, basta anotar os testes com o recurso @QuarkusTestResource(PostgresTestLifecycleManager::class), o melhor é que podemos utilizar quantos containers docker forem necessários para atender a necessidade do teste.
- Java
- Kotlin
@QuarkusTest
@QuarkusTestResource(PostgresTestLifecycleManager.class)
public class CarServiceIT {
@Inject
Flyway flyway;
@Inject
CarService carService;
@Inject
CarRepository carRepository;
@BeforeEach
void setUp() {
flyway.clean();
flyway.migrate();
}
@Test
void whenListMethodIsCalledAllCarsAreReturned() {
var list = carService.list();
assertEquals(1, list.size());
assertEquals(1, list.get(0).getId());
assertEquals("Mercedes-Benz", list.get(0).getMake());
assertEquals("500 SL", list.get(0).getModel());
assertEquals(1992, list.get(0).getYear());
}
@Test
void whenCreateMethodIsCalledWithValidDataCarResponseIsReturned() {
var carRequest = new CarRequest("BMW", "1 Series", 2013);
var carResponse = carService.create(carRequest);
assertEquals(2, carResponse.getId());
assertEquals("BMW", carResponse.getMake());
assertEquals("1 Series", carResponse.getModel());
assertEquals(2013, carResponse.getYear());
}
@Test
void whenCreateMethodIsCalledWithDuplicatedDataThrowsBadRequestException() {
var carRequest = new CarRequest("Mercedes-Benz", "500 SL", 1992);
assertThrowsExactly(BadRequestException.class, () -> carService.create(carRequest));
}
@Test
void whenUpdateMethodIsCalledWithValidDataCarResponseIsReturned() {
var carRequest = new CarRequest( "BMW", "1 Series", 2013);
var carResponse = carService.update(1L, carRequest);
assertEquals(1, carResponse.getId());
assertEquals("BMW", carResponse.getMake());
assertEquals("1 Series", carResponse.getModel());
assertEquals(2013, carResponse.getYear());
}
@Test
void whenFindMethodIsCalledWithValidIdCarResponseIsReturned() {
var carResponse = carService.find(1L);
assertEquals(1, carResponse.getId());
assertEquals("Mercedes-Benz", carResponse.getMake());
assertEquals("500 SL", carResponse.getModel());
assertEquals(1992, carResponse.getYear());
}
@Test
void whenFindMethodIsCalledWithInvalidIdThrowsNotFoundException() {
assertThrowsExactly(NotFoundException.class, () -> carService.find(500L));
}
@Test
void whenDeleteMethodIsCalledWithValidIdTheCarIsDeleted() {
carService.delete(1L);
assertNull(carRepository.findById(1L));
}
@Test
void whenDeleteMethodIsCalledWithInvalidIdThrowsNotFoundException() {
assertThrowsExactly(NotFoundException.class, () -> carService.delete(500L));
}
}@QuarkusTest
@QuarkusTestResource(PostgresTestLifecycleManager::class)
class CarServiceIT {
@Inject
lateinit var flyway: Flyway
@Inject
lateinit var carService: CarService
@Inject
lateinit var carRepository: CarRepository
@BeforeEach
fun setUp() {
flyway.clean()
flyway.migrate()
}
@Test
fun `when list method is called all cars is returned`() {
val list = carService.list()
assertEquals(1, list.size)
assertEquals(1, list[0].id)
assertEquals("Mercedes-Benz", list[0].make)
assertEquals("500 SL", list[0].model)
assertEquals(1992, list[0].year)
}
@Test
fun `when create method is called with valid data a carResponse is returned`() {
val carRequest = CarRequest(make = "BMW", model = "1 Series", year = 2013)
val carResponse = carService.create(carRequest)
assertEquals(2, carResponse.id)
assertEquals("BMW", carResponse.make)
assertEquals("1 Series", carResponse.model)
assertEquals(2013, carResponse.year)
}
@Test
fun `when create method is called with duplicated data, throws BadRequestException`() {
val carRequest = CarRequest(make = "Mercedes-Benz", model = "500 SL", year = 1992)
assertThrowsExactly(BadRequestException::class.java) {
carService.create(carRequest)
}
}
@Test
fun `when update method is called with valid data a carResponse is returned`() {
val carRequest = CarRequest(make = "BMW", model = "1 Series", year = 2013)
val carResponse = carService.update(1, carRequest)
assertEquals(1, carResponse.id)
assertEquals("BMW", carResponse.make)
assertEquals("1 Series", carResponse.model)
assertEquals(2013, carResponse.year)
}
@Test
fun `when find method is called with valid id a carResponse is returned`() {
val carResponse = carService.find(1)
assertEquals(1, carResponse.id)
assertEquals("Mercedes-Benz", carResponse.make)
assertEquals("500 SL", carResponse.model)
assertEquals(1992, carResponse.year)
}
@Test
fun `when find method is called with invalid id, throws NotFoundException`() {
assertThrowsExactly(NotFoundException::class.java) {
carService.find(500)
}
}
@Test
fun `when delete method is called with valid id a car is deleted`() {
carService.delete(1)
assertNull(carRepository.findById(1))
}
@Test
fun `when delete method is called with invalid id, throws NotFoundException`() {
assertThrowsExactly(NotFoundException::class.java) {
carService.delete(500)
}
}
}@QuarkusTest
@QuarkusTestResource(PostgresTestLifecycleManager.class)
public class CarServiceIT {
@Inject
Flyway flyway;
@Inject
CarService carService;
@Inject
CarRepository carRepository;
@BeforeEach
void setUp() {
flyway.clean();
flyway.migrate();
}
@Test
void whenListMethodIsCalledAllCarsAreReturned() {
var list = carService.list();
assertEquals(1, list.size());
assertEquals(1, list.get(0).getId());
assertEquals("Mercedes-Benz", list.get(0).getMake());
assertEquals("500 SL", list.get(0).getModel());
assertEquals(1992, list.get(0).getYear());
}
@Test
void whenCreateMethodIsCalledWithValidDataCarResponseIsReturned() {
var carRequest = new CarRequest("BMW", "1 Series", 2013);
var carResponse = carService.create(carRequest);
assertEquals(2, carResponse.getId());
assertEquals("BMW", carResponse.getMake());
assertEquals("1 Series", carResponse.getModel());
assertEquals(2013, carResponse.getYear());
}
@Test
void whenCreateMethodIsCalledWithDuplicatedDataThrowsBadRequestException() {
var carRequest = new CarRequest("Mercedes-Benz", "500 SL", 1992);
assertThrowsExactly(BadRequestException.class, () -> carService.create(carRequest));
}
@Test
void whenUpdateMethodIsCalledWithValidDataCarResponseIsReturned() {
var carRequest = new CarRequest( "BMW", "1 Series", 2013);
var carResponse = carService.update(1L, carRequest);
assertEquals(1, carResponse.getId());
assertEquals("BMW", carResponse.getMake());
assertEquals("1 Series", carResponse.getModel());
assertEquals(2013, carResponse.getYear());
}
@Test
void whenFindMethodIsCalledWithValidIdCarResponseIsReturned() {
var carResponse = carService.find(1L);
assertEquals(1, carResponse.getId());
assertEquals("Mercedes-Benz", carResponse.getMake());
assertEquals("500 SL", carResponse.getModel());
assertEquals(1992, carResponse.getYear());
}
@Test
void whenFindMethodIsCalledWithInvalidIdThrowsNotFoundException() {
assertThrowsExactly(NotFoundException.class, () -> carService.find(500L));
}
@Test
void whenDeleteMethodIsCalledWithValidIdTheCarIsDeleted() {
carService.delete(1L);
assertNull(carRepository.findById(1L));
}
@Test
void whenDeleteMethodIsCalledWithInvalidIdThrowsNotFoundException() {
assertThrowsExactly(NotFoundException.class, () -> carService.delete(500L));
}
}@QuarkusTest
@QuarkusTestResource(PostgresTestLifecycleManager::class)
class CarServiceIT {
@Inject
lateinit var flyway: Flyway
@Inject
lateinit var carService: CarService
@Inject
lateinit var carRepository: CarRepository
@BeforeEach
fun setUp() {
flyway.clean()
flyway.migrate()
}
@Test
fun `when list method is called all cars is returned`() {
val list = carService.list()
assertEquals(1, list.size)
assertEquals(1, list[0].id)
assertEquals("Mercedes-Benz", list[0].make)
assertEquals("500 SL", list[0].model)
assertEquals(1992, list[0].year)
}
@Test
fun `when create method is called with valid data a carResponse is returned`() {
val carRequest = CarRequest(make = "BMW", model = "1 Series", year = 2013)
val carResponse = carService.create(carRequest)
assertEquals(2, carResponse.id)
assertEquals("BMW", carResponse.make)
assertEquals("1 Series", carResponse.model)
assertEquals(2013, carResponse.year)
}
@Test
fun `when create method is called with duplicated data, throws BadRequestException`() {
val carRequest = CarRequest(make = "Mercedes-Benz", model = "500 SL", year = 1992)
assertThrowsExactly(BadRequestException::class.java) {
carService.create(carRequest)
}
}
@Test
fun `when update method is called with valid data a carResponse is returned`() {
val carRequest = CarRequest(make = "BMW", model = "1 Series", year = 2013)
val carResponse = carService.update(1, carRequest)
assertEquals(1, carResponse.id)
assertEquals("BMW", carResponse.make)
assertEquals("1 Series", carResponse.model)
assertEquals(2013, carResponse.year)
}
@Test
fun `when find method is called with valid id a carResponse is returned`() {
val carResponse = carService.find(1)
assertEquals(1, carResponse.id)
assertEquals("Mercedes-Benz", carResponse.make)
assertEquals("500 SL", carResponse.model)
assertEquals(1992, carResponse.year)
}
@Test
fun `when find method is called with invalid id, throws NotFoundException`() {
assertThrowsExactly(NotFoundException::class.java) {
carService.find(500)
}
}
@Test
fun `when delete method is called with valid id a car is deleted`() {
carService.delete(1)
assertNull(carRepository.findById(1))
}
@Test
fun `when delete method is called with invalid id, throws NotFoundException`() {
assertThrowsExactly(NotFoundException::class.java) {
carService.delete(500)
}
}
}Para garantir que os dados cadastrados em um teste não interfira no outro teste, utilizei o flyway clean / migrate, porém não recomendo essa abordagem em bases de dados muito maiores que esse exemplo.
REST assured + Testcontainers
Também é possível utilizar testcontainers em conjunto com rest-assured para testar endpoints REST, para isso precisamos primeiro adicionar a lib do rest-assured:
- pom.xml
- build.gradle
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>testImplementation("io.rest-assured:rest-assured")<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>testImplementation("io.rest-assured:rest-assured")E utilizar o PostgresTestLifecycleManager no teste do rest-assured:
- Java
- Kotlin
@QuarkusTest
@QuarkusTestResource(PostgresTestLifecycleManager.class)
@TestHTTPEndpoint(CarResources.class)
public class CarResourcesIT {
@Inject
Flyway flyway;
@BeforeEach
void setUp() {
flyway.clean();
flyway.migrate();
}
@Test
void whenListResourceIsCalledAllCarsIsReturned() {
RestAssured.given()
.when().get()
.then()
.statusCode(Status.OK.getStatusCode())
.body(is("[{\"id\":1,\"make\":\"Mercedes-Benz\",\"model\":\"500 SL\",\"year\":1992}]"));
}
@Test
void whenCreateResourceIsCalledWithValidDataCreatedIsReturned() {
RestAssured.given()
.when()
.contentType(ContentType.JSON)
.body("{\"make\": \"BMW\", \"model\": \"1 Series\", \"year\": 2013}")
.post()
.then()
.statusCode(Status.CREATED.getStatusCode())
.body("id", is(2))
.body("make", is("BMW"))
.body("model", is("1 Series"))
.body("year", is(2013));
}
@Test
void whenCreateResourceIsCalledWithDuplicatedDataBadRequestIsReturned() {
RestAssured.given()
.when()
.contentType(ContentType.JSON)
.body("{\"make\": \"Mercedes-Benz\", \"model\": \"500 SL\", \"year\": 1992}")
.post()
.then()
.statusCode(Status.BAD_REQUEST.getStatusCode());
}
@Test
void whenUpdateResourceIsCalledWithValidDataOkIsReturned() {
RestAssured.given()
.when()
.contentType(ContentType.JSON)
.body("{\"make\": \"BMW\", \"model\": \"1 Series\", \"year\": 2013}")
.put("/1")
.then()
.statusCode(Status.OK.getStatusCode())
.body("id", is(1))
.body("make", is("BMW"))
.body("model", is("1 Series"))
.body("year", is(2013));
}
@Test
void whenFindResourceIsCalledWithValidIdTheCarIsReturned() {
RestAssured.given()
.when()
.get("/1")
.then()
.statusCode(Status.OK.getStatusCode())
.body("id", is(1))
.body("make", is("Mercedes-Benz"))
.body("model", is("500 SL"))
.body("year", is(1992));
}
@Test
void whenFindResourceIsCalledWithInvalidIdNotFoundIsReturned() {
RestAssured.given()
.when()
.get("/500")
.then()
.statusCode(Status.NOT_FOUND.getStatusCode());
}
@Test
void whenDeleteResourceIsCalledWithValidIdNoContentIsReturned() {
RestAssured.given()
.when()
.delete("/1")
.then()
.statusCode(Status.NO_CONTENT.getStatusCode());
}
@Test
void whenDeleteResourceIsCalledWithInvalidIdNotFoundIsReturned() {
RestAssured.given()
.when()
.delete("/500")
.then()
.statusCode(Status.NOT_FOUND.getStatusCode());
}
}@QuarkusTest
@QuarkusTestResource(PostgresTestLifecycleManager::class)
@TestHTTPEndpoint(CarResources::class)
class CarResourcesIT {
@Inject
lateinit var flyway: Flyway
@BeforeEach
fun setUp() {
flyway.clean()
flyway.migrate()
}
@Test
fun `when list resource is called all cars is returned`() {
RestAssured.given()
.`when`().get()
.then()
.statusCode(Status.OK.statusCode)
.body(`is`("""[{"id":1,"make":"Mercedes-Benz","model":"500 SL","year":1992}]"""))
}
@Test
fun `when create resource is called with valid data a created is returned`() {
RestAssured.given()
.`when`()
.contentType(ContentType.JSON)
.body("""{"make": "BMW", "model": "1 Series", "year": 2013}""")
.post()
.then()
.statusCode(Status.CREATED.statusCode)
.body("id", `is`(2))
.body("make", `is`("BMW"))
.body("model", `is`("1 Series"))
.body("year", `is`(2013))
}
@Test
fun `when create resource is called with duplicated data a bad request is returned`() {
RestAssured.given()
.`when`()
.contentType(ContentType.JSON)
.body("""{"make": "Mercedes-Benz", "model": "500 SL", "year": 1992}""")
.post()
.then()
.statusCode(Status.BAD_REQUEST.statusCode)
}
@Test
fun `when update resource is called with valid data a ok is returned`() {
RestAssured.given()
.`when`()
.contentType(ContentType.JSON)
.body("""{"make": "BMW", "model": "1 Series", "year": 2013}""")
.put("/1")
.then()
.statusCode(Status.OK.statusCode)
.body("id", `is`(1))
.body("make", `is`("BMW"))
.body("model", `is`("1 Series"))
.body("year", `is`(2013))
}
@Test
fun `when find resource is called with valid id a car is returned`() {
RestAssured.given()
.`when`()
.get("/1")
.then()
.statusCode(Status.OK.statusCode)
.body("id", `is`(1))
.body("make", `is`("Mercedes-Benz"))
.body("model", `is`("500 SL"))
.body("year", `is`(1992))
}
@Test
fun `when find resource is called with invalid id a not found is returned`() {
RestAssured.given()
.`when`()
.get("/500")
.then()
.statusCode(Status.NOT_FOUND.statusCode)
}
@Test
fun `when delete resource is called with valid id a no content is returned`() {
RestAssured.given()
.`when`()
.delete("/1")
.then()
.statusCode(Status.NO_CONTENT.statusCode)
}
@Test
fun `when delete resource is called with invalid id a not found is returned`() {
RestAssured.given()
.`when`()
.delete("/500")
.then()
.statusCode(Status.NOT_FOUND.statusCode)
}
}@QuarkusTest
@QuarkusTestResource(PostgresTestLifecycleManager.class)
@TestHTTPEndpoint(CarResources.class)
public class CarResourcesIT {
@Inject
Flyway flyway;
@BeforeEach
void setUp() {
flyway.clean();
flyway.migrate();
}
@Test
void whenListResourceIsCalledAllCarsIsReturned() {
RestAssured.given()
.when().get()
.then()
.statusCode(Status.OK.getStatusCode())
.body(is("[{\"id\":1,\"make\":\"Mercedes-Benz\",\"model\":\"500 SL\",\"year\":1992}]"));
}
@Test
void whenCreateResourceIsCalledWithValidDataCreatedIsReturned() {
RestAssured.given()
.when()
.contentType(ContentType.JSON)
.body("{\"make\": \"BMW\", \"model\": \"1 Series\", \"year\": 2013}")
.post()
.then()
.statusCode(Status.CREATED.getStatusCode())
.body("id", is(2))
.body("make", is("BMW"))
.body("model", is("1 Series"))
.body("year", is(2013));
}
@Test
void whenCreateResourceIsCalledWithDuplicatedDataBadRequestIsReturned() {
RestAssured.given()
.when()
.contentType(ContentType.JSON)
.body("{\"make\": \"Mercedes-Benz\", \"model\": \"500 SL\", \"year\": 1992}")
.post()
.then()
.statusCode(Status.BAD_REQUEST.getStatusCode());
}
@Test
void whenUpdateResourceIsCalledWithValidDataOkIsReturned() {
RestAssured.given()
.when()
.contentType(ContentType.JSON)
.body("{\"make\": \"BMW\", \"model\": \"1 Series\", \"year\": 2013}")
.put("/1")
.then()
.statusCode(Status.OK.getStatusCode())
.body("id", is(1))
.body("make", is("BMW"))
.body("model", is("1 Series"))
.body("year", is(2013));
}
@Test
void whenFindResourceIsCalledWithValidIdTheCarIsReturned() {
RestAssured.given()
.when()
.get("/1")
.then()
.statusCode(Status.OK.getStatusCode())
.body("id", is(1))
.body("make", is("Mercedes-Benz"))
.body("model", is("500 SL"))
.body("year", is(1992));
}
@Test
void whenFindResourceIsCalledWithInvalidIdNotFoundIsReturned() {
RestAssured.given()
.when()
.get("/500")
.then()
.statusCode(Status.NOT_FOUND.getStatusCode());
}
@Test
void whenDeleteResourceIsCalledWithValidIdNoContentIsReturned() {
RestAssured.given()
.when()
.delete("/1")
.then()
.statusCode(Status.NO_CONTENT.getStatusCode());
}
@Test
void whenDeleteResourceIsCalledWithInvalidIdNotFoundIsReturned() {
RestAssured.given()
.when()
.delete("/500")
.then()
.statusCode(Status.NOT_FOUND.getStatusCode());
}
}@QuarkusTest
@QuarkusTestResource(PostgresTestLifecycleManager::class)
@TestHTTPEndpoint(CarResources::class)
class CarResourcesIT {
@Inject
lateinit var flyway: Flyway
@BeforeEach
fun setUp() {
flyway.clean()
flyway.migrate()
}
@Test
fun `when list resource is called all cars is returned`() {
RestAssured.given()
.`when`().get()
.then()
.statusCode(Status.OK.statusCode)
.body(`is`("""[{"id":1,"make":"Mercedes-Benz","model":"500 SL","year":1992}]"""))
}
@Test
fun `when create resource is called with valid data a created is returned`() {
RestAssured.given()
.`when`()
.contentType(ContentType.JSON)
.body("""{"make": "BMW", "model": "1 Series", "year": 2013}""")
.post()
.then()
.statusCode(Status.CREATED.statusCode)
.body("id", `is`(2))
.body("make", `is`("BMW"))
.body("model", `is`("1 Series"))
.body("year", `is`(2013))
}
@Test
fun `when create resource is called with duplicated data a bad request is returned`() {
RestAssured.given()
.`when`()
.contentType(ContentType.JSON)
.body("""{"make": "Mercedes-Benz", "model": "500 SL", "year": 1992}""")
.post()
.then()
.statusCode(Status.BAD_REQUEST.statusCode)
}
@Test
fun `when update resource is called with valid data a ok is returned`() {
RestAssured.given()
.`when`()
.contentType(ContentType.JSON)
.body("""{"make": "BMW", "model": "1 Series", "year": 2013}""")
.put("/1")
.then()
.statusCode(Status.OK.statusCode)
.body("id", `is`(1))
.body("make", `is`("BMW"))
.body("model", `is`("1 Series"))
.body("year", `is`(2013))
}
@Test
fun `when find resource is called with valid id a car is returned`() {
RestAssured.given()
.`when`()
.get("/1")
.then()
.statusCode(Status.OK.statusCode)
.body("id", `is`(1))
.body("make", `is`("Mercedes-Benz"))
.body("model", `is`("500 SL"))
.body("year", `is`(1992))
}
@Test
fun `when find resource is called with invalid id a not found is returned`() {
RestAssured.given()
.`when`()
.get("/500")
.then()
.statusCode(Status.NOT_FOUND.statusCode)
}
@Test
fun `when delete resource is called with valid id a no content is returned`() {
RestAssured.given()
.`when`()
.delete("/1")
.then()
.statusCode(Status.NO_CONTENT.statusCode)
}
@Test
fun `when delete resource is called with invalid id a not found is returned`() {
RestAssured.given()
.`when`()
.delete("/500")
.then()
.statusCode(Status.NOT_FOUND.statusCode)
}
}Você pode ler mais sobre rest-assured aqui.
Github do Projeto: