Desenvolvimento

Criando uma API Rest com Quarkus: Parte 3

Dando continuidade a série de artigos em que estamos criando uma api, em que usamos Java 21, Quarkus, Panache e RestEasy, na terceira e última parte vamos abordar a construção dos endpoints e criação dos testes de integração e de unidade. Caso você não tenha lido as primeiras partes, acesse aqui:

Criando os endpoints

Seguindo a lógica da nossa classe de serviços, para nossa controller também vamos precisar desenvolver o CRUD. Para o artigo não ficar muito grande vamos primeiro mostrar como seria a classe com o mínimo, localizada em quarkus-todo-crud-rest/src/main/java/br/com/codeinloop/controller. Vamos criar o TodoController.java com:

package br.com.codeinloop.controller;

import br.com.codeinloop.model.Todo;
import br.com.codeinloop.service.TodoService;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;

import java.net.MalformedURLException;
import java.net.URI;

@Path("/todo")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class TodoController {
    @Inject
    TodoService service;

}
Java

Na linha 15 @Path(“/todo”) estamos dizendo que todos os nossos métodos ali dentro vão responder no mesmo endpoint base, por exemplo localhost:8080/todo/criar, localhost:8080/todo/listar e assim em diante. Nas linhas seguintes definimos o que será produzido e consumido pela classe, nesse caso será JSON. Podemos definir método a método, porém como não vamos alterar o tipo de resposta, fazemos uma declaração geral pela classe. Uma outra possibilidade seria definir como fizemos antes e em apenas um método específico, por exemplo, upload de arquivo, mudar o tipo de consumo. Nas linhas 19 e 20 estamos apenas preparando nossa injeção da service, pois vamos usar no decorrer do nosso CRUD.

Criar

    @POST
    @Transactional
    public Response create(Todo todo) throws MalformedURLException {
        Todo newTodo = service.create(todo);
        URI location = UriBuilder.fromResource(TodoController.class)
                .path("{id}")
                .resolveTemplate("id", newTodo.getId())
                .build();
        return Response.created(location).entity(newTodo).build();
    }
Java

No método de criar temos alguns pontos interessantes. A anotação @POST define que a requisição será do tipo POST, e a @Transactional define que se trata de uma requisição que envolve transação no banco de dados. Em casos de erro teremos um rollback das informações inseridas, desta forma evitando que a informação fique salva de forma incompleta. Esse tipo de medida é muito útil para casos em que estamos trabalhando com mais de uma tabela.

Já no desenvolvimento do método apenas chamamos o método create da nossa service e em seguida devolvemos o status “created” contendo a URI do recurso criado e o body do mesmo. Perceba que estamos usando sempre o mesmo retorno response, convido a todos a dar uma olhada na documentação para descobrir outras formas possíveis de responder uma requisição.

Listar

Dando sequência no listar. Conforme especificado no site todobackend (mencionado no primeiro artigo), as regras de negócio que vamos seguir na construção do nosso backend especificam que, precisamos de dois endpoints para listagem, um que deve retornar tudo e outro que deve retornar um por ID. Para simplificar aqui vamos mostrar apenas o retorno com ID. No nosso repositório você poderá encontrar o código completo.

    @GET
    @Path("/{id}")
    public Response readOne(@PathParam("id") Long id){
        return Response.ok(service.readOne(id)).build();
    }
Java

Aqui temos um método bem similar ao anterior, com a diferença que aqui precisamos receber um parâmetro via URL. Para fazer isso vamos modificar o nosso path original, ou seja, incrementar ele com o @Path(“/{id}”) com isso fazendo uma composição com o path da classe. Em outras palavras teremos todo/{id} e como parâmetro deste método dizemos que o parametro do path id é do tipo Long e será tratado dali para frente como variável id. Perceba que como não estamos inserindo dados não colocamos o controle de transação, como fizemos no método anterior.

Atualizar e Deletar

Com os conhecimentos demonstrados anteriomente, agora os métodos atualizar e deletar do nosso crud é basicamente a mesma coisa, mudando apenas o tipo da requisição (PUT, DELETE, e afins) e o tipo de retorno. Para não tornar este artigo longo não iremos detalhar aqui, porém, pode ser consultado aqui.

Criando nossos testes

No nosso projeto entregamos dois tipos de testes. O primeiro visa cobrir um fluxo real, desde a chamada do endpoint até a última interação com o banco de dados, e o segundo testa apenas uma pequena parte do código.

Teste de Integração

A fim de simplificar o artigo vamos demonstrar apenas um teste e ele ficará salvo em: src/test/java/br/com/codeinloop/TodoControllerTest.java

package br.com.codeinloop;

import io.quarkus.test.junit.QuarkusTest;
import jakarta.ws.rs.core.MediaType;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;

@QuarkusTest
public class TodoControllerTest {

    @Test
    public void testCreateEndpoint() {
        String todoJson = "{ \"title\": \"Test Todo\", \"completed\": false, \"order\": 1 }";

        given()
                .contentType(MediaType.APPLICATION_JSON)
                .body(todoJson)
                .when().post("/todo")
                .then()
                .statusCode(201)
                .body("title", is("Test Todo"));
    }
}
Java

Neste teste estamos testando o nosso endpoint de criação de tarefa e para isso precisamos enviar uma requição POST do tipo JSON, o corpo da requisição é definido na linha 16. Ao fazer esse envio aguardamos duas coisas: a primeira é que o status code seja 201, criado, veja o significado aqui e a segunda é que o corpo tenha o título igual ao que foi enviado. Com isso vamos conseguir testar todo o fluxo desde a controller, service até o repository.

Teste de Unidade

Diferente do teste de integração, o teste de unidade visa testar a menor parte do código possível e para nossa proposta vamos testar se o método getUrl da nossa entidade retorna a url correta caso a entidade não tenha um ID definido. Com isso chegamos nesse resultado:

package br.com.codeinloop.model;

import org.junit.jupiter.api.Test;

import java.net.URI;
import java.net.URL;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

public class TodoTest {
    @Test
    void testGetUrl() throws Exception {
        Todo todo = new Todo();
        todo.setUrl(new URI("http://localhost:8080/todo").toURL());

        URL resultUrl = todo.getUrl();
        assertNotNull(resultUrl);
        assertEquals("http://localhost:8080/todo", resultUrl.toString());
    }

}
Java

Esperamos que em nenhum momento a url esteja sem valor e que o valor seja igual ao definido na linha 19.

Conclusão

Com isso concluímos a construção dos endpoints e criação dos testes de integração e de unidade. Utilizamos na criação dessa API Java 21, Quarkus, Panache e RestEasy. Nosso próximo passo será a publicação do nosso serviço online, quer um artigo sobre? Também vamos automatizar o deploy desse projeto.

O que achou deste crud? Ficou com alguma dúvida? Não deixe de comentar. Caso queira contribuir com o projeto ele está no github.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.