Spring Boot JPA metamodel must not be empty! when trying to run JUnit / Integration Tests

PacificNW_Lover :

Am using Spring Boot, JUnit 4 & Mockito in a maven based project to tests my Spring Boot Microservice REST API.

So, on startup, the DataInserter class loads data from owner.json and cars.json.

Normally, via my REST calls, everything works, but it seems I have something wrong with setting up unit tests and integration tests.

Project structure:

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=

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

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

Car entity:

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

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

CarRepository:

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

OwnerService:

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

CarService:

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

}


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:

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

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

@SpringBootTest
class MyApplicationTests {

    @Test
    void contextLoads() {
    }

}

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

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!

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.

Josef :

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 {}

Documentation: User Configuration and Slicing

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=183153&siteId=1