Construindo uma API RESTful com Java e Spring Framework
Hoje em dia, está cada vez mais comum termos aplicações que funcionam online, em navegadores ou dispositivos móveis. Essas aplicações tem por objetivo consumir informação por meio de interfaces que implementam uma série rotinas e padrões que chamamos de API.
O acrônimo API vem da expressão em inglês Application Programming Interface (em português, Interface de Programação de Aplicações). Uma API é um conjunto de padrões e regras documentadas para que uma aplicação X possa utilizar funcionalidades de uma aplicação Y sem precisar conhecer os detalhes da implementação dessa aplicação X.
Ainda está obscuro? Para facilitar o entendimento, vamos imaginar o dia-a-dia de uma empresa que possui um e-commerce. Os desenvolvedores que trabalham na solução tem como objetivo criar a dinâmica da loja, como criar, atualizar, deletar os produtos internamente e mostrar os produtos para os clientes. Essas funcionalidades podem ser criadas em uma aplicação do lado do servidor como se fosse uma API, de forma que o site do e-commerce possa usar essas informações.
Agora que já sabemos o que é uma API e para que utilizamos uma, precisamos entender os protocolos que são utilizadas para a comunicação entre as aplicações e como os dados trafegados podem ser representados.
HTTP, REST e representações de dados em APIs
HTTP e REST são a mesma coisa?
O principal protocolo de comunicação na Web é o HTTP. Ele funciona como um protocolo de requisição-resposta em um modelo que chamamos de cliente-servidor. No exemplo acima, do e-commerce, o navegador que é usado para acessar o site seria o cliente e o computador ou máquina virtual em algum serviço de cloud em que a API está hospedada é o servidor. Assim, o cliente manda uma requisição HTTP para o servidor e o servidor, com recursos e conteúdos próprios, retorna uma mensagem de resposta para o cliente.
O protocolo HTTP tem sido usado desde 1990 e a versão atual do protocolo é o HTTP/3. O protocolo define oito métodos que determinam ações a serem efetuadas no momento da requisição de algum recurso ao servidor. Desses oito, os 4 mais utilizados são:
- GET: método utilizado para ler e recuperar dados. Requisita uma representação do recurso especificado e retorna essa representação.
- POST: método utilizado para criar um novo recurso. Envia dados ao servidor. O tipo do corpo da solicitação é indicado pelo cabeçalho
Content-Type
. - PUT: cria um novo recurso ou substitui uma representação do recurso de destino com os novos dados. A diferença entre
PUT
ePOST
é quePUT
é idempotente: ao chamá-lo uma ou várias vezes sucessivamente o efeito é o mesmo, enquanto se chamar oPOST
repetidamente pode ter efeitos adicionais. Por exemplo, se criarmos um produto comPOST
, se a URL definida na API for chamada 20 vezes, 20 produtos serão criados e cada um deles terá um ID diferente. Já o com oPUT
se você executar a URL definida na API 20 vezes, o resultado tem que ser o mesmo: o mesmo produto atualizado 20 vezes. - DELETE: exclui o recurso.
Baseado nesses métodos , o servidor deve processar cada uma das requisições e retornar uma resposta adequada. O conteúdo da resposta pode estar no formato XML, JSON, YAML, texto, dentre outros. E essas as respostas são separadas em cinco grupos:
- 1XX — Informações Gerais
- 2XX — Sucesso
- 3XX — Redirecionamento
- 4XX — Erro no cliente
- 5XX — Erro no servidor
Mas isso não é REST? Não, não é. REST, acrônimo de Representational State Transfer, é uma abstração dessa arquitetura que detalhamos acima. É um estilo de arquitetura de software que define uma série de restrições para a criação de web services (serviços Web), ou seja, restringe como seus componentes devem interagir entre si. Esse termo foi introduzido e definido por Roy Fielding em sua tese de doutorado no final dos anos 90 e início dos anos 2000.
Na tese, Fielding definiu os princípios REST que eram conhecidos como “modelo de objeto HTTP” e passaram a ser utilizados no projeto dos padrões HTTP 1.1 e URI (Uniform Resource Identifiers). Dessa forma, podemos dizer que em sua semântica, o REST utiliza-se métodos HTTP. Além desse conceito, vale lembrar também que um serviço REST deve ser Stateless: toda requisição deve ser autossuficiente, ou seja, cada requisição é um requisição diferente e independente. Não deve existir na requisição nenhuma forma de guardar o estado de uma informação.
E o que é ser RESTful? Dizemos que uma API é RESTful, se garantimos que implementação da API está de acordo com essa arquitetura REST explicada acima. Conceitualmente, nos serviços RESTful, tanto os dados quanto as funcionalidades são considerados recursos e ficam acessíveis aos clientes através da utilização de URIs. Essas URI’s normalmente são endereços na web que identificam tanto o servidor no qual a aplicação está hospedada quanto a própria aplicação e qual dos recursos oferecidos pela mesma está sendo solicitado.
Dessa forma, expor as funcionalidades da sua API (ou funcionalidades dos seus serviços, se você preferir) no modo RESTful, os princípios REST e suas restrições se aplicam a você também. Com todos os conceitos na mesa, vamos colocar a mão na massa e construir uma API? Iremos utilizar a linguagem Java (versão 11) com Spring Framework como base da API. Além disso, usaremos como ferramentas:
- Apache Maven (para gestão de dependências)
- Postman (para execução de testes e requisições em geral na API)
- JUnit5 (para testes unitários e de integração)
- Lombok (para reduzir código boilerplate)
- Log4j (para adicionar logs na aplicação)
- TravisCI (para integração contínua)
Construindo nossa API
Primeiros passos com a API RESTful
A API deve criar, atualizar, deletar e listar viagens. Além disso, deve calcular estatísticas sobre as os tickets de viagem criados. Nesse exemplo, não vamos tratar camada de persistência, e, por isso, de forma ilustrativa, a entidade Travel conterá um campo chamado id. A API terá os seguintes endpoints:
POST/api-travels/travels: cria uma viagem.
Body da requisição:
{
"id": 1,
"orderNumber": "220788",
"amount": "22.88",
"startDate": "2019–09–11T09:59:51.312Z",
"type": "ONE-WAY"
}
Em que:
- id: número único da transação;
- orderNumber: número de identificação de uma viagem no sistema interno.
- amount: valor da viagem; deve ser uma String de tamanho arbitrário que pode ser parseada como um BigDecimal;
- startDate: data de início da viagem no formato ISO 8601 YYYY-MM-DDThh:mm:ss.sssZ no timezone local.
- endDate: data de fim da viagem no formato ISO 8601 YYYY-MM-DDThh:mm:ss.sssZ no timezone local. Pode ser nulo se a viagem é só de ida.
- type: se a viagem é apenas de ida (ONE-WAY), ida e volta (RETURN) ou é composta de múltiplos destinos (MULTI-CITY).
Deve retornar com body vazio com um dos códigos a seguir:
- 201: em caso de sucesso.
- 400: caso o JSON seja inválido.
- 422: se qualquer um dos campos não for parseável ou se a data de início da viagem é mais recente que a data de fim (para todos os casos, exceto viagens apenas de ida).
PUT/api-travels/travels/{id}: atualiza uma viagem.
Body da requisição:
{
"id": 1,
"orderNumber": "220788",
"amount": "30.06",
"startDate": "2019–09–11T09:59:51.312Z",
"type": "ONE-WAY"
}
Deve ser enviado o objeto que será modificado. O retorno deve ser o próprio objeto modificado.
{
"id": 1,
"orderNumber": "220788",
"amount": "30.06",
"startDate": "2019–09–11T09:59:51.312Z",
"type": "ONE-WAY"
}
A resposta deve conter os códigos a seguir:
- 200: em caso de sucesso.
- 400: caso o JSON seja inválido.
- 404: caso tentem atualizar um registro que não existe.
- 422: se qualquer um dos campos não for parseável (JSON mal formatado).
GET/api-travels/travels: retorna todas as viagens criadas.
Deve retornar uma lista de viagens.
{
"id": 1,
"orderNumber": "220788",
"amount": "30.06",
"startDate": "2019–09–11T09:59:51.312Z",
"type": "ONE-WAY"
},
{
"id": 2,
"nsu": "300691",
"amount": "120.0",
"startDate": "2019–09–11T10:22:30.312Z",
"type": "ONE-WAY"
}
A resposta deve conter os códigos a seguir:
- 200: caso exista viagens criadas.
- 404: caso não exista viagens criadas.
DELETE/api-travels/travels: remove todas as viagens.
Deve aceitar uma requisição com body vazio e retornar 204.
GET/api-travels/statistics: retorna estatísticas básicas sobre as viagens criadas.
{
"sum": "150.06",
"avg": "75.3",
"max": "120.0",
"min": "30.06",
"count": "2"
}
Em que:
- sum: um BigDecimal especificando a soma total das viagens criadas.
- avg: um BigDecimal especificando a média dos valores das viagens criadas.
- max: um BigDecimal especificando o maior valor dentre as viagens criadas.
- min: um BigDecimal especificando o menor valor dentre as viagens criadas.
- count: um long especificando o número total de viagens.
Todos os campos que são BigDecimal devem ter apenas duas casas decimais, por exemplo: 15.385 deve ser retornado como 15.39.
Detalhadas as funcionalidades que precisamos implementar, mãos à obra!
1) Primeiro passo é criar um projeto Spring Boot no Spring Initializr.
Como dependências, vamos selecionar o Spring Web (Spring MVC) e Lombok. O Spring MVC é um framework que ajuda no desenvolvimento de aplicações web no padrão MVC (model-view-controller). O Lombok é uma biblioteca Java focada em produtividade e redução de código por meio de anotações que ensinam o compilador a criar e manipular código Java. Ou seja, você não vai mais precisar escrever métodos getter, setter, equals, hashCode, construtores de classe, etc.
2) Com o projeto criado, vamos criar os pacotes model, controller e service.
Em seguida, criaremos as models: Travel
e Statistic
.
Exemplo da classe Travel do pacote model:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Travel {
private Long id;
private String orderNumber;
private LocalDateTime startDate;
private LocalDateTime endDate;
private BigDecimal amount;
private TravelTypeEnum type;
private List<Link> links;
public Travel(TravelTypeEnum type){
this.type = type;
}
public void addLink(Link link) {
if(links == null) links = new ArrayList<>();
links.add(link);
}
}
Exemplo da classe Statistic do pacote model:
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Statistic {
private BigDecimal sum;
private BigDecimal avg;
private BigDecimal max;
private BigDecimal min;
private long count;
}
3) Com as models criadas, criaremos as services com as funcionalidades da API: TravelService
e StatisticService
.
A classe TravelService
deve conter sete métodos para atender os requisitos do ciclo de vida desse serviço na API: isJSONValid
(para verificar se o JSON é válido), parseId
(para parsear o campo id do JSON), parseAmount
(para parsear o campo amount do JSON), parseStartDate
(para parsear o campo startDate do JSON), parseEndDate
(para parsear o campo endDate do JSON), isStartDateGreaterThanEndDate
(para verificar se a data de início da viagem é anterior a data de fim da viagem), create
(para criar uma viagem), update
(atualizar uma viagem), add
(para adicionar uma viagem na lista), findById
(para recuperar uma viagem por id), find
(para recuperar todas as viagens criadas) e delete
(remover viagens).
Exemplo da classe do pacote service TravelService:
@Service
public class TravelService {
private TravelFactory factory;
private List<Travel> travels;
public void createTravelFactory() {
if(factory == null) {
factory = new TravelFactoryImpl();
}
}
public void createTravelList() {
if(travels == null) {
travels = new ArrayList<>();
}
}
public boolean isJSONValid(String jsonInString) {
try {
return new ObjectMapper().readTree(jsonInString) != null;
} catch (IOException e) {
return false;
}
}
private long parseId(JSONObject travel) {
return Long.valueOf((int) travel.get("id"));
}
private BigDecimal parseAmount(JSONObject travel) {
return new BigDecimal((String) travel.get("amount"));
}
private LocalDateTime parseStartDate(JSONObject travel) {
var startDate = (String) travel.get("startDate");
return ZonedDateTime.parse(startDate).toLocalDateTime();
}
private LocalDateTime parseEndDate(JSONObject travel) {
var endDate = (String) travel.get("endDate");
return ZonedDateTime.parse(endDate).toLocalDateTime();
}
public boolean isStartDateGreaterThanEndDate(Travel travel) {
if (travel.getEndDate() == null) return false;
return travel.getStartDate().isAfter(travel.getEndDate());
}
private void setTravelValues(JSONObject jsonTravel, Travel travel) {
String orderNumber = (String) jsonTravel.get("orderNumber");
String type = (String) jsonTravel.get("type");
travel.setOrderNumber(orderNumber != null ? orderNumber : travel.getOrderNumber());
travel.setAmount(jsonTravel.get("amount") != null ? parseAmount(jsonTravel) : travel.getAmount());
travel.setStartDate(jsonTravel.get("initialDate") != null ? parseStartDate(jsonTravel) : travel.getStartDate());
travel.setEndDate(jsonTravel.get("finalDate") != null ? parseEndDate(jsonTravel) : travel.getEndDate());
travel.setType(type != null ? TravelTypeEnum.getEnum(type) : travel.getType());
}
public Travel create(JSONObject jsonTravel) {
createFactory();
Travel travel = factory.createTravel((String) jsonTravel.get("type"));
travel.setId(parseId(jsonTravel));
setTravelValues(jsonTravel, travel);
return travel;
}
public Travel update(Travel travel, JSONObject jsonTravel) {
setTravelValues(jsonTravel, travel);
return travel;
}
public void add(Travel travel) {
createTravelList();
travels.add(travel);
}
public List<Travel> find() {
createTravelList();
return travels;
}
public Travel findById(long id) {
return travels.stream().filter(t -> id == t.getId()).collect(Collectors.toList()).get(0);
}
public void delete() {
travels.clear();
}
public void clearObjects() {
travels = null;
factory = null;
}
}
Já a classe StatisticService
deve conter apenas o método para criar estatísticas da API: createStatistics
.
Exemplo da classe do pacote service StatisticsService:
@Service
public class StatisticService {
public Statistic create(List<Travel> travels) {
var statistics = new Statistic();
statistics.setCount(travels.stream().count());
statistics.setAvg(BigDecimal.valueOf(travels.stream().mapToDouble(t -> t.getAmount().doubleValue()).average().orElse(0.0))
.setScale(2, RoundingMode.HALF_UP));
statistics.setMin(BigDecimal.valueOf(travels.stream().mapToDouble(t -> t.getAmount().doubleValue()).min().orElse(0.0))
.setScale(2, RoundingMode.HALF_UP));
statistics.setMax(BigDecimal.valueOf(travels.stream().mapToDouble(t -> t.getAmount().doubleValue()).max().orElse(0.0))
.setScale(2, RoundingMode.HALF_UP));
statistics.setSum(BigDecimal.valueOf(travels.stream().mapToDouble(t -> t.getAmount().doubleValue()).sum())
.setScale(2, RoundingMode.HALF_UP));
return statistics;
}
}
4) Com as funcionalidades criadas, faremos agora as rotas nas controllers: TravelController
(para as rotas relacionadas às viagens) e StatisticController
(para as rotas relacionadas às estatísticas).
Na classe TravelController
, implementaremos as operações detalhadas no início da seção: criar uma viagem (POST), atualizar uma viagem (PUT), listar todas as viagens (GET) e remover todas as viagens (DELETE).
Exemplo da controller das rotas do CRUD de viagens:
@RestController
@RequestMapping("/api-travels/travels")
public class TravelController {
private static final Logger logger = Logger.getLogger(TravelController.class);
@Autowired
private TravelService travelService;
@GetMapping
public ResponseEntity<List<Travel>> find() {
if(travelService.find().isEmpty()) {
return ResponseEntity.notFound().build();
}
logger.info(travelService.find());
return ResponseEntity.ok(travelService.find());
}
@DeleteMapping
public ResponseEntity<Boolean> delete() {
try {
travelService.delete();
return ResponseEntity.noContent().build();
}catch(Exception e) {
logger.error(e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
}
@PostMapping
@ResponseBody
public ResponseEntity<Travel> create(@RequestBody JSONObject travel) {
try {
if(travelService.isJSONValid(travel.toString())) {
Travel travelCreated = travelService.create(travel);
var uri = ServletUriComponentsBuilder.fromCurrentRequest().path(travelCreated.getOrderNumber()).build().toUri();
if(travelService.isStartDateGreaterThanEndDate(travelCreated)){
logger.error("The start date is greater than end date.");
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(null);
}else {
travelService.add(travelCreated);
return ResponseEntity.created(uri).body(null);
}
}else {
return ResponseEntity.badRequest().body(null);
}
}catch(Exception e) {
logger.error("JSON fields are not parsable. " + e);
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(null);
}
}
@PutMapping(path = "/{id}", produces = { "application/json" })
public ResponseEntity<Travel> update(@PathVariable("id") long id, @RequestBody JSONObject travel) {
try {
if(travelService.isJSONValid(travel.toString())) {
Travel travelToUpdate = travelService.findById(id);
if(travelToUpdate == null){
logger.error("Travel not found.");
return ResponseEntity.notFound().build();
}else {
Travel travelToUpdate = travelService.update(travelToUpdate, travel);
return ResponseEntity.ok(travelToUpdate);
}
}else {
return ResponseEntity.badRequest().body(null);
}
}catch(Exception e) {
logger.error("JSON fields are not parsable." + e);
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(null);
}
}
}
Criadas as rotas, vamos subir a API com o comando:
mvn spring-boot:run
Também é possível executar a API pela sua IDE como Java Application. O resultado de ambos comandos deve ser semelhante à figura abaixo.
Por default, a API estará disponível no endereço: http://localhost:8080/. Com a API funcionando já podemos testar o funcionamento das rotas. Para os testes iniciais, utilizei o Postman e como referência os exemplos do enunciado da seção Construindo nossa API.
POST/api-travels/travels via Postman
PUT/api-travels/travels/1 via Postman
GET/api-travels/travels via Postman
DELETE/api-travels/travels via Postman
Na classe StatisticController
, implementaremos a operação e cálculo das estatísticas detalhada no início da seção.
Exemplo da controller da rota de estatística:
@RestController
@RequestMapping("/api-travels/statistics")
public class StatisticController {
private static final Logger logger = Logger.getLogger(StatisticController.class);
@Autowired
private TravelService travelService;
@Autowired
private StatisticService statisticsService;
@GetMapping(produces = { "application/json" })
public ResponseEntity<Statistic> getStatistics() {
List<Travel> travels = travelService.find();
Statistic statistics = statisticsService.create(travels);
logger.info(statistics);
return ResponseEntity.ok(statistics);
}
}
GET/api-travels/statistics via Postman
Executando testes unitários e de integração
Como testar sua API de forma simples e rápida
Com as funcionalidades da API implementadas, precisamos testá-las. Para automatizar os testes da aplicação utilizei o JUnit5. Para que ambos os testes (unitário e integração) sejam executados corretamente (nos goals corretos do Maven), precisamos adicionar alguns plugins no nosso pom.xml
: maven-failsafe-plugin e build-helper-maven-plugin. A configuração deve ser a mesma abaixo:
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<trimStackTrace>false</trimStackTrace>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId
<executions>
<execution>
<id>add-integration-test-sources</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>src/it/java</source>
</sources>
</configuration>
</execution>
<execution>
<id>add-integration-test-resources</id>
<phase>generate-test-resources</phase>
<goals>
<goal>add-test-resource</goal>
</goals>
<configuration>
<resources>
<resource>
<directory>src/it/resources</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
Conforme já estruturado na configuração, criaremos dois pacotes: src/test/java
, de teste unitário, que irá conter métodos para validar a lógica implementada nas services e controllers; e src/it/java
para classe TravelsJavaApiIntegrationTest
, de teste de integração para validar integração e fluxo das rotas na API. Para os testes de integração, utilizaremos o TestRestTemplate do próprio Spring Boot.
Exemplo da classe de teste unitário TravelsJavaApiUnitTests:
@SpringBootTest(classes = { TravelService.class, StatisticService.class })
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class })
@ActiveProfiles("test")
public class TravelsJavaApiUnitTests {
@Autowired
private TravelService travelService;
@Autowired
private StatisticService statisticService;
@Before
public void setUp() {
travelService.createFactory();
travelService.createTravelList();
}
@Test
public void shouldReturnNotNullTravelService() {
assertNotNull(travelService);
}
@Test
public void shouldReturnNotNullStatisticService() throws Exception {
assertNotNull(statisticService);
}
@Test
@SuppressWarnings("unchecked")
public void shouldReturnTravelCreatedWithSuccess() throws Exception {
String startDate = "2019-11-21T09:59:51.312Z";
String endDate = "2019-12-01T21:08:45.202Z";
JSONObject jsonTravel = new JSONObject();
jsonTravel.put("id", 1);
jsonTravel.put("orderNumber", "220788");
jsonTravel.put("amount", "22.88");
jsonTravel.put("type", TravelTypeEnum.RETURN.getValue());
jsonTravel.put("startDate", startDate);
jsonTravel.put("endDate", endDate);
Travel travel = travelsService.create(jsonTravel);
assertNotNull(travel);
assertEquals(travel.getId().intValue(), jsonTravel.get("id"));
assertEquals(travel.getOrderNumber(), jsonTravel.get("orderNumber"));
assertEquals(travel.getAmount().toString(), jsonTravel.get("amount"));
assertEquals(travel.getType().toString(), jsonTravel.get("type"));
assertEquals(travel.getStartDate(), ZonedDateTime.parse(startDate).toLocalDateTime());
assertEquals(travel.getEndDate(), ZonedDateTime.parse(endDate).toLocalDateTime());
}
@Test
@SuppressWarnings("unchecked")
public void shouldReturnTravelCreatedInStartDateIsGreaterThanEndDate() throws Exception {
JSONObject jsonTravel = new JSONObject();
jsonTravel.put("id", 2);
jsonTravel.put("orderNumber", "220788");
jsonTravel.put("amount", "22.88");
jsonTravel.put("type", TravelTypeEnum.RETURN.getValue());
jsonTravel.put("startDate", "2020-09-20T09:59:51.312Z");
jsonTravel.put("endDate", "2020-09-11T09:59:51.312Z");
Travel travel = travelsService.create(jsonTravel);
boolean travelInFuture = travelsService.isStartDateGreaterThanEndDate(travel);
assertTrue(travelInFuture);
}
@Test
@SuppressWarnings("unchecked")
public void shouldReturnTravelsStatisticsCalculated() throws Exception {
travelsService.delete();
String startDate = "2019-11-21T09:59:51.312Z";
String endDate = "2019-12-01T21:08:45.202Z";
JSONObject jsonTravel220788 = new JSONObject();
jsonTravel220788.put("id", 1);
jsonTravel220788.put("orderCode", "220788");
jsonTravel220788.put("amount", "22.88");
jsonTravel220788.put("type", TravelTypeEnum.RETURN.getValue());
jsonTravel220788.put("startDate", startDate);
jsonTravel220788.put("endDate", endDate);
Travel travel = travelsService.create(jsonTravel220788);
travelsService.add(travel);
JSONObject jsonTravel300691 = new JSONObject();
jsonTravel300691.put("id", 2);
jsonTravel300691.put("orderCode", "300691");
jsonTravel300691.put("amount", "120.0");
jsonTravel300691.put("type", TravelTypeEnum.ONE_WAY.getValue());
jsonTravel300691.put("startDate", startDate);
travel = travelsService.create(jsonTravel300691);
travelsService.add(travel);
Statistic statistic = statisticService.create(travelsService.find());
assertNotNull(statistic);
assertEquals("142.88", statistic.getSum().toString());
assertEquals("71.44", statistic.getAvg().toString());
assertEquals("22.88", statistic.getMin().toString());
assertEquals("120.00", statistic.getMax().toString());
assertEquals(2, statistic.getCount());
}
@After
public void tearDown() {
travelService.clearObjects();
}
Exemplo da classe de teste de integração FinancialJavaApiIntegrationTest:
@ActiveProfiles("test")
@TestMethodOrder(OrderAnnotation.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class TravelsJavaApiIntegrationTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
@Order(1)
public void contextLoad() {
assertNotNull(mockMvc);
}
@Test
@Order(2)
public void shouldReturnCreateTravel() throws Exception {
JSONObject mapToCreate = setObjectToCreate();
this.mockMvc.perform(post("/api-travels/travels").contentType(MediaType.APPLICATION_JSON_VALUE)
.content(new ObjectMapper().writeValueAsString(mapToCreate))).andExpect(status().isCreated());
}
@Test
@Order(3)
public void shouldReturnUpdateTravel() throws Exception {
JSONObject mapToUpdate = setObjectToUpdate();
this.mockMvc.perform(put("/api-travels/travels/1").contentType(MediaType.APPLICATION_JSON_VALUE)
.content(new ObjectMapper().writeValueAsString(mapToUpdate))).andExpect(status().isOk());
}
@Test
@Order(4)
public void shouldReturnGetAllTravels() throws Exception {
this.mockMvc.perform(get("/api-travels/travels")).andExpect(status().isOk());
}
@Test
@Order(5)
public void shouldReturnRemoveAllTravels() throws Exception {
this.mockMvc.perform(delete("/api-travels/travels")).andExpect(status().isNoContent());
}
@SuppressWarnings("unchecked")
private JSONObject setObjectToCreate() {
String startDate = "2019-11-21T09:59:51.312Z";
String endDate = "2019-12-01T21:08:45.202Z";
JSONObject map = new JSONObject();
map.put("id", 1);
map.put("orderNumber", "220788");
map.put("amount", "22.88");
map.put("type", TravelTypeEnum.RETURN.getValue());
map.put("startDate", startDate);
map.put("endDate", endDate);
return map;
}
@SuppressWarnings("unchecked")
private JSONObject setObjectToUpdate() {
String startDate = "2019-11-21T09:59:51.312Z";
JSONObject map = new JSONObject();
map.put("id", 1L);
map.put("orderNumber", "220788");
map.put("amount", "22.88");
map.put("type", TravelTypeEnum.ONE_WAY.getValue());
map.put("startDate", startDate);
return map;
}
}
Caso você não tenha familiarização com JUnit, leia esse artigo. Para executar somente os testes unitários use o comando:
mvn test
Para executar os testes unitários e de integração, basta usar o comando:
mvn integration-test
Com o fluxo de teste pronto, o último passo é criar o fluxo de integração contínua: fazer com que a evolução da API seja monitorada constantemente, mantendo a integração frequente do código que foi e será desenvolvido. Usaremos como ferramenta o TravisCI. Para mais detalhes da prática e do TravisCI, leia esse artigo. Na próxima seção, faremos a configuração final.
Configurando Integração Contínua com TravisCI
Construir sempre para conquistar sempre!
O primeiro passo para habilitar o fluxo da integração contínua é criar um arquivo .travis.yml, na raiz do projeto, com a seguinte configuração:https://mari-azevedo.medium.com/media/e53d2cf98179224a45cfcc402647bf2c
Nessa configuração, estabelecemos que o build será feito em uma distribuição Linux mais atual, com a linguagem Java, sem permissão de admin e com o JDK 11. Com a máquina virtual configurada, o TravisCI irá fazer o download do código em até 2 commits, executar a instalação do pacote e em seguida os testes.
Em seguida, é necessário habilitar o fluxo da API no dashboard do TravisCI:
Caso a configuração esteja correta, assim que um commit for realizado no projeto, um build da branch principal é disparado automaticamente.
O resultado esperado é que o build seja bem sucedido.
Ao analisar com detalhes o log do build, podemos ver que a branch foi construída e os testes executados com sucesso.
Pronto, nossa API está pronta e validada!