Skip to content

Commit 8095d2f

Browse files
committed
docs(performance): add comprehensive guide for performance testing setup and best practices
Includes instructions for running performance tests, database-bound testing with Testcontainers, and using `JpaEntityManagerService`. Provides example test class, configuration details, and advanced usage for query profiling.
1 parent 5e18459 commit 8095d2f

2 files changed

Lines changed: 153 additions & 0 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package edu.harvard.iq.dataverse.somepackage;
2+
3+
import edu.harvard.iq.dataverse.util.testing.performance.JpaEntityManagerService;
4+
import edu.harvard.iq.dataverse.util.testing.performance.JpaPerformanceTest;
5+
import net.ttddyy.dsproxy.QueryCount;
6+
import net.ttddyy.dsproxy.QueryCountHolder;
7+
import org.junit.jupiter.api.BeforeAll;
8+
import org.junit.jupiter.api.Test;
9+
10+
import java.time.Instant;
11+
import java.time.temporal.ChronoUnit;
12+
import jakarta.persistence.EntityManager;
13+
14+
import static org.junit.jupiter.api.Assertions.assertNotNull;
15+
16+
// Single annotation for automatic setup of
17+
// 1) basic tags for JUnit groups,
18+
// 2) shared PostgreSQL server via Testcontainers, and
19+
// 3) creation and injection of JPA entity manager service.
20+
@JpaPerformanceTest
21+
class SamplePerformanceIT {
22+
23+
static JpaEntityManagerService jpa;
24+
25+
@BeforeAll
26+
static void setUp() {
27+
// A manual start is necessary to allow you to selectively enable service features as necessary
28+
jpa.start();
29+
30+
// inTransactionVoid: Use this when you only need to execute database operations
31+
// (e.g., persisting test fixtures) without returning a value.
32+
jpa.inTransactionVoid(em -> {
33+
// EntityManager em is provided here.
34+
// em.persist(myEntity);
35+
});
36+
}
37+
38+
@Test
39+
void shouldMeasureOperationPerformance() {
40+
// Clear any previous query statistics
41+
QueryCountHolder.clear();
42+
Instant start = Instant.now();
43+
44+
// inTransaction: Use this when your operation returns a result that needs
45+
// to be asserted or measured.
46+
Object result = jpa.inTransaction(em -> {
47+
// Execute your performance-critical operation using the EntityManager.
48+
// return result;
49+
return null; // Placeholder
50+
});
51+
52+
Instant end = Instant.now();
53+
assertNotNull(result);
54+
55+
// Retrieve and log ORM statistics
56+
QueryCount count = QueryCountHolder.getGrandTotal();
57+
System.out.println("Elapsed ms: " + start.until(end, ChronoUnit.MILLIS));
58+
System.out.println("Total queries: " + count.getTotal());
59+
System.out.println("Select queries: " + count.getSelect());
60+
System.out.println("Insert queries: " + count.getInsert());
61+
System.out.println("Update queries: " + count.getUpdate());
62+
System.out.println("Delete queries: " + count.getDelete());
63+
}
64+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Performance Testing
2+
3+
## Introduction
4+
Performance tests measure how your application behaves under load, focusing on execution time, resource consumption, and database efficiency.
5+
Unlike *unit tests*, which verify isolated logic, or *integration* or *API tests*, which validate component interactions and full request lifecycles, performance tests quantify *how fast* operations complete and *how many* database queries they trigger.
6+
7+
## Running Performance Tests
8+
Performance tests are excluded from the default test run to save CI/CD time and local resources.
9+
To execute them, use the Maven `verify` lifecycle phase and override the `it.groups` property:
10+
11+
```shell
12+
mvn verify -Dit.groups=performance
13+
```
14+
15+
```{note}
16+
The `it.groups` property accepts a comma-separated list.
17+
You can combine groups (e.g., `-Dit.groups=integration,performance`) as necessary.
18+
However, it is highly recommended to run them in isolation due to their computational intensity and sensitivity to system load.
19+
```
20+
21+
## Testing database-bound code
22+
Performance tests for code relying on retrieving entities from a database are essential for catching regressions in ORM efficiency.
23+
They can identify N+1 query problems or ensure that heavy data processing pipelines (e.g., exporting large datasets) remain responsive as the codebase evolves.
24+
25+
### Prerequisites
26+
Any tests around database-bound code rely on [Testcontainers](https://www.testcontainers.org/) to spin up ephemeral database instances.
27+
Avoiding in-memory databases for such tests allow for more realistic testing as seen in actual deployments.
28+
Consequently, you must have **Docker** installed and running, allowing Testcontainer to start a PostgreSQL server.
29+
30+
- If you use a local Docker daemon, ensure it has sufficient memory allocated (typically 1GB+ is recommended for running Postgres containers alongside your tests).
31+
- If your Docker daemon runs remotely, ensure the `DOCKER_HOST` environment variable is correctly configured in your shell so Testcontainers can locate it.
32+
33+
The automated testing setup will look up a system property `postgresql.server.version` to determine which container image tag to use.
34+
The property is injected from `pom.xml` by Maven Failsafe and use a reasonable fallback value if missing.
35+
To test with a different version of PostgreSQL, you may set the Maven property `postgresql.server.version` for a run.
36+
37+
### Example
38+
Performance test classes must follow specific conventions to be discovered and executed correctly:
39+
40+
1. **Package Location:**
41+
Place your test class in `src/test/java`, mirroring the package structure of the code you want to test (e.g., `edu.harvard.iq.dataverse.export`).
42+
This placement grants the test class access to package private members in `src/main/java`, which is often necessary when testing internal services directly without going through the full API layer.
43+
2. **Naming Convention:**
44+
Name the class `*IT.java` so that the Maven Failsafe plugin automatically picks it up during the `integration-test` phase.
45+
3. **Setup Annotation:**
46+
Annotate the class with `@JpaPerformanceTest` to have everything set up automatically for you.
47+
A `JpaEntityManagerService` will be injected into a static class field for you, allowing interaction with a JPA Entity Manager.
48+
49+
Below is a minimal, generic example [`SamplePerformanceIT`](/_static/developers/testing/SamplePerformanceIT.java) demonstrating the structure and how to run a transaction with or without a return value.
50+
51+
```{literalinclude} /_static/developers/testing/SamplePerformanceIT.java
52+
:name: sample-performance-test
53+
:language: java
54+
:start-at: //
55+
```
56+
57+
### Understanding JpaEntityManagerService
58+
The `JpaEntityManagerService` class abstracts away the boilerplate required to set up a JPA environment for testing.
59+
Here is what it does under the hood:
60+
61+
1. **Automatic PostgreSQL Server Setup:**
62+
The involved JUnit Test Extension makes sure to create a single server instance to speed up test setups.
63+
Nonetheless, any test class will run within its own database on the server, guaranteeing test database isolation.
64+
65+
2. **Automatic Schema Generation:**
66+
When you call `.start()` on a `JpaEntityManagerService` instance, it initializes an EclipseLink `EntityManagerFactory` configured to automatically generate the database schema (`schema-generation.database.action=create`).
67+
This guarantees that every test run begins with a pristine database structure derived directly from your current JPA entity mappings.
68+
You do not need to run Flyway migrations or seed the database beforehand.
69+
70+
3. **Transaction Management:**
71+
The service handles the lifecycle of JPA transactions automatically.
72+
You simply pass a lambda to `inTransaction()` or `inTransactionVoid()`.
73+
The service will:
74+
1. Create an `EntityManager` and begin a transaction.
75+
2. Execute your lambda.
76+
3. Commit the transaction on success, or roll it back if a `RuntimeException` is thrown.
77+
4. Close the `EntityManager` in a `finally` block to prevent resource leaks.
78+
79+
4. **Query Statistics via Wrapped DataSource:**
80+
To make it easy to profile ORM behavior, `JpaEntityManagerService` wraps the underlying PostgreSQL `DataSource` using a proxy that intercepts all SQL statements.
81+
82+
By default, the proxy tracks query counts, which you can retrieve via `QueryCountHolder.getGrandTotal()`.
83+
This provides immediate, programmatic insight into database efficiency without needing to parse verbose SQL logs.
84+
It is particularly useful for:
85+
- Verifying that a batch operation executes in a single query rather than a loop.
86+
- Catching N+1 query problems by asserting on the number of `SELECT` statements.
87+
88+
*Advanced Usage:* The default service only tracks query counts.
89+
If you need detailed SQL logging (including bound parameters) or custom execution metrics, you can extend `JpaEntityManagerService` and register additional `StatementListener` implementations on the `ProxyDataSourceBuilder` during initialization.

0 commit comments

Comments
 (0)