Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Table of Contents
minLevel1
maxLevel6
outlinefalse
typeflat
printablefalse

Introducción

gRPC es un framework de procedimiento remoto de alto rendimiento desarrollado por Google. Utiliza el protocolo HTTP/2 para la comunicación entre los servidores y los clientes, y permite la comunicación entre los servicios de diferentes lenguajes de programación.

Configuración del entorno

Info

En el archivo build.gradle se añadieron las dependencias necesarias de gRPC

Code Block
    testImplementation 'io.grpc:grpc-testing:1.41.0'
    testImplementation group: 'com.google.protobuf', name: 'protobuf-java', version:'3.23.2'
    implementation 'io.grpc:grpc-stub:1.53.0'
    implementation 'io.grpc:grpc-api:1.53.0'
    implementation 'io.grpc:protoc-gen-grpc-java:1.53.0'
    implementation 'io.grpc:grpc-core:1.53.0'
    implementation 'io.grpc:grpc-netty-shaded:1.53.0'
    implementation 'com.google.api.grpc:proto-google-common-protos:2.14.1'
    implementation 'javax.annotation:javax.annotation-api:1.3.2'
    implementation 'io.grpc:grpc-protobuf:1.55.1'

Archivos .proto

En gRPC, los servicios y los mensajes que se envían entre el cliente y el servidor se definen en archivos .proto, que utilizan el lenguaje de definición de interfaz (IDL) de Protocol Buffers. Aquí es donde se definen las operaciones que el servicio puede realizar y los tipos de datos que se envían y reciben. Esencialmente, es la especificación de la API de gRPC.

Info

Agregar los archivos .proto necesarios para las pruebas de servicios en src/main/proto/servicename

ejemplo:

Stubs

Un "stub" se refiere al cliente de un servicio gRPC que se conecta a un servidor. Es esencialmente código generado a partir del archivo .proto que un cliente puede usar para invocar métodos de un servicio remoto. Toma un mensaje de protocolo, serializa el mensaje en el formato correcto, envía la solicitud a través de la red, y luego espera y deserializa las respuestas para enviarlas de vuelta al código de llamada.

El enfoque más común para realizar pruebas en Java con gRPC es utilizar un stub generado automáticamente. Esto permite enfocarse en probar la lógica específica del cliente sin preocuparse por la complejidad de la comunicación subyacente.

Generar los stubs (archivos Java) a partir del archivo .proto:

Gradle posee un plugin que permite generar los archivos Java, los cuales incluyen el código para los servicios y mensajes a partir de los archivos .proto, utilizando el compilador de Protocol Buffers (protobuf-protoc).

Info

Se cuenta con el plugin de protobuf en el archivo build.gradle:

Code Block
apply 
plugin
'com.google.protobuf'
Info

También, se incluye la siguiente configuración para el plugin:

Code Block
protobuf {
    protoc {
        artifact = 'com.google.protobuf:protoc:3.23.2'
    }
    plugins{
        grpc{
            artifact = 'io.grpc:protoc-gen-grpc-java:1.53.0'
        }
    }
    generateProtoTasks {
        all()*.plugins {
            grpc{
            }
        }
    }
}

Para que el plugin compile los archivos .proto se ejecuta el comando gradle build y se generan los archivos Java necesarios.

Los stubs serán generados automáticamente en el directorio build/generated/source/proto/main/grpc.

Note

Si se desea no utilizar un stub y probar la comunicación con un servidor gRPC , se tendría que implementar manualmente toda la lógica de comunicación, serialización, deserialización y manejo de la comunicación de red, lo que puede ser bastante complejo y propenso a errores.

Tipos de Stubs en gRPC

Blocking (síncrono)

Realiza llamadas síncronas. Esto significa que cuando se hace una llamada a un método en el stub, la ejecución de ese hilo se bloquea hasta que se recibe la respuesta del servidor. Este comportamiento es útil en situaciones en las que se necesita esperar la respuesta del servidor antes de continuar con la ejecución del programa, como cuando se está realizando una operación de consulta o actualización de datos.

Non-blocking (Future stub)

Las llamadas son no bloqueantes, lo que significa que la llamada devuelve inmediatamente un Future que el cliente puede usar para obtener la respuesta cuando esté disponible.

Async (asíncrono)

Las llamadas son asincrónicas. El cliente debe pasar un StreamObserver a la llamada que será llamado cuando la respuesta esté disponible.

Implementación del cliente

Después de generar el código del cliente y del servidor, se implementan los stubs en los script de prueba.

Creación del tipo de Stub

En gRPC en necesario especificar el tipo de stub (cliente) que que se requiere usar para hacer una petición. El tipo de stub que se elija, determina cómo se maneja la petición.

Para seleccionar el tipo de stub, se deben usar los métodos correspondientes de la clase generada automáticamente a partir del .proto.

Por ejemplo, si el servicio se llama AccountAggregationService, gRPC generará una clase AccountAggregationServiceGrpc que contiene métodos: newBlockingStub, newFutureStub, y newStub que se pueden usar para crear los distintos tipos de stubs.

A continuación se especifica como se crea un stub de gRPC bloqueante para la interfaz AccountAggregationService. Esto es esencialmente un cliente que se usará para hacer llamadas al servicio.

Code Block
AccountAggregationServiceGrpc.AccountAggregationServiceBlockingStub = AccountAggregationServiceGrpc.newBlockingStub();

Implementación de Clases Base

ServiceBase.java

Info

Se optó por crear una clase principal con una estructura abstracta que maneje la apertura y cierre de canales, la infraestructura necesaria para construir y utilizar "stubs" gRPC, y la configuración de la URL del servidor que todos los tests de servicios gRPC requieren. Las subclases derivadas de ServiceBase deberán proporcionar la implementación específica de la construcción del stub.

ServiceBase.java es una clase abstracta genérica que proporciona la estructura y herramientas para configurar una conexión al servicio y comunicarse con él utilizando la librería de gRPC para la creación de canales y stubs.

  • Al iniciar una prueba, la clase se conecta a un servicio usando una dirección URL (url).

  • Una vez establecida la conexión, crea un "stub" o punto de conexión para comunicarse con el servicio.

  • Al finalizar una prueba, cierra la conexión.


Code Block
public abstract class ServiceBase<T extends AbstractStub<T>> extends TestBase

🛠️ La razón principal para hacer la clase abstracta es que contiene un método abstracto (buildStub) que las clases hijas deben implementar obligatoriamente.

🔗 T es un tipo genérico. La clase está diseñada para trabajar con cualquier tipo T que extienda (o sea un subtipo de) AbstractStub<T>.

Code Block
protected abstract T buildStub(ManagedChannel channel);

buildStubtiene el propósito de construir (o inicializar) un "stub". Es abstracto, no tiene una implementación en esta clase ya que las clases que hereden de esta deben proporcionarla.

Info

Cada servicio en gRPC puede tener su propio stub. Esta función será implementada por las subclases para construir el stub dependiendo del servicio que se esté probando.

Al ser T es un tipo que extiende de AbstractStub<T>. El método buildStub devolverá un objeto de tipo T, que será una especie de AbstractStub.

(ManagedChannel channel): El método recibirá un parámetro llamado channel de tipo ManagedChannel El cual representa una conexión a un servidor gRPC.

Code Block
languagejava
@Before
    public void setup() {
        channel = ManagedChannelBuilder.forTarget(url)
                .usePlaintext()
                .intercept(new LoggingClientInterceptor(url))
                .build();
        blockingStub = buildStub(channel);
    }
@After
public void tearDown() throws Exception {
    if (channel != null) {
        channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);
    }
}

Estos métodos se utilizan para configurar y limpiar recursos antes y después de cada prueba individual.

🚀 setup():

  1. Crea un canal gRPC para comunicarse con un servicio remoto a la url que será proporcionada por la clase que herede de ServiceBase.

  2. Añade un interceptor de registro. (Para imprimir en el log: Calling URL, Calling method, Sending message y Received response)

  3. Construye un "stub" bloqueante utilizando el canal recién creado.

🧹 tearDown():

  1. Verifica si el canal existe.

  2. Si existe, cierra el canal inmediatamente y espera hasta 5 segundos para que se cierre completamente.

MirrorStrategyServiceBase

AccountAggregationServiceBase.java

Info

Esta clase proporciona la implementación concreta para el método buildStub que está declarado en ServiceBase. Esta implementación crea y devuelve una nueva instancia de un stub bloqueante para AccountAggregationServiceGrpc usando el canal gRPC proporcionado.


Code Block
public abstract class MirrorStrategyServiceBaseAccountAggregationServiceBase extends ServiceBase<AccountAggregationServiceGrpcMirrorStrategyServiceBase<AccountAggregationServiceGrpc.AccountAggregationServiceBlockingStub>  {

Esta clase es específico está parametrizada con AccountAggregationServiceGrpc.AccountAggregationServiceBlockingStub para el tipo genérico T.

Es decir, MirrorStrategyServiceBase trabaja específicamente con el "stub bloqueante" del servicio AccountAggregationServiceGrpc.

Recordemos que AccountAggregationServiceGrpc es la clase generada automáticamente al compilarse un archivo .proto. Y AccountAggregationServiceBlockingStub es un tipo de stub de una clase interna generadas dentro de AccountAggregationServiceGrpc.

Por cada servicio a probar es necesario tener estas clases generadas para poderlas ocupar en la creación del tipo de stub.


Code Block
public MirrorStrategyServiceBaseAccountAggregationServiceBase() {
        setUrl(baseMirrorStrategyADDRESS);
    }

Cuando se crea una instancia de esta clase, automáticamente se establece su URL al valor de baseMirrorStrategyADDRESS usando el método setUrl.

Info
  • El valor de baseMirrorStrategyADDRESS se obtiene de la clase de configuración cuyo origen es el config.properties. Esta implementación es igual a la del proyecto de API REST.

    Code Block
    public abstract class TestConfig extends Runner {
    
    @BeforeClass
    public static void setUp() throws IOException {
        if (globalSetup) {
            ...
            baseMirrorStrategyADDRESS = configProperties.getProperty(PropertiesHandler.paths().get("baseMirrorStrategyADDRESS"));
        }
    }
    /**
     * End up execution after test class is being executed.
     */
    @AfterClass
    ...
    }

Code Block
@Override
    protected AccountAggregationServiceGrpc.AccountAggregationServiceBlockingStub buildStub(ManagedChannel channel) {
        return AccountAggregationServiceGrpc.newBlockingStub(channel);
    }

El tag @Override indica que ese método está sobrescribiendo al método de la clase en ServiceBase. Aquí ya se está proporcionando una implementación específica del método buildStub, recibe el canal creado para posteriormente crear un objeto stub bloqueante con el canal obtenido y lo devuelve de tipo AccountAggregationServiceGrpc.AccountAggregationServiceBlockingStub esto ya permitirá hacer las llamadas a procedimientos remotos en el @Before de la clase ServiceBase.

Construcción Test Cases

ServiceDetailsPositiveTests.java

Esta clase es un ejemplo de una clase de prueba para un servicio es especifico y hereda de MirrorStrategyServiceBase.

Code Block
@Test
@Title
("GRPC - Consult account details")

En esencia, esta clase está estructurada de la misma forma que en los test del proyecto de REST.

Contiene las notaciones de método de prueba y de la descripción del caso (@Tesy y @Title)


Construcción de la solicitud:

  • Se crea un objeto request de tipo AccountDetailsRequestDTO (generado por el compilador de Protocol Buffers-protoc).

  • Después se usa el método newBuilder() para iniciar la construcción del mensaje.

Como lo veíamos anteriormente gRPC, utiliza Protocol Buffers (protobuf) como su mecanismo de serialización. Uno de los métodos que se generan a partir del .proto es newBuilder(). Este método permite construir una instancia del mensaje paso a paso (Indicando las propiedades y valores del mensaje) y devuelve una instancia del builder asociado a ese objeto.

  • La función setAccountNumber() establece el número de cuenta obtenido de dataProperties

Este es otro método generado automáticamente por protoc basado en la definición del mensaje en el archivo .proto. En este caso, indica que hay que mandar un campo llamado accountNumber en el mensaje de AccountDetailsRequestDTO.

  • Finalmente, build() completa y devuelve una instancia completamente construida del mensaje.

Code Block
    @Test
    @Title("GRPC - Consult account details")
    public void getAccountDetailsTest() {
        AccountDetailsRequestDTO request = AccountDetailsRequestDTO
                .newBuilder()
                .setAccountNumber(dataProperties.getProperty(PropertiesConsts.MIRROR_STRATEGY_AUTOMATION_USER_1_ACCOUNT_NUMBER))
                .build();
        perform.performGRPCRequest(request, req -> blockingStub.getAccountDetails(req));
        validate.validateResponseStatusstatusCodeIs(Status.Code.OK);
        validate.validateResponseAgainstSchemaagainstJsonSchema(JsonSchema.ACCOUNTS_DETAILS);
        validate.validateProtoPropertyMatchesValuejsonNodeMatchesValue(Property.ACCOUNT_DATA_ACCOUNT_NUMBER, dataProperties.getProperty(PropertiesConsts.MIRROR_STRATEGY_AUTOMATION_USER_1_ACCOUNT_NUMBER));
    }

Envío de la solicitud GRPC:

  • Se realiza la solicitud GRPC usando el método performGRPCRequest de la clase Perform.

  • Se le pasa la solicitud que se creó en el paso anterior.

  • Se utiliza una lambda req -> blockingStub.getAccountDetails(req) para especificar cómo debe realizarse la solicitud. Esto indica que hay una instancia de blockingStub que tiene un método getAccountDetails para realizar la acción deseada.

En este caso la expresión lambda se ocupa ya que en la clase Perform tenemos definido un método de interface funcional.

Al invocar Perform.performGRPCRequest, se le pasan dos argumentos:

  1. El objeto request.

  2. Una expresión lambda: req -> blockingStub.getAccountDetails(req).

Dentro de la definición del método Perform.performGRPCRequest, la función tomará el primer argumento (request) y en algún punto, invocará la expresión lambda pasándole este argumento. En ese momento, req tomará el valor de request.

Validación:

  • Se valida el estado de la respuesta utilizando el método validateResponseStatus statusCodeIs de la clase Validate.

  • Se comprueba si el último código de estado (obtenido a través de Perform.latestStatusCode) es "OK", lo que indica una respuesta exitosa.

Info

Al igual que en el proyecto de rest se definen los objetos de perform y validate en el la clase TestBase.java

  • Code Block
    public abstract class TestBase extends TestConfig {
    
        @Steps
        protected static Perform perform;
        @Steps
        protected static Validate validate;
    }

Envío del mensaje

Perform.java

Esta clase define una forma estructurada de realizar llamadas a GRPC y manejar errores de manera uniforme.

Code Block
    @FunctionalInterface
    public interface GRPCCall<S> {
        Message execute(S request) throws StatusRuntimeException;
    }
  • @FunctionalInterface es una anotación que indica que la interfaz tiene un método abstracto y puede servir como tipo objetivo para una expresión lambda.

  • GRPCCall<S> es la interfaz funcional que define un método execute, que toma un argumento S (la solicitud) y devuelve un objeto de tipo Message


Code Block
public <S> void performGRPCRequest(S request, GRPCCall<S> call) {
        try {
            response = protobufToJson(call.execute(request));
            latestStatusCode = Status.Code.OK;
        } catch (StatusRuntimeException e) {
            System.out.println("Error: " + e.getStatus())ErrorInfo errorInfo = null;
            response final com.google.rpc.Status status = nullStatusProto.fromThrowable(e);

            try {
  latestStatusCode = e.getStatus().getCode();         }   for  }

Este es el método principal que se usa previamente en la clase de los tests. Su propósito principal es ejecutar una llamada gRPC usando la función (call) con la solicitud dada (request). También maneja cualquier excepción StatusRuntimeException que pueda ocurrir durante la ejecución y registra el estado de la respuesta (ya sea exitoso o un código de error).

Info

response y latestStatusCode están definidos en la clase de TestConfig para que posteriormente puedan ser usados por las clases que extienden de TestConfig como Validate.

Validate.java

Contiene un conjunto de métodos para realizar diferentes tipos de validaciones en respuestas, obtenidas a través de llamadas gRPC.

A través del método protobufToJson se hace una conversión del mensaje de la respuesta a un formato json que nos permite validar elementos de la respuesta como el json schema, o el valor de alguno de sus nodos.

Code Block

public abstract class Validate extends TestConfig {(Any any : Objects.requireNonNull(status).getDetailsList()) {
                    if (!any.is(ErrorInfo.class)) {
                  @Step("Status Code is {1}")    continue;
public  void validateResponseStatus(Status.Code expectedCode) {         Assert.assertEquals(expectedCode, latestStatusCode);     }
     @Step("Json Schema file {1} is correct")     public void validateResponseAgainstSchema(String jsonSchema) { errorInfo = any.unpack(ErrorInfo.class);
     try {          }
  String jsonResponse = protobufToJson(response);       } catch (InvalidProtocolBufferException invalidProtocolBufferException) {
 try (InputStream schemaStream = Files.newInputStream(new File(jsonSchemasPath + jsonSchema + ".json").toPath())) {     logger.error("performGRPCRequest :: An error occurred while reading the properties.");
   JSONObject rawSchema = new JSONObject(new JSONTokener(schemaStream));    }

           Schema schemaJSONObject dataError = SchemaLoader.load(rawSchemanew JSONObject();

            if (errorInfo  schema.validate(new JSONObject(jsonResponse));
!= null) {
           }     for (Map.Entry<String, String> entry } catch (Exception e: errorInfo.getMetadataMap().entrySet()) {
            LOGGER.error("Failed to validate the response against the schema.", e);
 try {
           throw    new RuntimeException("Failed to validate the response against the schema dueJSONObject to:fieldValue "= +new eJSONObject(entry.getMessagegetValue(), e);
        }      }          @Step("Json node exist")dataError.put(entry.getKey(), fieldValue);
    public void validatePropertyValueExists(String propertyName, String expectedValue) {         try {} catch (JSONException jsonException) {
             String jsonResponse = protobufToJson(response);
             try {
         List<String> propertyValues = JsonPath.read(jsonResponse, "$.." + propertyName);             booleanJSONArray foundjsonArray = propertyValues.stream().anyMatch(value -> value.equals(expectedValuenew JSONArray(entry.getValue());
              if (!found) {                 Assert.fail("Expected value for property " + propertyName + " not found in the response.");dataError.put(entry.getKey(), jsonArray);
                        } catch (JSONException e2) {
    } catch (Exception e) {             LOGGER.error("Failed to validate the value for property " + propertyName, e); dataError.put(entry.getKey(), entry.getValue());
                Assert.fail("Failed to validate the value for property " + propertyName + " due to: " + e.getMessage());        }
                    }
    }         private String protobufToJson(Message message)}
throws InvalidProtocolBufferException {         return JsonFormat.printer().print(message);
    }
} }

            response = dataError.toString(4);
            logger.error("Error: {}", response);
            latestStatusCode = e.getStatus().getCode();
        }
    }

Este es el método principal que se usa en la clase de los tests. Su propósito principal es ejecutar una llamada gRPC usando la función (call) con la solicitud dada (request), convierte la respuesta Protobuf a JSON y la guarda en response, actualizalatestStatusCode a "OK".

Si se produce una excepción StatusRuntimeException, el método extrae la información detallada del error usando StatusProto.fromThrowable() y ErrorInfo. Luego, crea un objeto JSON (dataError) que contiene los metadatos de error y lo guarda en response, además de registrar el error, actualiza latestStatusCode con el código de estado del error.

A través del método protobufToJson se hace una conversión del mensaje de la respuesta a un formato json que nos permite validar elementos de la respuesta como el json schema, o el valor de alguno de sus nodos.

Info

response y latestStatusCode están definidos en la clase de TestConfig para que posteriormente puedan ser usados por las clases que extienden de TestConfig como Validate.

Validaciones de la respuesta

Validate.java

Contiene un conjunto de métodos para realizar diferentes tipos de validaciones en respuestas, obtenidas a través de llamadas gRPC.

Code Block
public abstract class Validate extends TestConfig {

    @Step("Status Code is {0}")
    public void statusCodeIs(Status.Code expectedCode) {
        Assert.assertEquals(expectedCode, latestStatusCode);
    }

    @Step("Validate json node {0} matches text {1}")
    public void jsonNodeMatchesValue(String jsonNode, String expectedText) {
        String actualValue = JsonPath.read(response, "$." + jsonNode);
        Assert.assertEquals(expectedText, actualValue);
    }

    @Step("Validate json Schema file {0} is correct")
    public void againstJsonSchema(String jsonSchema) {
        try (InputStream schemaStream = Files.newInputStream(new File(jsonSchemasPath + jsonSchema + ".json").toPath())) {
            JSONObject rawSchema = new JSONObject(new JSONTokener(schemaStream));
            Schema schema = SchemaLoader.load(rawSchema);
            schema.validate(new JSONObject(response));
        } catch (Exception e) {
            logger.error("Failed to validate the response against the schema.", e);
            throw new RuntimeException("Failed to validate the response against the schema due to: " + e.getMessage(), e);
        }
    }

    @Step("Validate json node {0} contains text {1}")
    public void jsonNodeContainsText(String jsonNode, String expectedText) {
        String actualValue = JsonPath.read(response, "$." + jsonNode);
        Assert.assertTrue(actualValue.contains(expectedText));
    }
}

Ejecución y resultados de las pruebas:

Note

Antes de ejecutar pruebas gRPC, es indispensable que las clases necesarias sean generadas a partir de los archivos Protocol Buffers (.proto).

@WithTag

El uso del tag WithTag es una forma de categorizar y filtrar pruebas específicas que queremos ejecutar.

La opción -Ptags del comando Gradle permite que sólo se ejecuten pruebas que coincidan con las etiquetas especificadas.

Code Block
@WithTag("Mirror-Strategy-Account-Aggregation-Service")
public abstract class AccountAggregationServiceBase extends MirrorStrategyServiceBase<AccountAggregationServiceGrpc.AccountAggregationServiceBlockingStub> {

Comando de ejecución

Para realizar una ejecución de los scripts se usa el comando Gradle.

Si se requiere ejecutar pruebas específicas, se pueden utilizar la etiqueta antes mencionada.

./gradlew test -Ptags=etiqueta-relacionada-con-grpc

Image Added