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.
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With