Ошибка даты модульного теста Spring Boot WebMvc: возвращенная дата выполнения () раньше, чем ожидалось (из-за предположения о GMT)

Я использую Spring Boot Test framework + Mockito для тестирования модулей контроллера. Я обнаружил, что тест не проходит из-за странной ошибки: когда я говорю Mockito вернуть объект с полем java.util.Date, ответ всегда содержит строку даты на день раньше, чем ожидалось.

Мой тест такой:

package com.mycompany.myapp.controller;

import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.text.ParseException;
import java.text.SimpleDateFormat;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import com.privalia.binlookup.model.BinInfo;
import com.privalia.binlookup.repo.BinInfoRepository;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.MOCK, properties = "classpath:test.properties")
@AutoConfigureMockMvc(secure=false)
public class BinInfoControllerUnitTests {
    @InjectMocks
    private BinInfoController controller;

    private MockMvc mockMvc;

    @MockBean
    private BinInfoRepository repository;

    private BinInfo mockBinInfo;

    @Before
    public void init() throws ParseException{
        SimpleDateFormat fmt = new SimpleDateFormat("dd/MM/yyyy");
        MockitoAnnotations.initMocks(this);
        mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
        this.mockBinInfo = new BinInfo();
        this.mockBinInfo.setId(new Long(42));
        this.mockBinInfo.setBin("touhou");
        this.mockBinInfo.setJson_full("{is_json:true}");
        this.mockBinInfo.setCreateAt(fmt.parse("18/08/2018"));
    }


    @Test
    public void testBinInfoControllerSearchBIN() throws Exception {
        when(this.repository.findByBin("touhou")).thenReturn(mockBinInfo);

        this.mockMvc.perform(get("/search/touhou")
                .accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
                .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
            .andExpect(content().string(not("")))
            .andExpect(jsonPath("$.bin", is("touhou")))
            .andExpect(jsonPath("$.json_full", is("{is_json:true}")))
            .andExpect(jsonPath("$.createAt", is("18/08/2018")));
    }
}

И мой контроллер:

package com.mycompany.myapp.controller;

import java.awt.MediaTracker;
import java.util.List;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.ResponseEntity.BodyBuilder;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.privalia.binlookup.model.BinInfo;
import com.privalia.binlookup.repo.BinInfoRepository;

@RestController
@RequestMapping(value = "/")
public class BinInfoController {

    @Autowired
    private BinInfoRepository repository;

    @RequestMapping(value = "/search/{bin}", method=RequestMethod.GET)
    public ResponseEntity<BinInfo> searchBIN(@Valid @PathVariable String bin) {
        BinInfo found = repository.findByBin(bin);
        BodyBuilder builder = ResponseEntity.status(HttpStatus.OK);
        builder.contentType(MediaType.APPLICATION_JSON_UTF8);
        return (found == null ? builder.body(null) : builder.body(found));
    }

}

Тест не пройден, потому что возвращенная строка createAt - это 17/08/2018, а не 18/08/2018.

Я нахожусь в часовом поясе «Мадрид / Европа (GMT +0200, CEST)», потому что сейчас лето и летнее время, и я подозреваю, что некоторые временные зоны мешают.

Мое предположение:

У фиктивного объекта есть объект java.util.Date без информации о часовом поясе, это 00:00 18 августа; при передаче контекста Spring или Jackson или Mockito в perform() проанализированный JSON считается GMT, то есть минус 2 часа, то есть до 17 августа.

но я не знаю, где найти соответствующую конфигурацию, сообщить ли Джексону, или Spring, или Mockito нет, чтобы выполнить преобразование.

Обновлено:

Причина подтверждена: когда я меняю строку даты на 18/08/2018 02:00:00, возвращаемая дата - 18/08/2018 00:00:00.

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
0
1 629
1

Ответы 1

После некоторой реальной борьбы я обнаружил, что мне нужно использовать OffsetDateTime в полях даты и времени моей сущности, а в PostgreSQL я должен использовать timestamptz (временная метка с часовым поясом). Кроме того, строка datetime для синтаксического анализа должна содержать информацию о часовом поясе.

Поле фасоли:

@NotNull
@Column(name = "updated_at", columnDefinition = "TIMESTAMP WITH TIME ZONE")
// Formats output date when this DTO is passed through JSON
@JsonFormat(pattern = "dd/MM/yyyy HH:mm:ss Z")
// Allows date to be passed into GET request in JSON
@DateTimeFormat(pattern = "dd/MM/yyyy HH:mm:ss Z")
@JsonProperty("updatedAt")
/**
 * A timestamp object representing the moment the bininfo is updated.
 * In database we store the value with UTC timestamp, timestamptz.
 * When we save, local time will be converted to UTC, and when retrieve,
 * the offset is handled automatically.
 */
private OffsetDateTime updatedAt;

Построение объекта для тестирования:

private String format; // 'dd/MM/yyyy HH:mm:ss Z'

private String dateString; // '18/08/2018 02:00:00 +0200'

@Before
public void init() throws ParseException{
    MockitoAnnotations.initMocks(this);
    mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
    this.mockBinInfo = new BinInfo();
    this.mockBinInfo.setId(new Long(42));
    this.mockBinInfo.setBin("touhou");
    this.mockBinInfo.setJson_full(this.json_full);
    this.mockBinInfo.setBrand(this.brand);
    this.mockBinInfo.setType(this.type);
    this.mockBinInfo.setCountry(this.country);
    this.mockBinInfo.setIssuer(this.issuer);
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
    OffsetDateTime d = OffsetDateTime.parse(dateString, formatter);
    this.mockBinInfo.setCreatedAt(d);
    this.mockBinInfo.setUpdatedAt(d);
}

Определение столбца таблицы PostgreSQL:

updated_at | timestamp with time zone |

И сохраненное значение:

test1=# select * from bin_info;
| .. |       updated_at
+----+------------------------
| .. | 2018-08-18 00:00:00+02

Обратите внимание, что в моем JSON метка времени - 02:00:00 +0200 (местное время со смещением часового пояса), в БД сохраняется как 00:00:00+02 (GMT со смещением). Я не менял настройки часового пояса / локали / страны JVM.

Другие вопросы по теме