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:
- En el test creamos una instancia de
MovieUpdateParam, pasándole una duración de 90 minutos. - El test debe serializar esa instancia en
JSONpara hacer un request al endpoint, para ello utilizaJackson. - Esperamos que genere esto:
{ "durationMinutes": 90 }, el problema es que para serializar ese90, como es unDuration(el de la instancia del punto 1),Jacksonva a utilizar por defecto su métodoserializede arriba. - Eso provoca que un
Duration.ofMinutes(90L)se serialice usandoduration.getSeconds(), lo que genera unJSONtal que así:{ "durationMinutes": 5400 }. - 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());
}
}
