Testando Duration Con Jackson

Que buen título. Digamos que estás programando con java, estás creando tu bonita API REST, y llega un momento en el que quieres testear un endpoint. Ese endpoint es POST/PUT, y espera recibir, por ejemplo, la duración en minutos de una película.

Dependiendo del framework usado (en este caso Dropwizard) e historias varias, el código variará muchísimo, pero pongamos que tenemos algo así:

@PUT
@Timed
@UnitOfWork
@Path("/update/movie")
public Response updateDuration(
  @Valid final MovieUpdateParam updateParam
) {
  ...
}
public class MovieUpdateParam {
  @NotNull
  private final Long durationMinutes;

  @JsonCreator
  public MovieUpdateParam(
    @JsonProperty("durationMinutes") final Long durationMinutes,
  ) {
    this.durationMinutes = durationMinutes;
  }

  public Duration getDurationMinutes() {
    return Duration.ofMinutes(duration);
  }
}

Entonces, para testearlo, algo como:

@Test
public void testMovieUpdateParam() {
  final MovieUpdateParam param = new MovieUpdateParam(
    Duration.ofMinutes(90L)
  );
  final Entity<MovieUpdateParam> entity = Entity.json(param);
  final Response response = ResourceTestRule.client()
    .target("/update/schedule")
    .request(MediaType.APPLICATION_JSON)
    .put(entity);

  assertThat(response.getStatus()).isEqualTo(Response.Status.OK.getStatusCode());
}

Buceando en el código (gracias debugger, gracias java), llegamos a estas líneas de Jackson:

public void serialize(Duration duration, JsonGenerator generator, SerializerProvider provider) throws IOException {
  if (this.useTimestamp(provider)) {
    if (provider.isEnabled(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)) {
      generator.writeNumber(DecimalUtils.toBigDecimal(duration.getSeconds(), duration.getNano()));
    } else {
      generator.writeNumber(duration.toMillis());
    }
  } else {
      generator.writeString(duration.toString());
  }
}

Pues ahí lo tienes, esto es lo que va a pasar:

  1. En el test creamos una instancia de MovieUpdateParam, pasándole una duración de 90 minutos.
  2. El test debe serializar esa instancia en JSON para hacer un request al endpoint, para ello utiliza Jackson.
  3. Esperamos que genere esto: { "durationMinutes": 90 }, el problema es que para serializar ese 90, como es un Duration (el de la instancia del punto 1), Jackson va a utilizar por defecto su método serialize de arriba.
  4. Eso provoca que un Duration.ofMinutes(90L) se serialice usando duration.getSeconds(), lo que genera un JSON tal que así: { "durationMinutes": 5400 }.
  5. Eso es lo que va a recibir el endpoint, acabando con un Duration.ofMinutes(5400L), por lo tanto lo que sea que queramos testear fallará.

Es un jaleo, lo sé. Resumiendo ¿cómo se soluciona el entuerto? pues con la anotación @JsonSerialize, que permite especificar una clase para serializar una propiedad en vez de utilizar el método por defecto de Jackson:

public class MovieUpdateParam {
  @NotNull
  private final Long durationMinutes;

  @JsonCreator
  public MovieUpdateParam(
    @JsonProperty("durationMinutes") final Long durationMinutes,
  ) {
    this.durationMinutes = durationMinutes;
  }

  @JsonSerialize(using = DurationSerializer.class)
  public Duration getDurationMinutes() {
    return Duration.ofMinutes(duration);
  }
}

class DurationSerializer extends JsonSerializer<Duration> {
  @Override
  public void serialize(final Duration value, final JsonGenerator generator, final SerializerProvider provider) throws IOException {
    generator.writeNumber(value.toMinutes());
  }
}

Utilizar Github For Windows Con Bitbucket

O GitHub for Mac, aunque aquí haya más alternativas como Tower (brutal) o SourceTree, pero el caso es que la GUI de GitHub no limita su uso a sus repositorios.

Aquí daré por sentado lo siguiente:

  • Tenéis Windows: es sangrante la falta de un cliente de git de referencia, y mira que he sido años usuario de TortoiseSVN, pero TortoiseGit… pse…
  • Tenéis cuenta en GitHub: atento, no es obligatorio, pero te va a facilitar el paso de la creación de las claves SSH y encontrar trabajo.
  • Tenéis cuenta en Bitbucket: repositorios privados, no digo más.

Bien, lo primero será descargar e instalar GitHub for Windows. Se han currado la interfaz Metro, eh?

Aunque como digo se puede utilizar sin GitHub, vamos a loguearnos en nuestra cuenta, porque esto automáticamente nos creará la clave SSH que necesitaremos en Bitbucket. Para confirmar que así ha sido, podéis ir a vuestro perfil y comprobar que se ha añadido una nueva en el listado de SSH keys.

Ahora vamos a copiar la clave pública generada, que se encuentra en el directorio .ssh de vuestro usuario en Windows (C:\Users\<tu usuario>\.ssh), en el fichero _github_rsa.pub_.

Con ese chorizín nos vamos a nuestra cuenta en Bitbucket y lo añadimos como nueva SSH key (https://bitbucket.org/account/user/<tu usuario>/ssh-keys/).

Suponiendo que ya tenéis un repositorio creado en Bitbucket, también en posible que ya lo tengáis clonado en vuestro PC. Si no, lo vamos a hacer con otra bonita herramienta que instala el programa, el Git Shell. Lo abrimos, nos vamos al directorio de nuestra elección y le cascamos un entrañable clone:

Git clone

Pero poniendo la dirección de vuestro repositorio, que hay que explicarlo todo!

Ya sólo queda arrastrar directamente esta nueva carpeta al programa, et voilà! a partir de ahora podemos gestionar nuestro prometedor repositorio desde esta más que correcta herramienta, veremos cómo evoluciona.