Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 531
PacificNW_Lover Avatar asked Nov 28 '22 21:11

PacificNW_Lover


2 Answers

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

like image 83
Josef Cech Avatar answered Dec 04 '22 03:12

Josef Cech


From doc

If you structure your code in a sensible way, your @SpringBootApplication class is used by default as the configuration of your tests.

So, A recommended approach is to move that area-specific configuration to a separate @Configuration class at the same level as your application

@Configuration
@EnableJpaAuditing
public class ApplicationSpecificConfig {
   ...
}

And a recommendation from doc is disable the default one for test.

You can create a @SpringBootConfiguration somewhere in the hierarchy of your test so that it is used instead. Alternatively, you can specify a source for your test, which disables the behavior of finding a default one.

like image 23
Eklavya Avatar answered Dec 04 '22 02:12

Eklavya