Skip to content

Commit f744bee

Browse files
committed
[FEATURE] Add programmatic assertion API
1 parent 09c4a71 commit f744bee

16 files changed

+359
-88
lines changed

.github/workflows/build.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Build Project
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
jobs:
12+
build:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- name: Checkout code
17+
uses: actions/checkout@v3
18+
19+
- name: Set up JDK
20+
uses: actions/setup-java@v3
21+
with:
22+
distribution: 'temurin'
23+
java-version: '17'
24+
cache: maven
25+
26+
- name: Build
27+
run: mvn clean install

.gitignore

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,33 @@
1-
# Default ignored files
2-
/target
1+
HELP.md
2+
target/
3+
!.mvn/wrapper/maven-wrapper.jar
4+
!**/src/main/**/target/
5+
!**/src/test/**/target/
6+
7+
### STS ###
8+
.apt_generated
9+
.classpath
10+
.factorypath
11+
.project
12+
.settings
13+
.springBeans
14+
.sts4-cache
15+
16+
### IntelliJ IDEA ###
17+
.idea
18+
*.iws
19+
*.iml
20+
*.ipr
21+
22+
### NetBeans ###
23+
/nbproject/private/
24+
/nbbuild/
25+
/dist/
26+
/nbdist/
27+
/.nb-gradle/
28+
build/
29+
!**/src/main/**/build/
30+
!**/src/test/**/build/
31+
32+
### VS Code ###
33+
.vscode/

README.md

Lines changed: 71 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
[![Language grade: Java](https://img.shields.io/lgtm/grade/java/g/Lemick/hibernate-spring-sql-query-count.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Lemick/hibernate-spring-sql-query-count/context:java)
21
# Hibernate SQL Query Assertions for Spring
32

43
Hibernate is a powerful ORM, but you need to have control over the executed SQL queries to avoid **huge performance problems** (N+1 selects, batch insert not working...)
@@ -15,7 +14,7 @@ A full-working demo of the examples below [is available here](https://github.com
1514

1615
*Tested versions*: Hibernate 5 & 6
1716

18-
### Assert SQL statements
17+
### Assert SQL statements declaratively
1918

2019
You just have to add the `@AssertHibernateSQLCount` annotation to your test and it will verify the SQL statements (SELECT, UPDATE, INSERT, DELETE) count at the end of the test :
2120

@@ -37,67 +36,93 @@ void create_two_blog_posts() {
3736
```
3837
If the actual count is different, an exception is thrown with the executed statements:
3938
```
40-
com.mickaelb.assertions.HibernateAssertCountException:
41-
Expected 5 INSERT but got 6:
42-
=> '/* insert com.lemick.demo.entity.BlogPost */ insert into blog_post (id, title) values (default, ?)'
43-
=> '/* insert com.lemick.demo.entity.PostComment */ insert into post_comment (id, blog_post_id, content) values (default, ?, ?)'
44-
=> '/* insert com.lemick.demo.entity.PostComment */ insert into post_comment (id, blog_post_id, content) values (default, ?, ?)'
45-
=> '/* insert com.lemick.demo.entity.BlogPost */ insert into blog_post (id, title) values (default, ?)'
46-
=> '/* insert com.lemick.demo.entity.PostComment */ insert into post_comment (id, blog_post_id, content) values (default, ?, ?)'
47-
=> '/* insert com.lemick.demo.entity.PostComment */ insert into post_comment (id, blog_post_id, content) values (default, ?, ?)'
39+
com.mickaelb.assertions.HibernateAssertCountException:
40+
Expected 5 INSERT but got 6:
41+
=> '/* insert com.lemick.demo.entity.BlogPost */ insert into blog_post (id, title) values (default, ?)'
42+
=> '/* insert com.lemick.demo.entity.PostComment */ insert into post_comment (id, blog_post_id, content) values (default, ?, ?)'
43+
=> '/* insert com.lemick.demo.entity.PostComment */ insert into post_comment (id, blog_post_id, content) values (default, ?, ?)'
44+
=> '/* insert com.lemick.demo.entity.BlogPost */ insert into blog_post (id, title) values (default, ?)'
45+
=> '/* insert com.lemick.demo.entity.PostComment */ insert into post_comment (id, blog_post_id, content) values (default, ?, ?)'
46+
=> '/* insert com.lemick.demo.entity.PostComment */ insert into post_comment (id, blog_post_id, content) values (default, ?, ?)'
4847
```
49-
### Assert L2C statistics
48+
49+
### Assert SQL statements programmatically
50+
51+
You can also assert statements from several transactions in your test using the programmatic API:
52+
```java
53+
@Test
54+
void multiple_assertions_using_programmatic_api() {
55+
QueryAssertions.assertInsertCount(2, () -> {
56+
BlogPost post_1 = new BlogPost("Blog post 1");
57+
post_1.addComment(new PostComment("Good article"));
58+
blogPostRepository.save(post_1);
59+
});
60+
61+
QueryAssertions.assertSelectCount(1, () -> blogPostRepository.findById(1L));
62+
63+
// Or even multiple asserts at once
64+
QueryAssertions.assertStatementCount(Map.of(INSERT, 2, SELECT, 1), () -> {
65+
BlogPost post_1 = new BlogPost("Blog post 1");
66+
post_1.addComment(new PostComment("Good article"));
67+
blogPostRepository.save(post_1);
68+
69+
blogPostRepository.findById(1L);
70+
});
71+
}
72+
```
73+
74+
### Assert L2C statistics declaratively
5075

5176
It supports assertions on Hibernate level two cache statistics, useful for checking that your entities are cached correctly and that they will stay forever:
5277
```java
53-
@Test
54-
@AssertHibernateL2CCount(misses = 1, puts = 1, hits = 1)
55-
void create_one_post_and_read_it() {
56-
doInTransaction(() -> {
57-
BlogPost post_1 = new BlogPost("Blog post 1");
58-
blogPostRepository.save(post_1);
59-
});
60-
61-
doInTransaction(() -> {
62-
blogPostRepository.findById(1L); // 1 MISS + 1 PUT
63-
});
64-
65-
doInTransaction(() -> {
66-
blogPostRepository.findById(1L); // 1 HIT
67-
});
68-
}
78+
@Test
79+
@AssertHibernateL2CCount(misses = 1, puts = 1, hits = 1)
80+
void create_one_post_and_read_it() {
81+
doInTransaction(() -> {
82+
BlogPost post_1 = new BlogPost("Blog post 1");
83+
blogPostRepository.save(post_1);
84+
});
85+
86+
doInTransaction(() -> {
87+
blogPostRepository.findById(1L); // 1 MISS + 1 PUT
88+
});
89+
90+
doInTransaction(() -> {
91+
blogPostRepository.findById(1L); // 1 HIT
92+
});
93+
}
6994
```
7095
## How to integrate
7196
1. Import the dependency
72-
```xml
73-
<dependency>
74-
<groupId>com.mickaelb</groupId>
75-
<artifactId>hibernate-query-asserts</artifactId>
76-
<version>2.0.0</version>
77-
</dependency>
78-
```
97+
```xml
98+
<dependency>
99+
<groupId>com.mickaelb</groupId>
100+
<artifactId>hibernate-query-asserts</artifactId>
101+
<version>2.0.0</version>
102+
</dependency>
103+
```
79104
2. Register the integration with Hibernate, you just need to add this key in your configuration (here for yml):
80105

81106
spring:
82-
jpa:
83-
properties:
84-
hibernate.session_factory.statement_inspector: com.mickaelb.integration.hibernate.HibernateStatementInspector
107+
jpa:
108+
properties:
109+
hibernate.session_factory.statement_inspector: com.mickaelb.integration.hibernate.HibernateStatementInspector
85110

86111
3. Register the Spring TestListener that will launch the SQL inspection if the annotation is present:
87112

88113
* By adding the listener on each of your integration test:
89114
```java
90-
@SpringBootTest
91-
@TestExecutionListeners(
92-
listeners = HibernateAssertTestListener.class,
93-
mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
94-
)
95-
class MySpringIntegrationTest {
96-
...
97-
}
115+
@SpringBootTest
116+
@TestExecutionListeners(
117+
listeners = HibernateAssertTestListener.class,
118+
mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
119+
)
120+
class MySpringIntegrationTest {
121+
...
122+
}
98123
```
99124

100125
* **OR** by adding a **META-INF/spring.factories** file that contains the definition, that will register the listener for all your tests:
101126
```
102-
org.springframework.test.context.TestExecutionListener=com.mickaelb.integration.spring.HibernateAssertTestListener
127+
org.springframework.test.context.TestExecutionListener=com.mickaelb.integration.spring.HibernateAssertTestListener
103128
```

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>com.mickaelb</groupId>
88
<artifactId>hibernate-query-asserts</artifactId>
9-
<version>2.0.0-SNAPSHOT</version>
9+
<version>2.1.0-SNAPSHOT</version>
1010

1111
<name>${project.groupId}:${project.artifactId}</name>
1212
<description>A library that can assert statement generated by Hibernate in Spring tests</description>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.mickaelb.api;
2+
3+
import com.mickaelb.integration.spring.assertions.sql.HibernateStatementAssertionResult;
4+
5+
import java.util.List;
6+
import java.util.Map;
7+
import java.util.function.Supplier;
8+
import java.util.stream.Collectors;
9+
10+
import static com.mickaelb.api.StatementType.*;
11+
import static com.mickaelb.integration.hibernate.HibernateStatementInspector.getStatistics;
12+
13+
public class QueryAssertions {
14+
15+
private static final Map<StatementType, Supplier<List<String>>> STATEMENT_SUPPLIERS = Map.of(
16+
INSERT, () -> getStatistics().getInsertStatements(),
17+
UPDATE, () -> getStatistics().getUpdateStatements(),
18+
SELECT, () -> getStatistics().getSelectStatements(),
19+
DELETE, () -> getStatistics().getDeleteStatements()
20+
);
21+
22+
private QueryAssertions() {
23+
}
24+
25+
public static void assertInsertCount(int expectedInsertCount, Runnable runnable) {
26+
doAssertStatementCount(runnable, INSERT, expectedInsertCount);
27+
}
28+
29+
public static void assertUpdateCount(int expectedUpdateCount, Runnable runnable) {
30+
doAssertStatementCount(runnable, UPDATE, expectedUpdateCount);
31+
}
32+
33+
public static void assertSelectCount(int expectedSelectCount, Runnable runnable) {
34+
doAssertStatementCount(runnable, SELECT, expectedSelectCount);
35+
}
36+
37+
public static void assertDeleteCount(int expectedDeleteCount, Runnable runnable) {
38+
doAssertStatementCount(runnable, DELETE, expectedDeleteCount);
39+
}
40+
41+
public static void assertStatementCount(Map<StatementType, Integer> expectedCounts, Runnable runnable) {
42+
Map<StatementType, Integer> sizesBeforeExecution = expectedCounts.keySet().stream()
43+
.collect(Collectors.toMap(
44+
statementType -> statementType,
45+
statementType -> STATEMENT_SUPPLIERS.get(statementType).get().size()
46+
));
47+
48+
runnable.run();
49+
50+
for (Map.Entry<StatementType, Integer> entry : expectedCounts.entrySet()) {
51+
StatementType statementType = entry.getKey();
52+
int expectedCount = entry.getValue();
53+
54+
Supplier<List<String>> statementSupplier = STATEMENT_SUPPLIERS.get(statementType);
55+
List<String> fullStatements = statementSupplier.get();
56+
57+
int sizeBeforeExecution = sizesBeforeExecution.get(statementType);
58+
List<String> executionScopedStatements = fullStatements.subList(sizeBeforeExecution, fullStatements.size());
59+
60+
HibernateStatementAssertionResult assertionResult = new HibernateStatementAssertionResult(statementType, executionScopedStatements, expectedCount);
61+
assertionResult.validate();
62+
}
63+
}
64+
65+
private static void doAssertStatementCount(Runnable runnable, StatementType statementType, int expectedCount) {
66+
Supplier<List<String>> statementSupplier = STATEMENT_SUPPLIERS.get(statementType);
67+
int sizeBeforeExecution = statementSupplier.get().size();
68+
runnable.run();
69+
70+
List<String> fullStatements = statementSupplier.get();
71+
List<String> executionScopedStatements = fullStatements.subList(sizeBeforeExecution, fullStatements.size());
72+
73+
HibernateStatementAssertionResult assertionResult = new HibernateStatementAssertionResult(statementType, executionScopedStatements, expectedCount);
74+
assertionResult.validate();
75+
}
76+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.mickaelb.api;
2+
3+
public enum StatementType {
4+
SELECT, INSERT, UPDATE, DELETE
5+
}

src/main/java/com/mickaelb/integration/hibernate/HibernateStatementStatistics.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ public void resetStatistics() {
1515
updateStatements.clear();
1616
insertStatements.clear();
1717
deleteStatements.clear();
18-
1918
}
2019

2120
@Override
@@ -39,18 +38,18 @@ public void notifyDeleteStatement(String sql) {
3938
}
4039

4140
public List<String> getSelectStatements() {
42-
return selectStatements;
41+
return new ArrayList<>(selectStatements);
4342
}
4443

4544
public List<String> getUpdateStatements() {
46-
return updateStatements;
45+
return new ArrayList<>(updateStatements);
4746
}
4847

4948
public List<String> getInsertStatements() {
50-
return insertStatements;
49+
return new ArrayList<>(insertStatements);
5150
}
5251

5352
public List<String> getDeleteStatements() {
54-
return deleteStatements;
53+
return new ArrayList<>(deleteStatements) ;
5554
}
5655
}

src/main/java/com/mickaelb/integration/spring/HibernateL2CCountTestListener.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,7 @@ public void beforeTestClass(TestContext testContext) {
2727

2828
@Override
2929
public void beforeTestMethod(TestContext testContext) {
30-
AssertHibernateL2CCount l2cCountAnnotation = testContext.getTestMethod().getAnnotation(AssertHibernateL2CCount.class);
31-
if (l2cCountAnnotation != null) {
32-
sessionFactoryStatistics.clear();
33-
}
30+
sessionFactoryStatistics.clear();
3431
}
3532

3633
@Override

src/main/java/com/mickaelb/integration/spring/HibernateSQLCountTestListener.java

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,20 @@
1212
import java.util.List;
1313
import java.util.function.Supplier;
1414

15-
import static com.mickaelb.integration.spring.assertions.sql.HibernateStatementAssertionResult.StatementType.*;
16-
import static com.mickaelb.integration.spring.assertions.sql.HibernateStatementAssertionResult.StatementType.DELETE;
15+
import static com.mickaelb.api.StatementType.*;
1716

1817
public class HibernateSQLCountTestListener implements AssertTestListener{
1918

2019
private Supplier<HibernateStatementStatistics> statisticsSupplier = HibernateStatementInspector::getStatistics;
2120
private Supplier<Boolean> transactionAvailabilitySupplier = TestTransaction::isActive;
2221

23-
2422
@Override
2523
public void beforeTestClass(TestContext testContext) {
26-
2724
}
2825

2926
@Override
3027
public void beforeTestMethod(TestContext testContext) {
31-
AssertHibernateSQLCount sqlCountAnnotation = testContext.getTestMethod().getAnnotation(AssertHibernateSQLCount.class);
32-
if (sqlCountAnnotation != null) {
33-
statisticsSupplier.get().resetStatistics();
34-
}
28+
statisticsSupplier.get().resetStatistics();
3529
}
3630

3731
@Override
@@ -41,7 +35,6 @@ public void afterTestMethod(TestContext testContext) {
4135
flushExistingPersistenceContext(testContext, transactionAvailabilitySupplier);
4236
evaluateSQLStatementCount(sqlCountAnnotation);
4337
}
44-
4538
}
4639

4740
private void flushExistingPersistenceContext(TestContext testContext, Supplier<Boolean> transactionAvailabilitySupplier) {

0 commit comments

Comments
 (0)