@SqlMergeMode - Spring Testing Annotation
In this tutorial, we will learn about the @SqlMergeMode annotation, and at the end, we will see how the Spring Boot application can utilize the @SqlMergeMode annotation for testing. Therefore, read the post till the end.
About @SqlMergeMode
The @SqlMergeMode is a part of the org.springframework.test.context.jdbc package. It can be applied at the test class level or the test method level.
@Sql declarations at the method level take precedence over those at the class level. But we can change this behavior by configuring @SqlMergeMode.
Two type of merge mode are there:
- MERGE
- OVERRIDE
The OVERRIDE merge mode will be used by default. With the OVERRIDE mode, method-level @Sql declarations will effectively override class-level @Sql declarations.
When we use the @Sql annotation at the class level and at the method level, too, it would be better to use the @SqlMergeMode.
For example, suppose we have create-schema.sql:
create-schema.sql
CREATE TABLE student (
id SERIAL PRIMARY KEY,
name VARCHAR(250) NOT NULL,
email VARCHAR(250) NOT NULL,
age INT
);
In the following example, if we remove the @SqlMergeMode(MERGE) declaration then the test will fail because table student will not be created in the database. Beacuse @Sql declarations at the method level take precedence over those at the class level.
@Sql({"/create-schema.sql"})
@SqlMergeMode(MERGE)
@DataJpaTest
public class StudentRepositoryTests {
@Autowired
private StudentRepository studentRepository;
@Test
@Sql({"/test-student-data.sql"})
void findByName_ReturnsTheStudent_1() {
Student student = studentRepository.findByName("Alpha").get();
assertThat(student).isNotNull();
assertThat(student.getEmail()).isEqualTo("alpha@knf.com");
assertThat(student.getName()).isEqualTo("Alpha");
assertThat(student.getId()).isEqualTo(101);
assertThat(student.getAge()).isEqualTo(50);
}
}
Complete Example with @SqlMergeMode & @Sql
Next we will create a spring boot JPA application, create repository layer which contains three query methods and finally we will do 'slice' testing with help of @DataJpaTest, @Sql, and @SqlMergeMode annotation to verify our system is working as expected.
@DataJpaTest Examples are available in our Blog:
- Testing a JPA application With @DataJpaTest
- Testing a JPA application with @DataJpaTest and Testcontainers
- @Sql - Spring Testing Annotation
Creating a spring boot application
First, open the Spring initializr https://start.spring.io
Then, Provide the Group and Artifact name. We have provided Group name com.knf.dev.demo and Artifact sqlmergemode-annotation-example. Here I selected the Maven project - language Java 17 - Spring Boot 3.2.5, Spring Data JPA, and PostgreSQL Driver.
Final Project Directory
Complete 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>3.2.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.knf.dev.demo</groupId>
<artifactId>sqlmergemode-annotation-example</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>sqlmergemode-annotation-example</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
spring-boot-starter-test starter will provide following libraries:
- JUnit
- Spring Test & Spring Boot Test
- AssertJ
- Hamcrest
- Mockito
- JSONassert
- JsonPath
application.yaml
#PostgreSQL configuration
spring:
datasource:
url: jdbc:postgresql://localhost:5432/postgres
username: postgres
password: root
jpa:
hibernate:
show-sql: true
open-in-view: false
Let's create the scripts for testing our application.
Create create-schema.sql
drop table if exists student;
CREATE TABLE student (
id SERIAL PRIMARY KEY,
name VARCHAR(250) NOT NULL,
email VARCHAR(250) NOT NULL,
age INT
);
Create test-student-data.sql
INSERT INTO student (id,name, email, age) VALUES
(101,'Alpha', 'alpha@knf.com', 50),
(102,'Beta', 'beta@knf.com', 40),
(103,'Gama', 'gama@knf.com', 30),
(104,'Pekka', 'pekka@knf.com', 20);
Create Student Entity
A Student object as JPA entity. I am not a big fan of Project Lombok; therefore, getters and setters are explicitly added.
package com.knf.dev.demo.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "student")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private Integer age;
public Student() {
}
public Student(String name, String email, Integer age) {
this.name = name;
this.email = email;
this.age = age;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
The @Entity annotation specifies that the class is an entity and is mapped to a database table.
The @Table annotation specifies the name of the database table to be used for mapping.
The @Id annotation specifies the primary key of an entity and the @GeneratedValue provides for the specification of generation strategies for the values of primary keys.
Create Student Repository
package com.knf.dev.demo.repository;
import com.knf.dev.demo.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;
public interface StudentRepository extends JpaRepository<Student,Long> {
//Using JPQL query
@Query("FROM Student WHERE age > ?1")
List<Student> findStudentByAgeGreaterThan(Integer age);
//Using native sql query
@Query(value = "select * from student as u where u.name = :name",
nativeQuery = true)
Optional<Student> findByName(@Param("name") String name);
//Derived Query Method
List<Student> findByAgeLessThan(Integer age);
}
- JpaRepository is a JPA-specific extension of Repository. It contains an API for basic CRUD operations and also API for pagination and sorting.
- findStudentByAgeGreaterThan(): If we want to retrieve students whose age is greater than the given age.
- findByName(): This method will get student entity by name.(Optional)
- findByAgeLessThan(): If we want to retrieve students whose age is less than the given age.
Driver class
package com.knf.dev.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SqlmergemodeAnnotationExampleApplication {
public static void main(String[] args) {
SpringApplication.run(SqlmergemodeAnnotationExampleApplication.class, args);
}
}
Implementing the Tests
When using JUnit 4, @SpringBootTest annotation should be used in combination with @RunWith(SpringRunner.class). But for this example we are using JUnit 5, there’s no need to add the equivalent @ExtendWith(SpringExtension.class).
package com.knf.dev.demo;
import com.knf.dev.demo.entity.Student;
import com.knf.dev.demo.repository.StudentRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.SqlMergeMode;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
import static org.springframework.test.
context.jdbc.SqlMergeMode.MergeMode.MERGE;
@Sql({"/create-schema.sql"})
@SqlMergeMode(MERGE)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class StudentRepositoryTests {
@Autowired
private StudentRepository studentRepository;
@Test
@Sql({"/test-student-data.sql"})
void findByName_ReturnsTheStudent_1() {
Student student = studentRepository.findByName("Alpha").get();
assertThat(student).isNotNull();
assertThat(student.getEmail()).isEqualTo("alpha@knf.com");
assertThat(student.getName()).isEqualTo("Alpha");
assertThat(student.getId()).isEqualTo(101);
assertThat(student.getAge()).isEqualTo(50);
}
@Test
@Sql({"/test-student-data.sql"})
void findByAgeGreaterThan_ReturnsTheListStudents_1() {
List<Student> students = studentRepository
.findStudentByAgeGreaterThan(29);
//Convert list of students to list of id(Integer)
List<Integer> ids = students.stream()
.map(o -> o.getId().intValue())
.collect(Collectors.toList());
assertThat(students.size()).isEqualTo(3);
assertThat(ids).hasSameElementsAs(Arrays.asList(103, 102, 101));
}
@Test
@Sql({"/test-student-data.sql"})
void findByAgeLessThan_ReturnsTheListStudents() {
List<Student> students = studentRepository
.findByAgeLessThan(31);
//Convert list of students to list of id(Integer)
List<Integer> ids = students.stream()
.map(o -> o.getId().intValue())
.collect(Collectors.toList());
assertThat(students.size()).isEqualTo(2);
assertThat(ids).hasSameElementsAs(Arrays.asList(104, 103));
}
}
- We run tests against a real database; for that, configure AutoConfigureTestDatabase.Replace.NONE.
- The @Sql annotation executes SQL scripts and SQL statements using datasource for testing.
- assertThat is used to check the specified value matches the expected value. It will accept the two parameters, the first contains the actual value, and the second will have the object matching the condition.
Run the test
Or you can run the test using following command:
mvn test -Dtest=StudentRepositoryTests