I am writing my end-to-end tests for an application. One of those tests will verify that when some required input is missing, it will not persist the data to the database. All is running fine, untill I decided to annotate my test class with #Transactional, since I don't need to persist the results when the tests are finished. The moment I add the #Transactionalannotation, the application is suddenly ok with a non-null field to be null.
How the database is set up:
create table MY_TABLE
(
MY_FIELD VARCHAR(20) NOT NULL
)
The corresponding model:
#Entity
#Table(name = "MY_TABLE")
public class MyObject {
#Column(name = "MY_FIELD", nullable = false)
private String MyField = null;
}
Saving goes via the service layer:
#Service
public class MyAppServiceImpl implements MyAppService {
#Inject
private MyObjectRepository myObjectRepository; //this interface extends CrudRepository<MyObject , Long>
#Override
#Transactional
public MyObject save(MyObject myObject) {
return myObjectRepository.save(myObject);
}
}
In the resource layer, the value of myField is set to NULL, so at the moment the data is persisted, I should receive an ORA-error
#Controller
public class MyAppResourceImpl implements MyAppResource {
#Inject
private MyAppService myAppService;
public ResponseEntity doPost(#RequestBody String xml) {
MyObject myobject = new MyObject();
myObject.setMyField(null);
try {
myAppService.save(myObject);
return new ResponseEntity(null, HttpStatus.CREATED);
} catch (SomeExceptions e) {
return new ResponseEntity("Could not save", HttpStatus.BAD_REQUEST);
}
}
}
Finally, the test:
#RunWith(SpringRunner.class)
#SpringBootTest
//#Transactional
public class MyAppResourceImplEndToEndTest {
#Inject
private MyAppResourceImpl myAppResource;
#Test
public void testWithFieldNull() throws Exception {
ResponseEntity response = myAppResource.doPost("some-xml");
Assertions.assertThat(response.GetStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
}
This test works, as expected, but other tests persist data into the database. This is not the behavior I wanted, so I annotated the test class with #Transactional. Now no tests persist their results to the database, but this specific test method fails because
Expected :400
Actual :201
Why does the #Transactional annotation suddenly cause NULL values to be persisted in NOT NULL columns?
Related
As we all know, there is a big problem with a partial update of the entity. Since the automatic conversion from json strings to the entity, all fields that have not been transferred will be marked null. And as a result, the fields that we did not want to reset will be reset.
I will show the classical scheme:
#RestController
#RequestMapping(EmployeeController.PATH)
public class EmployeeController {
public final static String PATH = "/employees";
#Autowired
private Service service;
#PatchMapping("/{id}")
public Employee update(#RequestBody Employee employee, #PathVariable Long id) {
return service.update(id, employee);
}
}
#Service
public class Service {
#Autowired
private EmployeeRepository repository;
#Override
public Employee update(Long id, Employee entity) {
Optional<T> optionalEntityFromDB = repository.findById(id);
return optionalEntityFromDB
.map(e -> saveAndReturnSavedEntity(entity, e))
.orElseThrow(RuntimeException::new);
}
private T saveAndReturnSavedEntity(Employee entity, Employee entityFromDB) {
entity.setId(entityFromDB.getId());
return repository.save(entity);
}
}
#Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
}
and as I have already said that in the current implementation we will not be able to perform a partial update in any way. That is, it is impossible to send an update of only one field in a json line; all fields will be updated, and in null (excepted passed).
The solution to this problem is that you need to perform the conversion from string json to the entity in manual. That is, do not use all the magic from Spring Boot (which is very sad).
I will also give an example of how this can be implemented using merge at the json level:
#RestController
#RequestMapping(EmployeeRawJsonController.PATH)
public class EmployeeRawJsonController {
public final static String PATH = "/raw-json-employees";
#Autowired
private EmployeeRawJsonService service;
#PatchMapping("/{id}")
public Employee update(#RequestBody String json, #PathVariable Long id) {
return service.update(id, json);
}
}
#Service
public class EmployeeRawJsonService {
#Autowired
private EmployeeRepository employeeRepository;
public Employee update(Long id, String json) {
Optional<Employee> optionalEmployee = employeeRepository.findById(id);
return optionalEmployee
.map(e -> getUpdatedFromJson(e, json))
.orElseThrow(RuntimeException::new);
}
private Employee getUpdatedFromJson(Employee employee, String json) {
Long id = employee.getId();
updateFromJson(employee, json);
employee.setId(id);
return employeeRepository.save(employee);
}
private void updateFromJson(Employee employee, String json) {
try {
new ObjectMapper().readerForUpdating(employee).readValue(json);
} catch (IOException e) {
throw new RuntimeException("Cannot update from json", e);
}
}
}
#Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
}
With this solution, we eliminate the problem associated with the partial update.
But here another problem arises, that we are losing the automatic addition of validation of beans.
That is, in the first case, validation is enough to add one annotation #Valid:
#PatchMapping("/{id}")
public Employee update(#RequestBody #Valid Employee employee, #PathVariable Long id) {
return service.update(id, employee);
}
But we can't do the same when we perform manual deserialization.
My question is, is there any way to enable automatic validation for the second case?
Or maybe there are other solutions that allow you to use Spring Boot magic for Bean Validation.
What you need is not the normal validation , which can achieved through manual validator call.Let’s now go the manual route and set things up programmatically:
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<User>> violations = validator.validate(object);
for (ConstraintViolation<User> violation : violations) {
log.error(violation.getMessage());
}
To validate a bean, we must first have a Validator object, which is constructed using a ValidatorFactory.
Normal validations on Spring Controllers specified with #Valid annotations are triggered automatically during the DataBinding phase when a request is served.All validators registered with the DataBinder will be executed at that stage. We can't do that for your case, so you can manually trigger the validation like above.
struggling long time with that issue and I have the weird feeling it has something to do on how I am setting my #Transactional annotation.
So what do I want to do?
I am preparing some data and save them with the available repositories in the database.
This can be found here in my FormTest class in the prepareExampleApplication method
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#ActiveProfiles("test")
#DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class FormTest {
#Autowired
private ApplicationRepository applicationRepository;
#Autowired
private AppActionRepository appActionRepository;
#Autowired
private RoleRepository roleRepository;
#Autowired
private TestRestTemplate restTemplate;
#Autowired
private FormRepository formRepository;
#Autowired
private FormElementRepository formElementRepository;
#Autowired
private SectionRepository sectionRepository;
private Application application;
private InputField refInputField;
private Select refSelectBox;
#Before
public void prepareExampleApplication() {
Form form = formRepository.save(ModelFactory.getForm("Project"));
Application application = ModelFactory.getApplication("Example", form);
this.application = applicationRepository.save(application);
Role role = new Role();
role.setRoleName("ADMIN");
role.setApp(application);
role = roleRepository.save(role);
Section section = ModelFactory.getSection(form, null, null);
section = formElementRepository.save(section);
InputField inputField = ModelFactory.getInputField(form, section, section);
refInputField = formElementRepository.save(inputField);
//once again. Just for my own eyes to see if it is there
Iterable<Form> all = formRepository.findAll();
// lot more stuff
}
#Test
#Transactional
public void testUserInput() {
// first create a new container to give inouts
Long id = this.application.getEntity().getId();
// for the sake of debugging I am using the formRepo to really SEARCH for the persisted form AND IT IS THERE!!!
Form byId = formRepository.findById(id).orElseThrow(NotFoundException::new);
URI uri = this.restTemplate.postForLocation("/api/form/" + id + "/forminstance", null);
long containerId = TestHelper.extractId(uri);
}
}
The next thing I am doing having this data is to use the restTemplate and dispatch a post request to a REST service. You can find the POST call in the test method. For debugging reasons - and to see that the repo is working - I am REALLY using the repository to get the id of the form instead of using the class field that had been filled with the preparation method. AND the form will be returned!!
Within my rest service I am using the formRepository once again, looking for the entity I have found before in the test class. But THIS TIME the repository does not return anything. Only null. I have tried SO MANY different things with setting #Transactional on different locations, but whatever I do the formRepository within the REST service does only give me back 0 entites and a null. Here is the REST service
#Service
#Api("FormService")
#Path("/form")
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
public class FormService {
#Autowired
private FormInstanceRepository formInstanceRepository;
#Autowired
private FormRepository formRepository;
#Autowired
private FormElementRepository formElementRepository;
#Autowired
private UserInputRepository userInputRepository;
#POST
#Path("{id}/forminstance")
#Transactional
public Response createFormInstance(#Context UriInfo info, #PathParam("id") long formId) {
// returns ALWAYS 0 elements
Iterable<Form> all = formRepository.findAll();
// returns always null
Form form = formRepository.findById(formId).orElse(null);
FormInstance formInstance = new FormInstance();
formInstance.setForm(form);
FormInstance save = formInstanceRepository.save(formInstance);
UriBuilder builder = info.getAbsolutePathBuilder();
builder.path(Long.toString(save.getId()));
return Response.created(builder.build()).build();
}
IF you know the answer I am really interested in the explanation to understand my error. I am using an in-memory H2 db for the tests.
Adding the Form entity and FormRepository, too
#Entity
#Data
public class Form {
#Id
#GeneratedValue
private Long id;
private String name;
}
-
public interface FormRepository extends CrudRepository<Form, Long> {
}
Thanks in advance for your help!!
I am building project on spring boot and want to add validation that are easy to integrate.
I have Pojo for my project as below:
public class Employee{
#JsonProperty("employeeInfo")
private EmployeeInfo employeeInfo;
}
EmployeeInfo class is as below:
public class EmployeeInfo extends Info {
#JsonProperty("empName")
private String employeeName;
}
Info class is as below:
#JsonIgnoreProperties(ignoreUnknown = true)
public class Info {
#JsonProperty("requestId")
protected String requestId;
}
How to I validate if request Id is not blank with javax.validation
My controller class is as below:
#RequestMapping(value = "/employee/getinfo", method = RequestMethod.GET, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<> getEmployee(#RequestBody Employee employee) {
//need to validate input request here
//for e.g to check if requestId is not blank
}
Request :
{
"employeeInfo": {
"requestId": "",
}
}
Considering you are making use of validation-api:
Please try using below to validate if your String is not null or not containing any whitespace
#NotBlank
In order to validate request parameters in controller methods, you can either use builtin validators or custom one(where you can add any type of validations with custom messages.)
Details on how to use custom validations in spring controller, Check how to validate request parameters with validator like given below:
#Component
public class YourValidator implements Validator {
#Override
public boolean supports(Class<?> clazz) {
return clazz.isAssignableFrom(Employee.class);
}
#Override
public void validate(Object target, Errors errors) {
if (target instanceof Employee) {
Employee req = (Employee) target;
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "employeeInfo.requestId", "YourCustomErrorCode", "yourCustomErrorMessage");
//Or above validation can also be done as
if(req.getEmployeeInfo().getRequestId == null){
errors.rejectValue("employeeInfo.requestId", "YourCustomErrorCode", "YourCustomErrorMessage");
}
}
}
}
I have the following method on a service class:
#Service
public class Service {
(...)
public Page<ChannelAccount> getByCustomerAndChannelType(Pageable pageable, Customer customer, ChannelType channelType) {
return channelAccountRepository.findByCustomerAndChannelType(pageable, customer, channelType);
}
}
This returns the expected result. Now I trying to build the unit test for it. So far I got this:
#RunWith(MockitoJUnitRunner.class)
public class ChannelAccountServiceTest {
#InjectMocks
private ChannelAccountService channelAccountService;
#Mock
private ChannelAccountRepository channelAccountRepository;
(...)
#Test
public void testGetByCustomerAndChannelTypePageable() {
Page<ChannelAccount> pageResult = new PageImpl<>(channelAccountService.getAllChannelAccounts());
Mockito.when(channelAccountRepository.findByCustomerAndChannelType(pageable, customer, ChannelType.FACEBOOK)).thenReturn(pageResult);
Page<ChannelAccount> channelAccountPage = channelAccountRepository.findByCustomerAndChannelType(pageable, customer, ChannelType.FACEBOOK);
assertEquals(pageResult, channelAccountPage);
}
Somehow this doesn't feels right. What am I missing here?
Not sure why you are calling this method as it has nothing to do with the case itself:
Page<ChannelAccount> pageResult = new PageImpl<>(channelAccountService.getAllChannelAccounts());
I would do the following in the test:
Pageable pageableStub = Mockito.mock(Pageable.class);
Page pageStub = Mockito.mock(Page.class);
Mockito.when(channelAccountRepository
.findByCustomerAndChannelType(pageableStub, customer, ChannelType.FACEBOOK))
.thenReturn(pageStub);
Page<ChannelAccount> channelAccountPage = channelAccountService
.findByCustomerAndChannelType(pageableStub, customer, ChannelType.FACEBOOK);
assertTrue(pageResult == channelAccountPage);
I would check whether the objects are the same instances instead of equals (even more strict).
I have controller:
#Controller
public class EventMenuController{
#RequestMapping(value = "/updateEvent", method = RequestMethod.POST)
public String updateEvent(Model model,
#Valid #ModelAttribute("existedEvent") Event event,
BindingResult result,
#ModelAttribute("linkedCandidates") Set<Candidate> candidates,
#ModelAttribute("linkedvacancies") Set<Vacancy> vacancies,
#RequestParam(required = true, value = "selectedEventStatusId")Integer EventStatusId,
#RequestParam(required = true, value = "selectedEventTypeId")Integer EventTypeId ,
RedirectAttributes attributes) {
if (result.hasErrors()) {
//model.addAttribute("idEvent", event.getId());
event.setCandidates(candidates);
event.setVacancies(vacancies);
return "eventDetails";
}
eventService.updateEventAndLinkedEntities(event, candidates, vacancies ,EventTypeId,EventStatusId);
attributes.addAttribute("idEvent",event.getId() );//event is null therefore NPE here
attributes.addAttribute("message", "submitted correctly at "+new Date());
return "redirect:eventDetails";
}
}
For testing this method I wrote following class:
#ContextConfiguration(locations = { "classpath:/test/BeanConfigUI.xml" })
#RunWith(SpringJUnit4ClassRunner.class)
#WebAppConfiguration
public class EventMenuControllerTest {
#Test
public void updateEvent() throws Exception{
MockHttpServletRequestBuilder request = MockMvcRequestBuilders
.post("/updateEvent");
request.param("selectedEventStatusId", "1");
request.param("selectedEventTypeId", "1");
EventMenuController eventMenuController = (EventMenuController) wac.getBean("eventMenuController");
EventService mockEventService = Mockito.mock(EventService.class);
eventMenuController.eventService = mockEventService;
Mockito.doNothing().when(mockEventService).updateEventAndLinkedEntities(any(Event.class), any(Set.class),any(Set.class), any(Integer.class), any(Integer.class));
ResultActions result = mockMvc.perform(request);
result.andExpect(MockMvcResultMatchers.view().name("redirect:eventDetails"));
result.andExpect(MockMvcResultMatchers.model().attributeExists("idEvent"));
result.andExpect(MockMvcResultMatchers.model().attributeExists("message"));
}
}
In the process request executing on server side I see error that show me that event object is null.
Question:
What request I must write that pass event to server-side(controller method) using MockMvc ?
The Event class object is not initialized. You can either create an Event object or create a mock of an Event object depending on your test case and send it to the EventMenuController class object. You can do this similar to how you have sent a mock object of EventService to EventMenuController.
It is better practice to use fields as part of a class and not as part of a method. This will give you flexibility to mock any field.