问题描述
我正在使用Spring Boot,JUnit 4&在基于maven的项目中,模拟Mockito来测试我的Spring Boot Microservice REST API.
Am using Spring Boot, JUnit 4 & Mockito in a maven based project to tests my Spring Boot Microservice REST API.
因此,在启动时,DataInserter类从owner.json和cars.json加载数据.
So, on startup, the DataInserter class loads data from owner.json and cars.json.
通常,通过我的REST调用,一切正常,但是似乎在设置单元测试和集成测试时出现了问题.
Normally, via my REST calls, everything works, but it seems I have something wrong with setting up unit tests and integration tests.
项目结构:
myapi
│
├── pom.xml
│
├── src
├── main
│ │
│ ├── java
│ │ │
│ │ └── com
│ │ │
│ │ └── myapi
│ │ │
│ │ ├── MyApplication.java
│ │ │
│ │ ├── bootstrap
│ │ │ │
│ │ │ └── DataInserter.java
│ │ │
│ │ ├── controllers
│ │ │ │
│ │ │ ├── OwnerController.java
│ │ │ │
│ │ │ └── CarController.java
│ │ │
│ │ ├── exceptions
│ │ │ │
│ │ │ └── OwnerNotFoundException.java
│ │ │
│ │ ├── model
│ │ │ │
│ │ │ ├── AuditModel.java
│ │ │ │
│ │ │ ├── Car.java
│ │ │ │
│ │ │ └── Owner.java
│ │ │
│ │ ├── repository
│ │ │ │
│ │ │ ├── OwnerRepository.java
│ │ │ │
│ │ │ └── CarRepository.java
│ │ │
│ │ └── service
│ │ │
│ │ ├── OwnerService.java
│ │ │
│ │ ├── OwnerServiceImpl.java
│ │ │
│ │ ├── CarService.java
│ │ │
│ │ └── CarServiceImpl.java
│ └── resources
│ │
│ ├── application.properties
│ │
│ ├── data
│ │ │
│ │ ├── cars.json
│ │ │
│ │ └── owners.json
│ │
│ └── logback.xml
└── test
│
├── java
│ │
│ └── com
│ │
│ └── myapi
│ │
│ ├── MyApplicationTests.java
│ │
│ └── service
│ │ │
│ │ │
│ │ └── OwnerControllerTest.java
│ │
│ │
│ └── controllers
│ │
│ │
│ └── OwnerControllerIntegrationTest.java
└── resources
│
├── application.properties
│
├── data
│ │
│ ├── cars.json
│ │
│ └── owners.json
│
└── logback.xml
pom.xml:
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.myapi</groupId>
<artifactId>car-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>car-api</name>
<description>Car REST API</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@Component
public class DataInserter implements ApplicationListener<ContextRefreshedEvent> {
@Value("classpath:data/owners.json")
Resource ownersResource;
@Value("classpath:data/cars.json")
Resource carsResource;
@Autowired
private OwnerService ownerService;
@Autowired
private CarsService carService;
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
List<Owner> populatedOwners = new ArrayList<>();
try {
Owner aOwner;
File ownersFile = ownersResource.getFile();
File carsFile = carsResource.getFile();
String ownersString = new String(Files.readAllBytes(ownersFile.toPath()));
String carsString = new String(Files.readAllBytes(carsFile.toPath()));
ObjectMapper mapper = new ObjectMapper();
List<Owner> owners = Arrays.asList(mapper.readValue(ownersString, Owner[].class));
List<ElectricCars> cars = Arrays.asList(mapper.readValue(carsString, ElectricCars[].class));
// Populate owners one by one
for (Owner owner : owners) {
aOwner = new Owner(owner.getName(), owner.getAddress(), owner.getCity(), owner.getState(), owner.getZipCode());
ownerService.createOwner(aOwner);
populatedOwners.add(aOwner);
}
// Populate owner cars one by one
for (int i = 0; i < populatedOwners.size(); i++) {
carService.createCars(populatedOwners.get(i).getId(), cars.get(i));
}
}
catch(IOException ioe) {
ioe.printStackTrace();;
}
}
}
src/main/resources/data/cars.json:
src/main/resources/data/cars.json:
[
{
"make": "Honda",
"model": "Accord",
"year": "2020"
},
{
"make": "Nissan",
"model": "Maxima",
"year": "2019"
},
{
"make": "Toyota",
"model": "Prius",
"year": "2015"
},
{
"make": "Porsche",
"model": "911",
"year": "2017"
},
{
"make": "Hyundai",
"model": "Elantra",
"year": "2018"
},
{
"make": "Volkswagen",
"model": "Beatle",
"year": "1973"
},
{
"make": "Ford",
"model": "F-150",
"year": "2010"
},
{
"make": "Chevrolet",
"model": "Silverado",
"year": "2020"
},
{
"make": "Toyota",
"model": "Camary",
"year": "2018"
},
{
"make": "Alfa",
"model": "Romeo",
"year": "2017"
}
]
src/main/resources/data/owners.json:
src/main/resources/data/owners.json:
[
{
"name": "Tom Brady"
"address": "123 Amherst Place",
"city": "Boston",
"state": "MA",
"zipCode": 53211
},
{
"name": "Kobe Bryant"
},
{
"name": "Mike Tyson"
},
{
"name": "Scottie Pippen"
},
{
"name": "John Madden"
},
{
"name": "Arnold Palmer"
},
{
"name": "Tiger Woods"
},
{
"name": "Magic Johnson"
},
{
"name": "George Foreman"
},
{
"name": "Charles Barkley"
}
]
src/main/resources/applications.properties:
src/main/resources/applications.properties:
server.servlet.context-path=/car-api
server.port=8080
server.error.whitelabel.enabled=false
# Database specific
spring.jpa.hibernate.ddl-auto=create
spring.datasource.url=jdbc:mysql://localhost:3306/car_db?useSSL=false
spring.datasource.ownername=root
spring.datasource.password=
AuditModel:
AuditModel:
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@JsonIgnoreProperties(
value = {"createdAt", "updatedAt"},
allowGetters = true
)
public abstract class AuditModel implements Serializable {
@ApiModelProperty(hidden = true)
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "created_at", nullable = false, updatable = false)
@CreatedDate
private Date createdAt;
@ApiModelProperty(hidden = true)
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "updated_at", nullable = false)
@LastModifiedDate
private Date updatedAt;
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
public Date getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Date updatedAt) {
this.updatedAt = updatedAt;
}
}
MyApplication.java
MyApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
所有者实体:
Owner entity:
@Entity
@Table(name = "owner")
public class Owner extends AuditModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
private String name;
private String address,
private String city;
private String state;
private int zipCode;
@OneToMany(cascade = CascadeType.ALL,
fetch = FetchType.EAGER,
mappedBy = "owner")
private List<Car> cars = new ArrayList<>();
public Owner() {
}
// Getter & Setters omitted for brevity.
}
汽车实体:
@Entity
@Table(name="car")
public class Car extends AuditModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
String make;
String model;
String year;
@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "owner_id", nullable = false)
private Owner owner;
// Getter & Setters omitted for brevity.
}
OwnerRepository:
OwnerRepository:
@Repository
public interface OwnerRepository extends JpaRepository<Owner, Long> {
@Query(value = "SELECT * FROM owner WHERE name = ?", nativeQuery = true)
Owner findOwnerByName(String name);
}
CarRepository:
CarRepository:
@Repository
public interface CarRepository extends JpaRepository<Car, Long> {
}
OwnerService:
OwnerService:
public interface OwnerService {
boolean createOwner(Owner owner);
Owner getOwnerByOwnerId(Long ownerId);
List<Owner> getAllOwners();
}
OwnerServiceImpl:
OwnerServiceImpl:
@Service
public class OwnerServiceImpl implements OwnerService {
@Autowired
OwnerRepository ownerRepository;
@Autowired
CarRepository carRepository;
@Override
public List<Owner> getAllOwners() {
return ownerRepository.findAll();
}
@Override
public boolean createOwner(Owner owner) {
boolean created = false;
if (owner != null) {
ownerRepository.save(owner);
created = true;
}
return created;
}
@Override
public Owner getOwnerByOwnerId(Long ownerId) {
Optional<Owner> owner = null;
if (ownerRepository.existsById(ownerId)) {
owner = ownerRepository.findById(ownerId);
}
return owner.get();
}
}
CarService:
CarService:
public interface CarService {
boolean createCar(Long ownerId, Car car);
}
CarServiceImpl:
CarServiceImpl:
@Service
public class CarServiceImpl implements CarService {
@Autowired
OwnerRepository ownerRepository;
@Autowired
CarRepository carRepository;
@Override
public boolean createCar(Long ownerId, Car car) {
boolean created = false;
if (ownerRepository.existsById(ownerId)) {
Optional<Owner> owner = ownerRepository.findById(ownerId);
if (owner != null) {
List<Car> cars = owner.get().getCars();
cars.add(car);
owner.get().setCars(cars);
car.setOwner(owner.get());
carRepository.save(car);
created = true;
}
}
return created;
}
}
OwnerController:
OwnerController:
@RestController
public class OwnerController {
private HttpHeaders headers = null;
@Autowired
OwnerService ownerService;
public OwnerController() {
headers = new HttpHeaders();
headers.add("Content-Type", "application/json");
}
@RequestMapping(value = { "/owners" }, method = RequestMethod.POST, produces = "APPLICATION/JSON")
public ResponseEntity<Object> createOwner(@Valid @RequestBody Owner owner) {
boolean isCreated = ownerService.createOwner(owner);
if (isCreated) {
return new ResponseEntity<Object>(headers, HttpStatus.OK);
}
else {
return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
}
}
@RequestMapping(value = { "/owners" }, method = RequestMethod.GET, produces = "APPLICATION/JSON")
public ResponseEntity<Object> getAllOwners() {
List<Owner> owners = ownerService.getAllOwners();
if (owners.isEmpty()) {
return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<Object>(owners, headers, HttpStatus.OK);
}
@RequestMapping(value = { "/owners/{ownerId}" }, method = RequestMethod.GET, produces = "APPLICATION/JSON")
public ResponseEntity<Object> getOwnerByOwnerId(@PathVariable Long ownerId) {
if (null == ownerId || "".equals(ownerId)) {
return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
}
Owner owner = ownerService.getOwnerByOwnerId(ownerId);
return new ResponseEntity<Object>(owner, headers, HttpStatus.OK);
}
}
CarController:
CarController:
@RestController
public class CarController {
private HttpHeaders headers = null;
@Autowired
CarService carService;
public CarController() {
headers = new HttpHeaders();
headers.add("Content-Type", "application/json");
}
@RequestMapping(value = { "/cars/{ownerId}" }, method = RequestMethod.POST, produces = "APPLICATION/JSON")
public ResponseEntity<Object> createCarBasedOnOwnerId(@Valid @RequestBody Car car, Long ownerId) {
boolean isCreated = carService.createCar(ownerId, car);
if (isCreated) {
return new ResponseEntity<Object>(headers, HttpStatus.OK);
}
else {
return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
}
}
MyApplicationTests:
MyApplicationTests:
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class MyApplicationTests {
@Test
void contextLoads() {
}
}
OwnerControllerTest:
OwnerControllerTest:
@RunWith(SpringRunner.class)
@WebMvcTest(OwnerControllerTest.class)
@TestPropertySource(locations="classpath:application.properties")
public class OwnerControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean
private OwnerService ownerService;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
public void givenEndPointNotFoundThenReturn404() throws Exception {
Owner owner = new Owner("Tom Brady", "123 Amherst Place", "Boston", "MA", 53211);
Mockito.when(ownerService.getOwnerByOwnerId(1L)).thenReturn(null);
ResultActions resultActions = mockMvc.perform(
MockMvcRequestBuilders.get("/car-api/owners/0"));
resultActions.andExpect(status().is4xxClientError());
}
}
运行mvn clean install
时,出现以下错误(位于target/sure-fire-reports/com.myapi.service.OwnerControllerTest.txt
内部):
When I run mvn clean install
, I get the following error (located inside target/sure-fire-reports/com.myapi.service.OwnerControllerTest.txt
):
-------------------------------------------------------------------------------
Test set: com.myapi.service.OwnerControllerTest
-------------------------------------------------------------------------------
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.32 s <<< FAILURE! - in com.myapi.service.OwnerControllerTest
givenEndPointNotFoundThenReturn404 Time elapsed: 0 s <<< ERROR!
java.lang.IllegalStateException: Failed to load ApplicationContext
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jpaAuditingHandler': Cannot resolve reference to bean 'jpaMappingContext' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jpaMappingContext': Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: JPA metamodel must not be empty!
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jpaMappingContext': Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: JPA metamodel must not be empty!
Caused by: java.lang.IllegalArgumentException: JPA metamodel must not be empty!
在没有此测试用例的情况下,DataInserter会填充数据库,并且我能够执行所有REST调用并获取所有适当的JSON有效负载.
Without this test case, the DataInserter populates the database and I am able to do all REST calls and get all the appropriate JSON payloads.
推荐答案
您需要将@EnableJpaAuditing
注释移至单独的@Configuration
类,否则即使对于不相关的应用程序片也将加载该注释.
You need to move @EnableJpaAuditing
annotation to a separate @Configuration
class otherwise it will be loaded even for unrelated application slices.
@Configuration
@EnableJpaAuditing
public class JpaAuditingConfiguration {}
@SpringBootApplication
类用作所有带有切片的测试的默认配置,因此粘贴到它的任何其他配置将影响比您期望的更多测试.
@SpringBootApplication
class is used as default configuration for all tests with slices so any additional configuration glued to it will affect more tests than you would expect.
这在文档中也有很好的解释:用户配置和切片
This is also well explained in documentation: User Configuration and Slicing
这篇关于Spring Boot JPA元模型不能为空!尝试运行JUnit/集成测试时的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!