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

Spring Boot. Personalizar el json de error 404 y Swagger

En el proyecto en el que estoy trabajando ahora tenemos una API de servicios Rest hecha con Spring Boot y documentada usando Swagger. Existe una clase anotada con @RestControllerAdvice para personalizar la respuesta de error. He observado que cuando sucede un error 404 no se está ejecutando el método correspondiente por lo que, el JSON de error retornado, tiene el formato por defecto de Spring. Para que esto no suceda.

En el fichero application.properties

spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false

Implementar @RestControllerAdvice

@RestControllerAdvice
public class MyControllerAdvice {

    @ExceptionHandler(NoHandlerFoundException.class)
    public ResponseEntity<?> handleControllerException(HttpServletRequest request,
                                                       NoHandlerFoundException ex) {
        LOGGER.debug("handleControllerException. message: {}", ex.getMessage());
        // Return custom response entity
    }
}

Con esto ya responde con el formato de JSON que esperábamos.

Hacer funcionar Swagger UI

Al añadir la propiedad spring.resources.add-mappings esta solución vemos que deja de funciona Swagger UI. Da este error:

org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation

El problema es que el autoconfigurador mvc de spring mapea recursos por defecto que son los que utiliza Swagger para mostrar su página web.

Finalmente copio el código del autoconfigurador pero hago que en lugar de aplicar a todas las url "/**" aplique sólo a "/swagger-ui.hml" tal y como se puede ver en el siguiente código:

@Configuration
@EnableWebMvc
public class MyWebMvcConfigurerAdapter extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");

        registry.addResourceHandler("/swagger-ui.html")
                .addResourceLocations(getStaticLocations());
    }

    private String[] getStaticLocations() {
        return new String[]{
                "/",
                "classpath:/META-INF/resources/",
                "classpath:/resources/",
                "classpath:/static/",
                "classpath:/public/"
        };
    }
}

Fuente

Error al arrancar docker: Failed with result ‘protocol’

Ejecutamos systemctl status docker.service para mostrar la información del estado del servicio.

docker.service - Docker Application Container Engine
   Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
   Active: failed (Result: protocol) since Fri 2017-12-29 10:05:40 CET; 2min 14s ago
     Docs: https://docs.docker.com
  Process: 14862 ExecStart=/usr/bin/dockerd -H fd:// (code=exited, status=1/FAILURE)
 Main PID: 14862 (code=exited, status=1/FAILURE)
      CPU: 43ms

dic 29 10:05:40 Dell-XPS-8900 systemd[1]: docker.service: Unit entered failed state.
dic 29 10:05:40 Dell-XPS-8900 systemd[1]: docker.service: Failed with result 'protocol'.
dic 29 10:05:40 Dell-XPS-8900 systemd[1]: docker.service: Service hold-off time over, scheduling restart.
dic 29 10:05:40 Dell-XPS-8900 systemd[1]: Stopped Docker Application Container Engine.
dic 29 10:05:40 Dell-XPS-8900 systemd[1]: docker.service: Start request repeated too quickly.
dic 29 10:05:40 Dell-XPS-8900 systemd[1]: Failed to start Docker Application Container Engine.
dic 29 10:05:40 Dell-XPS-8900 systemd[1]: docker.service: Unit entered failed state.
dic 29 10:05:40 Dell-XPS-8900 systemd[1]: docker.service: Failed with result 'protocol'.

A continuación Ejecutamos journalctl -xe para ver los logs del sistema.

dic 29 10:05:40 Dell-XPS-8900 systemd[1]: Starting Docker Application Container Engine...
-- Subject: Unit docker.service has begun start-up
-- Defined-By: systemd
-- Support: http://www.ubuntu.com/support
-- 
-- Unit docker.service has begun starting up.
dic 29 10:05:40 Dell-XPS-8900 dockerd[14862]: ERROR: The 'disable-legacy-registry' configuration option has been removed. Interacting with legacy (v1) registries is no longer supported
dic 29 10:05:40 Dell-XPS-8900 systemd[1]: docker.service: Main process exited, code=exited, status=1/FAILURE
dic 29 10:05:40 Dell-XPS-8900 systemd[1]: Failed to start Docker Application Container Engine.
-- Subject: Unit docker.service has failed
-- Defined-By: systemd
-- Support: http://www.ubuntu.com/support
-- 
-- Unit docker.service has failed.
-- 
-- The result is failed.
dic 29 10:05:40 Dell-XPS-8900 systemd[1]: docker.service: Unit entered failed state.
dic 29 10:05:40 Dell-XPS-8900 systemd[1]: docker.service: Failed with result 'protocol'.
dic 29 10:05:40 Dell-XPS-8900 systemd[1]: docker.service: Service hold-off time over, scheduling restart.
dic 29 10:05:40 Dell-XPS-8900 systemd[1]: Stopped Docker Application Container Engine.

Aquí se puede ver el motivo real del problema:

ERROR: The 'disable-legacy-registry' configuration option has been removed. Interacting with legacy (v1) registries is no longer supported

En el post Conectar a un Docker Registry inseguro en Ubuntu con Docker v17.06+ explicaba precisamente que una solución para acceder a un registry no securizado era modificar el fichero /etc/docker/daemon.json y añadir la línea:

"disable-legacy-registry": false,

Pues por lo visto esta opción de configuración ha sido deprecada en la versión actual de docker (17.12).

Se puede comprobar en el siguiente enlace.

Saludos

.ignore IntelliJ IDEA plugin

En mi opinión uno de los plugins esenciales cuando trabajas con IntelliJ IDEA.

Este me permite ocultar, en la vista “project”, aquellos ficheros que se encuentren ignorados por el gestor de versiones (.gitignore).

https://plugins.jetbrains.com/plugin/7495–ignore

Para activar/desactivar la ocultación de los ficheros ignorados sólo hay que abrir la paleta de comandos (control + shift + a) y buscar “show/hide ignored files”. Otra opción es desde el menú contextual (botón derecho sobre el nombre del proyecto).

Saludos