@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:
- Testing a JPA application With @DataJpaTest
- Testing a JPA application with @DataJpaTest and Testcontainers
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
#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
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