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

Before moving on, read our previous tutorial about @Sql(click here) annotationWithin the Spring testing context, SQL scripts and statements are executed against a database using the Spring @Sql annotation.

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:

  1. MERGE
  2. 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. 

The method-level @SqlMergeMode declaration overrides a class-level declaration.

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: 


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.5Spring 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

We are writing test against real(Postgresql) database. 
So, Let’s configure Spring Boot to use PostgreSQL as our data source. We are simply adding PostgreSQL database URL, username, and password in the src/main/resources/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

It doesn't make sense to test inherited default methods like save(), findById(), deleteById(), or findAll() from JpaRepository. If we are doing like so, means we are testing the framework.

So, i created three methods findStudentByAgeGreaterThan()findByName(), and findByAgeLessThan()
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

Popular posts from this blog

Learn Java 8 streams with an example - print odd/even numbers from Array and List

Java Stream API - How to convert List of objects to another List of objects using Java streams?

Registration and Login with Spring Boot + Spring Security + Thymeleaf

Java, Spring Boot Mini Project - Library Management System - Download

ReactJS, Spring Boot JWT Authentication Example

Top 5 Java ORM tools - 2024

Java - Blowfish Encryption and decryption Example

Spring boot video streaming example-HTML5

Google Cloud Storage + Spring Boot - File Upload, Download, and Delete