In modern software development, security is no longer optional—it’s a critical requirement. Applications are continuously exposed to threats such as SQL injection, misconfigured authentication, insecure network channels, and misused secrets. While penetration testing and static analysis play a role, integration-level security testing with tools like Testcontainers brings an extra layer of confidence by simulating realistic attack vectors in controlled environments.
This tutorial explores how Java developers can leverage Testcontainers for security testing, from database injection testing to network isolation and cloud-native validations. By the end, you’ll be ready to implement production-grade security tests that can run locally or in CI/CD pipelines.
What is Security Testing in Java?
Security testing ensures that an application:
- Protects data from unauthorized access.
- Handles malicious inputs gracefully (e.g., SQL injection).
- Enforces encryption and authentication correctly.
- Maintains resilience against misconfigured or compromised infrastructure.
Why Testcontainers?
- Realistic environments: Spin up real services like PostgreSQL, MySQL, Redis, or Keycloak.
- Isolation: Run tests in disposable containers, preventing leaks into production systems.
- Reproducibility: Same security test runs locally and in CI/CD with no environment drift.
- Flexibility: Combine with JUnit 5, Mockito, WireMock, and security tools.
Setting Up Testcontainers for Security Testing
First, include Testcontainers dependencies in pom.xml
:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.20.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.20.2</version>
<scope>test</scope>
</dependency>
For network simulations or penetration-style checks, you may also use:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>toxiproxy</artifactId>
<version>1.20.2</version>
<scope>test</scope>
</dependency>
Example 1: SQL Injection Testing with PostgreSQL
Let’s validate that a repository layer is safe against SQL injection.
@Testcontainers
public class SecurityInjectionTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("securedb")
.withUsername("user")
.withPassword("password");
@Test
void shouldPreventSqlInjection() throws SQLException {
Connection conn = DriverManager.getConnection(
postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword());
Statement stmt = conn.createStatement();
String maliciousInput = "'; DROP TABLE users; --";
// Example query that should use prepared statements
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM users WHERE username = ?");
ps.setString(1, maliciousInput);
ResultSet rs = ps.executeQuery();
assertFalse(rs.next(), "SQL injection attempt should not return results");
}
}
This test ensures unsafe concatenated SQL doesn’t execute malicious code.
Example 2: Network Isolation & Latency Simulation
Security often depends on network resilience. Using ToxiproxyContainer
, you can simulate attacks like high latency or dropped connections.
@Testcontainers
class NetworkSecurityTest {
@Container
static ToxiproxyContainer toxiproxy = new ToxiproxyContainer("shopify/toxiproxy:2.1.4");
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
@Test
void shouldHandleNetworkLatency() throws Exception {
ToxiproxyContainer.ContainerProxy proxy =
toxiproxy.getProxy(postgres, 5432);
proxy.toxics().latency("latency", ToxicDirection.DOWNSTREAM, 3000);
assertThrows(SQLException.class, () -> {
DriverManager.getConnection(proxy.getContainerIpAddress(),
proxy.getProxyPort(),
postgres.getUsername(),
postgres.getPassword());
});
}
}
This test validates whether your application times out gracefully under suspicious delays.
Example 3: Authentication Testing with Keycloak
You can use the Keycloak Testcontainer to validate OAuth2/JWT flows securely.
@Container
static KeycloakContainer keycloak = new KeycloakContainer("quay.io/keycloak/keycloak:23.0")
.withRealmImportFile("test-realm.json");
By simulating identity providers, you ensure login, token refresh, and expired sessions behave as expected.
Best Practices for Security Testing with Testcontainers
- Use Realistic Data: Seed containers with edge-case datasets (malicious inputs, long strings).
- Automate in CI/CD: Run security tests as part of pipelines in Jenkins or GitHub Actions.
- Combine Tools: Use WireMock for API fakes + Testcontainers for DB/message brokers.
- Fail Fast: Make tests explicit about what should fail (e.g., injection attempts).
- Reuse Containers: Optimize execution with
@Testcontainers
reusable lifecycle.
CI/CD Integration
Running security tests with Testcontainers in CI/CD pipelines ensures shifts-left security.
- Jenkins: Use Docker-in-Docker or bind host Docker socket.
- GitHub Actions: Add
services: docker
in workflow YAML. - GitLab CI: Native Docker executor works seamlessly.
jobs:
security-tests:
runs-on: ubuntu-latest
services:
docker:
image: docker:20.10
steps:
- uses: actions/checkout@v4
- name: Run Security Tests
run: mvn test -Dgroups=security
Version Tracker
- JUnit 4 → JUnit 5: Improved parameterized tests and annotations for security-specific grouping.
- Mockito Updates: Static/final method mocking helps simulate cryptographic libraries.
- Testcontainers Growth: Added modules for Keycloak, LocalStack, and Toxiproxy for advanced scenarios.
Conclusion & Key Takeaways
Security testing with Testcontainers bridges the gap between unit tests and penetration tests.
You can validate injection resistance, secure networking, and authentication robustness—all within your development pipeline.
Key Points to Remember:
- Test against real services, not mocks alone.
- Simulate malicious conditions in safe, reproducible environments.
- Integrate with CI/CD for continuous protection.
FAQ
1. What’s the difference between functional and security tests?
Functional tests validate correctness; security tests validate robustness against attacks.
2. Can I use Testcontainers for penetration testing?
Yes, you can simulate injection attacks, misconfigurations, and network anomalies, though it complements—not replaces—dedicated tools.
3. How do I test authentication flows?
Use Keycloak, Okta, or LDAP Testcontainers modules to simulate identity providers.
4. Can Testcontainers simulate SSL/TLS misconfigurations?
Yes, by starting services with custom certificates and testing handshake failures.
5. What’s the role of Toxiproxy in security testing?
It simulates malicious or faulty network conditions like packet drops or latency spikes.
6. How do I run these tests in GitHub Actions?
Enable Docker service, checkout code, and run mvn test
. Containers spin up automatically.
7. Can I combine Mockito with Testcontainers in security tests?
Yes, Mockito can stub external services while Testcontainers runs real databases or auth servers.
8. How do I seed databases with malicious input?
Mount SQL init scripts or use JDBC within container lifecycle hooks.
9. Is Testcontainers enough for compliance testing (e.g., PCI-DSS)?
It helps validate security at integration level, but you still need audits and penetration tests.
10. What if a container image has vulnerabilities?
Always scan images (e.g., Trivy, Snyk) as part of pipelines to avoid introducing risks.