{"id":49907403,"url":"https://github.com/kefirfromperm/jmina","last_synced_at":"2026-05-16T11:11:37.777Z","repository":{"id":246170912,"uuid":"819306146","full_name":"kefirfromperm/jmina","owner":"kefirfromperm","description":"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.","archived":false,"fork":false,"pushed_at":"2026-03-23T06:06:56.000Z","size":258,"stargazers_count":6,"open_issues_count":4,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-03-23T07:28:07.822Z","etag":null,"topics":["code-quality","java","logging","slf4j","test","unit-test"],"latest_commit_sha":null,"homepage":"https://jmina.dev/","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kefirfromperm.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["kefirfromperm"]}},"created_at":"2024-06-24T08:43:47.000Z","updated_at":"2026-03-22T16:18:37.000Z","dependencies_parsed_at":"2024-07-20T11:01:37.436Z","dependency_job_id":"0f49cfaa-61cc-4eea-964d-80fe76d24c1d","html_url":"https://github.com/kefirfromperm/jmina","commit_stats":null,"previous_names":["kefirfromperm/mina","kefirfromperm/jmina"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/kefirfromperm/jmina","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kefirfromperm%2Fjmina","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kefirfromperm%2Fjmina/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kefirfromperm%2Fjmina/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kefirfromperm%2Fjmina/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kefirfromperm","download_url":"https://codeload.github.com/kefirfromperm/jmina/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kefirfromperm%2Fjmina/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33100366,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-16T04:41:52.686Z","status":"ssl_error","status_checked_at":"2026-05-16T04:41:52.009Z","response_time":115,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["code-quality","java","logging","slf4j","test","unit-test"],"created_at":"2026-05-16T11:11:37.480Z","updated_at":"2026-05-16T11:11:37.771Z","avatar_url":"https://github.com/kefirfromperm.png","language":"Java","funding_links":["https://github.com/sponsors/kefirfromperm"],"categories":[],"sub_categories":[],"readme":"# JMina for unit tests\n\n[![build](https://github.com/kefirfromperm/jmina/actions/workflows/build.yml/badge.svg)](https://github.com/kefirfromperm/jmina/actions/workflows/build.yml)\n[![Maven Central Version](https://img.shields.io/maven-central/v/dev.jmina/jmina)](https://central.sonatype.com/artifact/dev.jmina/jmina)\n[![GitHub License](https://img.shields.io/github/license/kefirfromperm/jmina)](LICENSE)\n[![javadoc](https://javadoc.io/badge2/dev.jmina/jmina/javadoc.svg)](https://javadoc.io/doc/dev.jmina/jmina)\n[![Examples](https://img.shields.io/badge/examples-indigo)](https://github.com/kefirfromperm/jmina-examples)\n\n\u003e [!NOTE]\n\u003e JMina is not [Apache MINA](https://mina.apache.org/) and is not associated with it in any way. Unfortunately, I\n\u003e remembered about it after I registered the domain name.\n\nJMina is a unit testing library for Java that lets you verify log call arguments anywhere inside your code — without\nchanging a single line of production logic. It implements the [SLF4J](https://www.slf4j.org/) logger interface and\nintercepts log calls during test execution.\n\n```java\nMina.on(MyClass.class, DEBUG, \"value: {}\").check((String val) -\u003e assertEquals(\"expected\", val));\n```\n\n\u003e [!TIP]\n\u003e More examples can be found in the repository https://github.com/kefirfromperm/jmina-examples\n\n## Why JMina?\n\nTesting internal state is hard. You can return values, expose fields, or pass mocks around — but sometimes the cleanest\nsolution is to log the value and verify it in the test. JMina makes that practical:\n\n- **No production code changes.** Use your existing SLF4J log calls as test observation points. No injected mocks, no\n  extra return values, no refactoring.\n- **Works alongside your real logger.** Configure a delegate SLF4J provider and JMina passes every log call through to\n  it, so logging still works normally during tests.\n- **Fails at the right place.** When a forbidden log is triggered, the test fails immediately with a stack trace\n  pointing into your code — not at a generic assertion at the end of the test.\n\n## How It Works\n\nJMina registers itself as an SLF4J service provider. When your code calls `LoggerFactory.getLogger(...)`, SLF4J routes\nthrough JMina. On every log call, JMina:\n\n1. Matches the call against the conditions you registered with `on(...)`.\n2. Runs your verification lambda (or throws immediately if `.exception()` was used).\n3. Optionally forwards the call to a delegate logger (e.g., Logback, SLF4J Simple).\n\nBecause JMina sits at the SLF4J layer, no instrumentation or bytecode manipulation is needed.\n\n## Setup\n\nJMina is published to Maven Central.\n\n### Gradle\n\n```kotlin\ndependencies {\n    testImplementation(\"dev.jmina:jmina:0.1.4\")\n}\n```\n\n### Maven\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003edev.jmina\u003c/groupId\u003e\n    \u003cartifactId\u003ejmina\u003c/artifactId\u003e\n    \u003cversion\u003e0.1.4\u003c/version\u003e\n    \u003cscope\u003etest\u003c/scope\u003e\n\u003c/dependency\u003e\n```\n\n### System Properties\n\nConfigure JMina by setting system properties for the test task.\n\n| System Property           | Value                                                         | Description                                                       |\n|---------------------------|---------------------------------------------------------------|-------------------------------------------------------------------|\n| `slf4j.provider`          | `dev.jmina.log.MinaServiceProvider`                           | Use JMina as the SLF4J provider. **Mandatory.**                   |\n| `jmina.delegate.provider` | An SLF4J service provider class name of your logging library  | Forward log calls to a real logger alongside JMina interception   |\n| `jmina.context.global`    | `true` or `false`                                             | Use global context store or thread-local (see below)              |\n\nGradle example:\n\n```kotlin\ntasks {\n    test {\n        useJUnitPlatform()\n        systemProperty(\"slf4j.provider\", \"dev.jmina.log.MinaServiceProvider\")\n        systemProperty(\"jmina.delegate.provider\", \"ch.qos.logback.classic.spi.LogbackServiceProvider\")\n        systemProperty(\"jmina.context.global\", \"false\")\n    }\n}\n```\n\n### Global vs Thread-Local Context\n\nJMina manages test conditions in a *context store*. There are two modes:\n\n**Thread-local context** (`jmina.context.global=false`) — Each thread gets its own isolated context. Conditions\nregistered on one thread are invisible to others. This is the right choice when:\n- Your production code runs on a single thread, and\n- Tests run in parallel (each test thread stays isolated).\n\n**Global context** (`jmina.context.global=true`) — All threads share one context. Conditions registered on the test\nthread are visible to any thread that calls the logger. Use this when:\n- Your production code spawns its own threads (the log call happens on a different thread than the test), and\n- Tests run sequentially (parallel tests would collide on the shared context).\n\n## Quick Example\n\nSay you're testing a class that solves a quadratic equation and you want to verify the discriminant computed inside the\nmethod. Add a log call at that point in your production code:\n\n```java\npublic class QuadraticEquation {\n    private final Logger log = LoggerFactory.getLogger(QuadraticEquation.class);\n\n    public List\u003cDouble\u003e solve(double a, double b, double c) {\n        double discriminant = b * b - 4 * a * c;\n\n        log.debug(\"discriminant: {}\", discriminant);    // observation point\n\n        if (discriminant \u003c 0) {\n            return Collections.emptyList();\n        } else {\n            List\u003cDouble\u003e roots = new ArrayList\u003c\u003e();\n\n            if (discriminant \u003e 0) {\n                roots.add((-b - sqrt(discriminant)) / (2 * a));\n                roots.add((-b + sqrt(discriminant)) / (2 * a));\n            } else {\n                roots.add(-b / (2 * a));\n            }\n\n            return roots;\n        }\n    }\n}\n```\n[QuadraticEquation.java](src/test/java/dev/jmina/example/QuadraticEquation.java)\n\nThen verify the internal value in your test:\n\n```java\npublic class QuadraticEquationTest {\n    @Test\n    public void testSolve() {\n        // Register a check before running the code under test\n        Mina.on(QuadraticEquation.class, DEBUG, \"discriminant: {}\")\n                .check((Double discriminant) -\u003e assertEquals(9.0, discriminant));\n\n        List\u003cDouble\u003e roots = new QuadraticEquation().solve(1, -1, -2);\n\n        // Assert that all registered conditions were actually triggered\n        Mina.assertAllCalled();\n\n        assertEquals(-1.0, roots.get(0));\n        assertEquals(2.0, roots.get(1));\n    }\n\n    @AfterEach\n    public void clean() {\n        Mina.clean();  // Always clean up the context after each test\n    }\n}\n```\n[QuadraticEquationTest.java](src/test/java/dev/jmina/example/QuadraticEquationTest.java)\n\n## API Overview\n\n### Conditions\n\n`Mina.on(...)` filters which log calls to intercept. All parameters are optional — omit any to make it a wildcard for\nthat field.\n\n```java\n// Specific: class + level + message pattern\nMina.on(MyClass.class, DEBUG, \"value: {}\").check(...);\n\n// By class and level only\nMina.on(MyClass.class, WARN).check(...);\n\n// By level only — matches any logger at that level\nMina.on(ERROR).check(...);\n\n// By message pattern only — matches any logger, any level\nMina.on(\"value: {}\").check(...);\n\n// No filter — matches every log call\nMina.on().check(...);\n```\n\nYou can also filter by logger name string or SLF4J `Marker` instead of a class.\n\n### Checks\n\nAfter `on(...)`, call one of the `check` methods to define what to verify when the condition matches:\n\n```java\n// Just assert the log was called (use with Mina.assertAllCalled())\n.check()\n\n// Assert argument values with equals()\n.check(\"expected\", 42)\n\n// Lambda with typed arguments (1–6 arguments supported; last can be Throwable)\n.check((String msg) -\u003e assertTrue(msg.startsWith(\"OK\")))\n.check((String msg, Throwable t) -\u003e assertNotNull(t))\n\n// No-argument lambda — run any assertion\n.check(() -\u003e assertTrue(someCondition))\n\n// Verify only the throwable\n.checkThrowable(t -\u003e assertInstanceOf(IOException.class, t))\n```\n\n### Forbidden Logs\n\nUse `.exception()` to assert that a log call must **not** happen. If the condition matches, JMina throws immediately —\nfailing the test with a stack trace pointing to the exact log statement inside your code.\n\n```java\n// Fail the test the moment this error log is triggered\nMina.on(PaymentService.class, ERROR, \"Payment failed {}\").exception();\n\npaymentService.process(validPayment);  // this must not reach the error branch\n```\n\n## Comparison with Alternatives\n\n| Approach                  | Production code change | Typed arguments    | Any SLF4J backend  | Fails at error point |\n|---------------------------|------------------------|--------------------|--------------------|----------------------|\n| **JMina**                 | None                   | Yes                | Yes                | Yes                  |\n| Mockito (mock logger)     | Inject mock logger     | No (string verify) | N/A                | No                   |\n| Logback `ListAppender`    | None                   | No (strings only)  | Logback only       | No                   |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkefirfromperm%2Fjmina","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkefirfromperm%2Fjmina","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkefirfromperm%2Fjmina/lists"}