@Sql - Spring Testing Annotation

In this tutorial, we will learn about the @Sql annotation, and at the end, we will see how the Spring Boot application can utilize the @Sql annotation for testing. Therefore, read the post till the end.

About @Sql

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

Within the Spring testing context, SQL scripts and statements are executed against a database using the Spring @Sql annotation.

@Sql declarations at the method level take precedence over those at the class level. But we can change this behavior by configuring @SqlMergeMode

The annotation supports the following attributes:

  • scripts
  • statements
  • executionPhase
  • config
  • value

1. scripts

It specifies the path on which SQL scripts should run.

The following example shows how to use it:

@Sql(scripts = {"/create-schema.sql"})
public class StudentRepositoryTests {


We can specify the path of multiple SQL scripts, like below.

@Sql(scripts = {"/create-schema.sql","/test-student-data-1.sql"})
public class StudentRepositoryTests {


2. executionPhase

It determines when the SQL scripts and statements are run. Four types of executionPhase are listed below:

  • BEFORE_TEST_METHOD (Default)
  • AFTER_TEST_METHOD
  • BEFORE_TEST_CLASS
  • AFTER_TEST_CLASS

Default is BEFORE_TEST_METHOD.


The following example shows how to use the type AFTER_TEST_METHOD:

@Test
@Sql(scripts={"/clean-up-process.sql"},
executionPhase = AFTER_TEST_METHOD)
void findByName_ReturnsTheStudent_1() {

Here, the script inside clean-up-process.sql will be executed after the corresponding test method.


The following example shows how to use the type BEFORE_TEST_CLASS:

@Sql(
scripts = {"/create-schema.sql"},
executionPhase = BEFORE_TEST_CLASS
)
public class StudentRepositoryTests {

Here, the script inside create-schema.sql will be executed once per test class before any test method is run.


The following example shows how to use the type AFTER_TEST_CLASS:

@Sql(
scripts = {"/clean-up-process.sql"},
executionPhase = AFTER_TEST_CLASS
)
public class StudentRepositoryTests {

Here, the script inside clean-up-process.sql will be executed once per test class after all test methods have run.


3. statements

It sets up the execution of inline SQL statements.

The following example shows how to use it:

@Test
@Sql(statements = "INSERT INTO student (id,name, email, age) " +
"VALUES (112,'Tera', 'tera@knf.com', 100)")
void findByName_ReturnsTheStudent_2() {


4. config

We can configure the way we parse and run the SQL scripts by using the @SqlConfig annotation.  We can use it to configure a particular @Sql annotation.

The following example shows how to use it:

@Test
@Sql(scripts={"/test-student-data-1.sql"},
config = @SqlConfig(encoding = "utf-8",
transactionMode = SqlConfig.TransactionMode.ISOLATED))
void findByName_ReturnsTheStudent_1() {


5. value

It is the alias of scripts element. Although they can be used with the statements element, the scripts and value cannot be used simultaneously.


Complete Example with @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 and @Sql 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 sql-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>sql-annotation-example</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>sql-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-1.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 test-student-data-2.sql

INSERT INTO student (id,name, email, age) VALUES
(120,'Carl', 'carl@knf.com', 19),
(121,'Rick', 'rick@knf.com', 42),
(122,'Maggy', 'maggy@knf.com', 29),
(123,'Morgan', 'morgan@knf.com', 42);


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

public static void main(String[] args) {
SpringApplication.run(SqlAnnotationExampleApplication.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 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.Sql
.ExecutionPhase.BEFORE_TEST_CLASS;


@Sql(
scripts = {"/create-schema.sql"},
executionPhase = BEFORE_TEST_CLASS
)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class StudentRepositoryTests {

@Autowired
private StudentRepository studentRepository;

@Test
@Sql({"/test-student-data-1.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(statements = "INSERT INTO student (id,name, email, age) " +
"VALUES (112,'Tera', 'tera@knf.com', 100)")
void findByName_ReturnsTheStudent_2() {

Student student = studentRepository.findByName("Tera").get();
assertThat(student).isNotNull();
assertThat(student.getEmail()).isEqualTo("tera@knf.com");
assertThat(student.getName()).isEqualTo("Tera");
assertThat(student.getId()).isEqualTo(112);
assertThat(student.getAge()).isEqualTo(100);
}

@Test
@Sql({"/test-student-data-1.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-1.sql", "/test-student-data-2.sql"})
void findByAgeGreaterThan_ReturnsTheListStudents_2() {

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(5);
assertThat(ids).hasSameElementsAs(
Arrays.asList(103, 102, 101, 121, 123));
}

@Test
@Sql({"/test-student-data-1.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

Spring Boot + Mockito simple application with 100% code coverage

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