Spring Boot JPA 元模型不能为空!尝试运行 JUnit / 集成测试时

Posted

技术标签:

【中文标题】Spring Boot JPA 元模型不能为空!尝试运行 JUnit / 集成测试时【英文标题】:Spring Boot JPA metamodel must not be empty! when trying to run JUnit / Integration Tests 【发布时间】:2020-06-21 17:09:59 【问题描述】:

我在一个基于 maven 的项目中使用 Spring Boot、JUnit 4 和 Mockito 来测试我的 Spring Boot 微服务 REST API。

因此,在启动时,DataInserter 类会从 owner.json 和 cars.json 加载数据。

通常,通过我的 REST 调用,一切正常,但我在设置单元测试和集成测试时似乎有问题。

项目结构:

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:

<?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:

[
  
      "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:

[
  
    "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:

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=

审计模型:

@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

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);
    



所有者实体:

@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.


所有者存储库:

@Repository
public interface OwnerRepository extends JpaRepository<Owner, Long> 
    @Query(value = "SELECT * FROM owner WHERE name = ?", nativeQuery = true)
    Owner findOwnerByName(String name);


汽车存储库:

@Repository
public interface CarRepository extends JpaRepository<Car, Long> 


所有者服务:

public interface OwnerService 

    boolean createOwner(Owner owner);

    Owner getOwnerByOwnerId(Long ownerId);

    List<Owner> getAllOwners();



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();
    


汽车服务:

public interface CarService 

    boolean createCar(Long ownerId, Car car);


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;
    




所有者控制器:

@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);
    



汽车控制器:

@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);
        
    


我的应用程序测试:

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class MyApplicationTests 

    @Test
    void contextLoads() 
    



所有者控制器测试:

@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 内):

-------------------------------------------------------------------------------
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 有效负载。

【问题讨论】:

看起来测试正在加载不相关的bean(jpaMappingContext,jpaAuditingHandler)-您的主应用程序类是否有一些额外的配置?主应用类上的所有注解也对切片有效。 @Josef - 我添加了包含 main() 方法的MyApplication.java...我有@EnableJpaAuditing。有什么想法吗? 谢谢,我已经把答案贴在下面了。 【参考方案1】:

您需要将 @EnableJpaAuditing 注释移动到单独的 @Configuration 类,否则即使对于不相关的应用程序切片也会加载它。

@Configuration
@EnableJpaAuditing
public class JpaAuditingConfiguration 

@SpringBootApplication 类用作所有带有切片的测试的默认配置,因此任何附加到它的配置都会影响比您预期的更多的测试。

这在文档中也有很好的解释:User Configuration and Slicing

【讨论】:

这对我有用,显然集成测试不喜欢 @EnableJpaAuditing 注释。【参考方案2】:

来自doc

如果您以合理的方式构建代码,您的 默认使用@SpringBootApplication 类作为配置 你的测试。

因此,推荐的方法是将特定区域的配置移动到与您的应用程序处于同一级别的单独 @Configuration 类

@Configuration
@EnableJpaAuditing
public class ApplicationSpecificConfig 
   ...

并且来自 doc 的建议是禁用默认测试。

您可以在层次结构中的某处创建@SpringBootConfiguration 你的测试,以便它被使用。或者,您可以 为您的测试指定一个来源,这会禁用查找行为 一个默认的。

【讨论】:

以上是关于Spring Boot JPA 元模型不能为空!尝试运行 JUnit / 集成测试时的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot 2.0.2 必须存在至少一个 JPA 元模型

Spring Boot JPA 查询不为空

JPA OneToOne,外键在 Spring Boot 上为空

解决spring boot jpa查询,语句正确,返回为空问题

外键在一对多关系中始终为空 - Spring Boot Data with JPA

指定为非空的参数为空:无法更新 Spring Boot jpa 中的单个实体字段。导致指定为非空的参数为空