У меня есть контроллер, который получает некоторые данные формы и должен
- сохранить некоторые объекты в базу данных
- сохранить все изображения, отправленные в файловую систему
Контроллер аннотирован @Transactional
(хотя я читал, что не стоит создавать эту аннотацию на уровне контроллера ...) с rollbackFor = Exception.class
, потому что, если возникает какое-либо исключение, я хочу Изменения отката, внесенные в любые объекты.
Когда я запускаю тест и проверяю, есть ли объект, которого я ожидал бы пропустить, есть или нет, он все еще там. Таким образом, @Transactional
, похоже, не работает так, как я намеревался.
StringController. java, в src / main / java / com / example / controllers:
package com.example.controllers;
import com.example.services.DefaultImageManipulationService;
import com.example.services.ImageManipulationService;
import com.example.entities.Classified;
import com.example.entities.Place;
import com.example.inbound.ClassifiedFormData;
import com.example.repositories.ClassifiedRepository;
import com.example.repositories.PlaceRepository;
import com.example.services.StorageService;
import org.springframework.http.MediaType;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.awt.*;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
@RestController
@CrossOrigin(origins = "http://localhost:4200")
public class ClassifiedController {
private final ClassifiedRepository classifiedRepository;
private final PlaceRepository placeRepository;
private final StorageService storageService;
private final ImageManipulationService imageManipulationService;
public ClassifiedController(ClassifiedRepository classifiedRepository,
PlaceRepository placeRepository,
StorageService storageService,
DefaultImageManipulationService imageManipulationService) {
this.classifiedRepository = classifiedRepository;
this.placeRepository = placeRepository;
this.storageService = storageService;
this.imageManipulationService = imageManipulationService;
}
@Transactional(rollbackFor = Exception.class)
@PostMapping(path = "/classifieds", consumes = { MediaType.MULTIPART_FORM_DATA_VALUE })
public void addClassified(@RequestPart(name="data") ClassifiedFormData classifiedFormData,
@RequestPart(name="images") MultipartFile[] images) {
/* The end goal here is to get a classified and a place into the DB.
If anything goes wrong, the transaction should be rolled back, and any saved images and thumbnails
should be deleted. */
List<String> filePaths = null;
Path pathToImagesForThisClassified = null;
String thumbnailPath = null;
Path pathToThumbnailsForThisClassified = null;
try {
Classified classified = new Classified();
classified.setSummary(classifiedFormData.getSummary());
classified.setDescription(classifiedFormData.getDescription());
classified.setPrice(classifiedFormData.getPrice());
classified.setCurrency(classifiedFormData.getCurrency());
classifiedRepository.save(classified);
if (true) {
throw new Exception("The saved Classified should be deleted because of the @Transactional annotation");
}
String idAsStr = String.valueOf(classified.getId());
pathToImagesForThisClassified = Paths.get("images", idAsStr);
filePaths = storageService.storeAll(pathToImagesForThisClassified, images);
File thumbnail = imageManipulationService.resize(filePaths.get(classifiedFormData.getThumbnailIndex()),
new Dimension(255, 255));
pathToThumbnailsForThisClassified = Paths.get("thumbnails", idAsStr);
thumbnailPath = storageService.store(pathToThumbnailsForThisClassified, thumbnail);
classified.setImagePaths(filePaths);
classified.setThumbnailImagePath(thumbnailPath);
classifiedRepository.save(classified);
Place place = new Place(classified);
place.setCountry(classifiedFormData.getCountry());
place.setLabel(classifiedFormData.getLabel());
place.setLatitude(Double.valueOf(classifiedFormData.getLat()));
place.setLongitude(Double.valueOf(classifiedFormData.getLon()));
placeRepository.save(place);
} catch (Exception e) {
e.printStackTrace();
storageService.deleteRecursively(pathToImagesForThisClassified);
storageService.deleteRecursively(pathToThumbnailsForThisClassified);
}
}
}
классифицированных контроллеров. java в src / test / java / com / example / controllers:
package com.example.controllers;
import com.example.entities.Classified;
import com.example.entities.Place;
import com.example.inbound.ClassifiedFormData;
import com.example.repositories.ClassifiedRepository;
import com.example.repositories.PlaceRepository;
import com.example.services.StorageService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.TestConstructor;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@DisplayName("ClassifiedController")
public class ClassifiedControllerTest {
private final MockMvc mvc;
private final ClassifiedRepository classifiedRepository;
private final PlaceRepository placeRepository;
private final StorageService storageService;
public ClassifiedControllerTest(MockMvc mvc, ClassifiedRepository classifiedRepository,
PlaceRepository placeRepository, StorageService storageService) {
this.mvc = mvc;
this.classifiedRepository = classifiedRepository;
this.placeRepository = placeRepository;
this.storageService = storageService;
}
@DisplayName("Any saved entities and files are deleted if an exception is encountered")
@Test
public void givenInvalidFormData_whenPosted_thenStatus400AndClean() throws Exception {
// GIVEN
ClassifiedFormData classifiedFormData = new ClassifiedFormData();
classifiedFormData.setCountry("Spain");
classifiedFormData.setCurrency("EUR");
classifiedFormData.setSummary("Test");
classifiedFormData.setDescription("Test");
classifiedFormData.setLabel("Test");
classifiedFormData.setPrice(32.45);
classifiedFormData.setThumbnailIndex((byte)1);
classifiedFormData.setLat("42.688630");
classifiedFormData.setLon("-2.945620");
MockMultipartFile classified = new MockMultipartFile("data", "", "application/json",
("{\"summary\":\"feefwfewfew\",\"description\":\"fewfewfewfewfwe\",\"price\":\"34\"," +
"\"currency\":\"CAD\",\"thumbnailIndex\":0,\"lat\":\"52.2460367\",\"lon\":\"0.7125173\"," +
"\"label\":\"Bury St Edmunds, Suffolk, East of England, England, IP33 1BZ, United Kingdom\"," +
"\"country\":\"United Kingdom\"}").getBytes());
byte[] image1Bytes = getClass().getClassLoader().getResourceAsStream("test_image.jpg").readAllBytes();
byte[] image2Bytes = getClass().getClassLoader().getResourceAsStream("test_image.jpg").readAllBytes();
String image1Filename = "image1.jpg";
String image2Filename = "image2.jpg";
MockMultipartFile image1 =
new MockMultipartFile("images", image1Filename,"image/jpeg", image1Bytes);
MockMultipartFile image2 =
new MockMultipartFile("images", image2Filename, "image/jpeg", image2Bytes);
Path expectedImagePath = Paths.get("images", "5");
Path expectedThumbnailPath = Paths.get("thumbnails", "5");
// WHEN-THEN
mvc.perform(MockMvcRequestBuilders.multipart("/classifieds")
.file(classified)
.file(image1)
.file(image2)
.contentType(MediaType.MULTIPART_FORM_DATA_VALUE))
.andExpect(status().isOk());
Optional<Classified> classifiedOptional = classifiedRepository.findById((long)5);
assertFalse(classifiedOptional.isPresent()); // This is the assertion that is failing
Optional<Place> placeOptional = placeRepository.findByClassifiedId(5);
assertFalse(placeOptional.isPresent());
Resource image1AsResource = storageService.loadAsResource(expectedImagePath, image1Filename);
Resource image2AsResource = storageService.loadAsResource(expectedImagePath, image2Filename);
Resource thumbnailAsResource = storageService.loadAsResource(expectedThumbnailPath, "thumbnail.jpg");
assertFalse(image1AsResource.exists());
assertFalse(image2AsResource.exists());
assertFalse(thumbnailAsResource.exists());
}
}
Результат теста:
java.lang.Exception: The saved Classified should be deleted because of the @Transactional annotation
at com.example.controllers.ClassifiedController.addClassified(ClassifiedController.java:67)
at com.example.controllers.ClassifiedController$$FastClassBySpringCGLIB$$7850f537.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
[some lines omitted for brevity]
expected: <false> but was: <true>
org.opentest4j.AssertionFailedError: expected: <false> but was: <true>
at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:55)
at org.junit.jupiter.api.AssertFalse.assertFalse(AssertFalse.java:40)
at org.junit.jupiter.api.AssertFalse.assertFalse(AssertFalse.java:35)
at org.junit.jupiter.api.Assertions.assertFalse(Assertions.java:210)
at com.example.controllers.ClassifiedControllerTest.givenInvalidFormData_whenPosted_thenStatus400AndClean(ClassifiedControllerTest.java:148)