Spring Testing. Mostrar información de las peticiones

Cuando usamos MockMvc para testear controladores de Spring, es muy útil disponer del máximo de información referente a la petición realizada desde el test. Una forma muy sencilla es indicarle al «builder» que queremos que muestre en la salida del test dicha información.

import org.junit.Before;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;

@RunWith(MockitoJUnitRunner.class)
public abstract class MyTestClass {

    MockMvc mvc;

    @Mock
    MyRepository myRepository;

    @Before
    public void initialize() {

        mvc = MockMvcBuilders.standaloneSetup(new MyController(new MyService(myRepository)));
                .alwaysDo(print()) // Esta es la línea importante
                .build();
    }
}

Esto hace que para cada petición se se hace desde un test unitario via MockMvc muestre una información detallada de la misma:

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /my-request-path
       Parameters = {my-request-param=[my-request-param-value]}
          Headers = {my-header=[my-header-value]}

Handler:
             Type = com.example.MyController
           Method = public org.springframework.http.ResponseEntity<java.util.List<com.example.MyResponseClass>> com.example.MyController.myMethod(com.example.MyRequestBodyClass)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[application/json;charset=UTF-8]}
     Content type = application/json;charset=UTF-8
             Body = [{"field":"value"}]
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

Tener en cuenta que hasta versión 5.0 de Spring Framework no se muestra el contenido de la request tal y como se puede leer en el siguiente jira:

https://jira.spring.io/browse/SPR-14717

Mejorar la salida de los tests de una aplicación Spring Boot

Cuando hago aplicaciones con SpringBoot abruma un poco la cantidad de información que este muestra en el log cuando está activado el debug.

El tema es que el nivel de log lo suelo configurar a través del fichero application.properties pero cuando se ejecutan tests de JUnit donde no interviene Spring para nada este valor no se recoje y por defecto muestra todo a nivel DEBUG.

Añadiendo este fichero logback-test.xml en la ruta src/test/resources conseguiremos que los mensajes del framework Spring se muestren con nivel WARN mientras que los del paquete de mi aplicación (en el ejemplo: com.my.package) se muestren con el nivel más alto TRACE.

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/base.xml" />
    <logger name="org.springframework" level="WARN"/>
    <logger name="com.my.package level="TRACE"/>
</configuration>

De esta forma la salida (standard output) de los tests queda muchísimo más limpia y prácticamente se limita a los mensajes própios de mi aplicación.

Gradle. Buscar nuevas versiones de las librerías que usa tu proyecto

La tarea de buscar manualmente si existen nuevas versiones de las librerías que usamos en un proyecto puede ser muy tediosa. El plugin de gradle gradle-versions-plugin nos permite elaborar un informe acerca de las nuevas versiones de librerías que usemos.

En el fichero build.gradle de la carpeta raíz de nuestro proyecto Gradle tendremos que:

1. Asegurarnos que tiene definido el repositorio jcenter.
2. Añadir, en el apartado buildscript -> dependencies la dependencia del plugin gradle-versions.
3. En la sección allprojects indicar que este plugin estará disponible para todos los proyectos.

El siguiente trozo de código muestra las líneas implicadas.

buildscript {
    
    repositories {
        jcenter()
    }
    dependencies {
        classpath "com.github.ben-manes:gradle-versions-plugin:0.20.0"
    }
}

allprojects {

    apply plugin: "com.github.ben-manes.versions"
}

A continuación, ya podemos extraer un informe donde se nos indicará qué librerías que estamos utilizando disponen de una versión más actual a la utilizada por nuestro proyecto. En mi caso, como se trata de un proyecto Android lo haré sobre el módulo app.

./gradlew app:dependencyUpdates

La salida de este comando para mi proyecto es la siguiente:

> Task :app:dependencyUpdates 

------------------------------------------------------------
:app Project Dependency Updates (report to plain text file)
------------------------------------------------------------

The following dependencies are using the latest milestone version:
 - android.arch.persistence.room:compiler:1.1.1
 - android.arch.persistence.room:runtime:1.1.1
 - com.android.support:appcompat-v7:28.0.0
 - com.android.support:cardview-v7:28.0.0
 - com.android.support:design:28.0.0
 - com.android.support.test:runner:1.0.2
 - com.android.support.test.espresso:espresso-core:3.0.2
 - com.github.PhilJay:MPAndroidChart:v3.1.0-alpha
 - com.google.code.gson:gson:2.8.5
 - com.google.truth:truth:0.42
 - com.google.truth.extensions:truth-java8-extension:0.42
 - com.jakewharton:butterknife:9.0.0-rc1
 - com.jakewharton:butterknife-compiler:9.0.0-rc1
 - junit:junit:4.12
 - org.eclipse.paho:org.eclipse.paho.android.service:1.1.1
 - org.mockito:mockito-core:2.23.0

The following dependencies have later milestone versions:
 - com.android.support.constraint:constraint-layout [1.1.3 -> 2.0.0-alpha2]
     http://tools.android.com
 - com.android.tools.build:aapt2 [3.2.1-4818971 -> 3.4.0-alpha03-5013011]
     https://developer.android.com/studio
 - com.android.tools.lint:lint-gradle [26.2.1 -> 26.4.0-alpha03]
     https://developer.android.com/studio
 - com.google.dagger:dagger-android [2.15 -> 2.19]
     https://github.com/google/dagger
 - com.google.dagger:dagger-android-processor [2.15 -> 2.19]
     https://github.com/google/dagger
 - com.google.dagger:dagger-android-support [2.15 -> 2.19]
     https://github.com/google/dagger
 - com.google.dagger:dagger-compiler [2.15 -> 2.19]
     https://github.com/google/dagger
 - io.reactivex.rxjava2:rxandroid [2.0.2 -> 2.1.0]
     https://github.com/ReactiveX/RxAndroid
 - io.reactivex.rxjava2:rxjava [2.1.16 -> 2.2.3]
     https://github.com/ReactiveX/RxJava
 - org.eclipse.paho:org.eclipse.paho.client.mqttv3 [1.1.1 -> 1.2.0]
     http://www.eclipse.org/paho

Gradle updates:
 - Gradle: [4.6 -> 4.10.2 -> 5.0-rc-3]

Generated report file build/dependencyUpdates/report.txt


BUILD SUCCESSFUL in 3s
1 actionable task: 1 executed

Spring. Hacer que HttpLogginInterceptor muestre mensajes en el log con nivel DEBUG en lugar de INFO

Si usas okhttp en alugna aplicación Spring (o Java) hay una clase HttpLoggingInterceptor que nos permite, en tiempo de desarrollo, volcar toda la información acerca de las peticiones que hace el cliente http en el log de la aplicación.

Por defecto, HttpLoggingInterceptor muestra los mensajes en nivel de INFO, en mi caso, prefería que se mostrasen con nivel de debug.

@Configuration
public class HttpClientModule {
    
    private static final Logger logger = LoggerFactory.getLogger(HttpClientModule.class);

    @Bean
    public OkHttpClient okHttpClient(@Value("${debug.requests}") boolean debug) {
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        if (debug) {
            // La siguiente línea es la importante
            HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(logger::debug); 
            interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
            builder.addInterceptor(interceptor);
        }
        return builder.build();
    }
}

Suscribirse a un topic MQTT desde Android

Para la conexión utilizaremos la librería Eclipse Paho Android Service.

Configuración Gradle

En el fichero `build.gradle` de nuestro proyecto Android será necesario añadir las siguientes dependencias:

    implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.1'
    implementation('org.eclipse.paho:org.eclipse.paho.android.service:1.1.1') {
        exclude group: 'com.android.support'
        exclude module: 'appcompat-v7'
        exclude module: 'support-v4'
    }

Servidor MQTT de pruebas (Mosquitto)

Dockerfile

FROM eclipse-mosquitto:1.4.12
ADD mosquitto.conf /mosquitto/config/mosquitto.conf

mosquito.conf

persistence true
persistence_location /mosquitto/data/
log_dest file /mosquitto/log/mosquitto.log

docker-compose.yml

version: '3'
services:
  mqttbroker:
    build: .
    image: test-mosquitto:0.0.1-SNAPSHOT
    container_name: test.mosquitto.container
    ports:
      - 9001:9001
      - 1883:1883

Para levantar el servicio MQTT será suficiente con ejecutar `docker-compose up -d` en la ruta donde se encontrasen los archivos anteriores.

Código Android

La aplicación de prueba consiste en una activity vacía que se suscribe al topic `test-topic` y muestra los mensajes recibidos en el log de Android.
AndroidManifest.xml
En el fichero AndroidManifest.xml será necesario añadir los siguiente permisos:

<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />

También habrá que añadir el servicio `MqttService` proporcionado por la librería:

<service android:name="org.eclipse.paho.android.service.MqttService" />

MainActivity.java

package org.eurecat.test.mqtt.android;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

import org.eclipse.paho.android.service.MqttAndroidClient;
import org.eclipse.paho.client.mqttv3.IMqttActionListener;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.IMqttToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;

import java.util.UUID;

public class MainActivity extends AppCompatActivity implements MqttCallback, IMqttActionListener {

    private static final String SERVER_URI = "tcp://10.42.0.1:1883";
    private static final String TOPIC = "test-topic";
    private static final int QOS = 1;
    private static final String TAG = "MainActivity";

    private MqttAndroidClient mqttAndroidClient;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        String clientId = UUID.randomUUID().toString();
        Log.d(TAG, "onCreate: clientId: " + clientId);

        mqttAndroidClient = new MqttAndroidClient(getApplicationContext(), SERVER_URI, clientId);
        mqttAndroidClient.setCallback(this);

        MqttConnectOptions mqttConnectOptions = new MqttConnectOptions();
        mqttConnectOptions.setCleanSession(false);


        try {
            Log.d(TAG, "onCreate: Connecting to " + SERVER_URI);
            mqttAndroidClient.connect(mqttConnectOptions, null, this);
        } catch (MqttException ex){
            Log.e(TAG, "onCreate: ", ex);
        }
    }

    @Override
    public void onSuccess(IMqttToken asyncActionToken) {
        Log.d(TAG, "onSuccess: ");
        try {
            mqttAndroidClient.subscribe(TOPIC, QOS);
        } catch (Exception e) {
            Log.e(TAG, "Error subscribing to topic", e);
        }
    }

    @Override
    public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
        Log.e(TAG, "Failed to connect to: " + SERVER_URI, exception);
    }

    @Override
    public void connectionLost(Throwable cause) {
        Log.d(TAG, "connectionLost: ", cause);
    }

    @Override
    public void messageArrived(String topic, MqttMessage message) {
        Log.d(TAG, "Incoming message: " + new String(message.getPayload()));
    }

    @Override
    public void deliveryComplete(IMqttDeliveryToken token) {
        Log.d(TAG, "deliveryComplete: ");
    }

}

Pruebas

En Ubuntu, es posible instalar el cliente ejecutando:

sudo apt install mosquitto-clients

Para enviar un mensaje al topic `test-topic` ejecutaremos:

mosquitto_pub -h localhost -t test-topic -m 'test mqtt message'

Esta es la salida del log de android:

10-26 13:44:05.679 24785 24785 D MainActivity: Incoming message: test mqtt message