Я использую 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.




После некоторой реальной борьбы я обнаружил, что мне нужно использовать 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.