https://github.com/kefirfromperm/jmina
JMina is a lightweight Java utility for testing and debugging complex systems through log interception. It was created to address the challenges of diagnosing flaky tests—especially those that fail intermittently in CI/CD environments with limited visibility into internal state.
https://github.com/kefirfromperm/jmina
code-quality java logging slf4j test unit-test
Last synced: about 1 month ago
JSON representation
JMina is a lightweight Java utility for testing and debugging complex systems through log interception. It was created to address the challenges of diagnosing flaky tests—especially those that fail intermittently in CI/CD environments with limited visibility into internal state.
- Host: GitHub
- URL: https://github.com/kefirfromperm/jmina
- Owner: kefirfromperm
- License: apache-2.0
- Created: 2024-06-24T08:43:47.000Z (about 2 years ago)
- Default Branch: master
- Last Pushed: 2026-03-23T06:06:56.000Z (3 months ago)
- Last Synced: 2026-03-23T07:28:07.822Z (3 months ago)
- Topics: code-quality, java, logging, slf4j, test, unit-test
- Language: Java
- Homepage: https://jmina.dev/
- Size: 252 KB
- Stars: 6
- Watchers: 1
- Forks: 0
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
README
# JMina for unit tests
[](https://github.com/kefirfromperm/jmina/actions/workflows/build.yml)
[](https://central.sonatype.com/artifact/dev.jmina/jmina)
[](LICENSE)
[](https://javadoc.io/doc/dev.jmina/jmina)
[](https://github.com/kefirfromperm/jmina-examples)
> [!NOTE]
> JMina is not [Apache MINA](https://mina.apache.org/) and is not associated with it in any way. Unfortunately, I
> remembered about it after I registered the domain name.
JMina is a unit testing library for Java that lets you verify log call arguments anywhere inside your code — without
changing a single line of production logic. It implements the [SLF4J](https://www.slf4j.org/) logger interface and
intercepts log calls during test execution.
```java
Mina.on(MyClass.class, DEBUG, "value: {}").check((String val) -> assertEquals("expected", val));
```
> [!TIP]
> More examples can be found in the repository https://github.com/kefirfromperm/jmina-examples
## Why JMina?
Testing internal state is hard. You can return values, expose fields, or pass mocks around — but sometimes the cleanest
solution is to log the value and verify it in the test. JMina makes that practical:
- **No production code changes.** Use your existing SLF4J log calls as test observation points. No injected mocks, no
extra return values, no refactoring.
- **Works alongside your real logger.** Configure a delegate SLF4J provider and JMina passes every log call through to
it, so logging still works normally during tests.
- **Fails at the right place.** When a forbidden log is triggered, the test fails immediately with a stack trace
pointing into your code — not at a generic assertion at the end of the test.
## How It Works
JMina registers itself as an SLF4J service provider. When your code calls `LoggerFactory.getLogger(...)`, SLF4J routes
through JMina. On every log call, JMina:
1. Matches the call against the conditions you registered with `on(...)`.
2. Runs your verification lambda (or throws immediately if `.exception()` was used).
3. Optionally forwards the call to a delegate logger (e.g., Logback, SLF4J Simple).
Because JMina sits at the SLF4J layer, no instrumentation or bytecode manipulation is needed.
## Setup
JMina is published to Maven Central.
### Gradle
```kotlin
dependencies {
testImplementation("dev.jmina:jmina:0.1.4")
}
```
### Maven
```xml
dev.jmina
jmina
0.1.4
test
```
### System Properties
Configure JMina by setting system properties for the test task.
| System Property | Value | Description |
|---------------------------|---------------------------------------------------------------|-------------------------------------------------------------------|
| `slf4j.provider` | `dev.jmina.log.MinaServiceProvider` | Use JMina as the SLF4J provider. **Mandatory.** |
| `jmina.delegate.provider` | An SLF4J service provider class name of your logging library | Forward log calls to a real logger alongside JMina interception |
| `jmina.context.global` | `true` or `false` | Use global context store or thread-local (see below) |
Gradle example:
```kotlin
tasks {
test {
useJUnitPlatform()
systemProperty("slf4j.provider", "dev.jmina.log.MinaServiceProvider")
systemProperty("jmina.delegate.provider", "ch.qos.logback.classic.spi.LogbackServiceProvider")
systemProperty("jmina.context.global", "false")
}
}
```
### Global vs Thread-Local Context
JMina manages test conditions in a *context store*. There are two modes:
**Thread-local context** (`jmina.context.global=false`) — Each thread gets its own isolated context. Conditions
registered on one thread are invisible to others. This is the right choice when:
- Your production code runs on a single thread, and
- Tests run in parallel (each test thread stays isolated).
**Global context** (`jmina.context.global=true`) — All threads share one context. Conditions registered on the test
thread are visible to any thread that calls the logger. Use this when:
- Your production code spawns its own threads (the log call happens on a different thread than the test), and
- Tests run sequentially (parallel tests would collide on the shared context).
## Quick Example
Say you're testing a class that solves a quadratic equation and you want to verify the discriminant computed inside the
method. Add a log call at that point in your production code:
```java
public class QuadraticEquation {
private final Logger log = LoggerFactory.getLogger(QuadraticEquation.class);
public List solve(double a, double b, double c) {
double discriminant = b * b - 4 * a * c;
log.debug("discriminant: {}", discriminant); // observation point
if (discriminant < 0) {
return Collections.emptyList();
} else {
List roots = new ArrayList<>();
if (discriminant > 0) {
roots.add((-b - sqrt(discriminant)) / (2 * a));
roots.add((-b + sqrt(discriminant)) / (2 * a));
} else {
roots.add(-b / (2 * a));
}
return roots;
}
}
}
```
[QuadraticEquation.java](src/test/java/dev/jmina/example/QuadraticEquation.java)
Then verify the internal value in your test:
```java
public class QuadraticEquationTest {
@Test
public void testSolve() {
// Register a check before running the code under test
Mina.on(QuadraticEquation.class, DEBUG, "discriminant: {}")
.check((Double discriminant) -> assertEquals(9.0, discriminant));
List roots = new QuadraticEquation().solve(1, -1, -2);
// Assert that all registered conditions were actually triggered
Mina.assertAllCalled();
assertEquals(-1.0, roots.get(0));
assertEquals(2.0, roots.get(1));
}
@AfterEach
public void clean() {
Mina.clean(); // Always clean up the context after each test
}
}
```
[QuadraticEquationTest.java](src/test/java/dev/jmina/example/QuadraticEquationTest.java)
## API Overview
### Conditions
`Mina.on(...)` filters which log calls to intercept. All parameters are optional — omit any to make it a wildcard for
that field.
```java
// Specific: class + level + message pattern
Mina.on(MyClass.class, DEBUG, "value: {}").check(...);
// By class and level only
Mina.on(MyClass.class, WARN).check(...);
// By level only — matches any logger at that level
Mina.on(ERROR).check(...);
// By message pattern only — matches any logger, any level
Mina.on("value: {}").check(...);
// No filter — matches every log call
Mina.on().check(...);
```
You can also filter by logger name string or SLF4J `Marker` instead of a class.
### Checks
After `on(...)`, call one of the `check` methods to define what to verify when the condition matches:
```java
// Just assert the log was called (use with Mina.assertAllCalled())
.check()
// Assert argument values with equals()
.check("expected", 42)
// Lambda with typed arguments (1–6 arguments supported; last can be Throwable)
.check((String msg) -> assertTrue(msg.startsWith("OK")))
.check((String msg, Throwable t) -> assertNotNull(t))
// No-argument lambda — run any assertion
.check(() -> assertTrue(someCondition))
// Verify only the throwable
.checkThrowable(t -> assertInstanceOf(IOException.class, t))
```
### Forbidden Logs
Use `.exception()` to assert that a log call must **not** happen. If the condition matches, JMina throws immediately —
failing the test with a stack trace pointing to the exact log statement inside your code.
```java
// Fail the test the moment this error log is triggered
Mina.on(PaymentService.class, ERROR, "Payment failed {}").exception();
paymentService.process(validPayment); // this must not reach the error branch
```
## Comparison with Alternatives
| Approach | Production code change | Typed arguments | Any SLF4J backend | Fails at error point |
|---------------------------|------------------------|--------------------|--------------------|----------------------|
| **JMina** | None | Yes | Yes | Yes |
| Mockito (mock logger) | Inject mock logger | No (string verify) | N/A | No |
| Logback `ListAppender` | None | No (strings only) | Logback only | No |