Repository: avaje/avaje-inject (java)

Issue:
@InjectTest does not rewire static fields or instance fields for each Test Class resp. Test Method
Minimal reproducer (tested using avaje-inject 11.5 and 11.6.-RC5):
```
package main;

import io.avaje.inject.BeanScope;
import jakarta.inject.Singleton;

public class Main {

  @Singleton
  public static class SingletonGreeter {

    public String greet() {
      return "Hello from SingletonGreeter";
    }
  }

  public static void main(String[] args) {
    try(BeanScope beanScope = BeanScope.builder().build()) {
      SingletonGreeter singletonGreeter = beanScope.get(SingletonGreeter.class);
      System.out.println(singletonGreeter.greet());
    }
  }
}
```

Test using `@InjectTest`:
```
package main;

import main.Main.SingletonGreeter;
import io.avaje.inject.test.InjectTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;

@InjectTest
class MainTest {
  @Inject SingletonGreeter sg;

  @Test
  void one() {
    System.out.println(sg.toString());
  }

  @Test
  void two() {
    System.out.println(sg.toString());
  }
}
```

```
Jul 14, 2025 1:38:13 PM io.avaje.inject.spi.DBeanScope start
INFO: Wired beans in 11ms
main.Main$SingletonGreeter@3541cb24
main.Main$SingletonGreeter@3541cb24
```

Both test methods return the same instance of SingletonGreeter. I expect the SingletonGreeter to be rewired between the two test methods calls, returning different instances in each test method.

Programmatic Approach works:
```
package main;

import main.Main.SingletonGreeter;
import io.avaje.inject.BeanScope;
import io.avaje.inject.test.TestBeanScope;
import jakarta.inject.Inject;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class MainProgrammaticalTest {
  @Inject SingletonGreeter sg;
  BeanScope instanceScope;

  @BeforeEach
  void beforeEach() {
    instanceScope = TestBeanScope.builder()
        .forTesting()
        .build();
    sg = instanceScope.get(SingletonGreeter.class);
  }

  @AfterEach
  void afterEach() {
    instanceScope.close();
  }


  @Test
  void one() {
    System.out.println(sg.toString());
  }

  @Test
  void two() {
    System.out.println(sg.toString());
  }
}
```

```
Jul 14, 2025 1:53:40 PM io.avaje.inject.spi.DBeanScope start
INFO: Wired beans in 13ms
Jul 14, 2025 1:53:40 PM io.avaje.inject.spi.DBeanScope start
INFO: Wired beans in 2ms
main.Main$SingletonGreeter@672872e1
Jul 14, 2025 1:53:40 PM io.avaje.inject.spi.DBeanScope start
INFO: Wired beans in 2ms
main.Main$SingletonGreeter@f381794
```

Both test methods return different instances of SingletonGreeter, as expected.

The same is true for static variables annotated with `@Inject`: They do not get rewired between the test classes, all test classes get the same global instance.

I expected the `@InjectTest` method to work the same way, as described in https://avaje.io/inject/#test-static .
Am i doing something wrong? 
Thank you very much.

Pull request:
[test] Add a switch to create a scope per test
resolves #858, though I'm not really sold on the name of the flag. Please suggest a better name if you think of one

Patch:
diff --git "a/inject-test/src/test/java/org/example/injectextension/\306\216NA.java" "b/inject-test/src/test/java/org/example/injectextension/\306\216NA.java"
new file mode 100644
index 00000000..e2d66c68
--- /dev/null
+++ "b/inject-test/src/test/java/org/example/injectextension/\306\216NA.java"
@@ -0,0 +1,13 @@
+package org.example.injectextension;
+
+import jakarta.inject.Singleton;
+
+@Singleton
+public class ƎNA {
+
+  public String greet() {
+    return "Ah, these are the most utmost grand of days";
+  }
+
+  public void giveGift() {}
+}


Test patch:
diff --git a/inject-test/src/main/java/io/avaje/inject/test/InjectTest.java b/inject-test/src/main/java/io/avaje/inject/test/InjectTest.java
index 72603759..6b8786e9 100644
--- a/inject-test/src/main/java/io/avaje/inject/test/InjectTest.java
+++ b/inject-test/src/main/java/io/avaje/inject/test/InjectTest.java
@@ -1,12 +1,12 @@
 package io.avaje.inject.test;
 
-import org.junit.jupiter.api.extension.ExtendWith;
-
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
+import org.junit.jupiter.api.extension.ExtendWith;
+
 /**
  * An avaje-inject test supporting {@code @Inject} along with Mockito annotations -
  * {@code @Mock, @Spy, @Captor}.
@@ -20,4 +20,7 @@ public @interface InjectTest {
 
   /** Wiring profiles to use */
   String[] profiles() default {};
+
+  /** Create a new test beanscope for each test methods */
+  boolean scopePerMethod() default false;
 }
diff --git a/inject-test/src/main/java/io/avaje/inject/test/MetaInfo.java b/inject-test/src/main/java/io/avaje/inject/test/MetaInfo.java
index 7c6610a1..62f0b6de 100644
--- a/inject-test/src/main/java/io/avaje/inject/test/MetaInfo.java
+++ b/inject-test/src/main/java/io/avaje/inject/test/MetaInfo.java
@@ -1,10 +1,10 @@
 package io.avaje.inject.test;
 
+import java.util.Optional;
+
 import io.avaje.inject.BeanScope;
 import io.avaje.inject.BeanScopeBuilder;
 
-import java.util.Optional;
-
 /**
  * Wraps the underlying metadata (fields with annotations @Mock, @Spy, @Inject, @Captor).
  */
@@ -45,14 +45,17 @@ final class MetaInfo {
   }
 
   private TestBeans buildTestBeans(GlobalTestBeans.Beans parent, Object testInstance) {
+    var injectTest =
+        Optional.ofNullable(testInstance)
+            .map(Object::getClass)
+            .map(c -> c.getAnnotation(InjectTest.class));
+
     // wiring profiles
-    String[] profiles = Optional.ofNullable(testInstance)
-      .map(Object::getClass)
-      .map(c -> c.getAnnotation(InjectTest.class))
-      .map(InjectTest::profiles)
-      .orElse(new String[0]);
+    String[] profiles = injectTest.map(InjectTest::profiles).orElse(new String[0]);
 
-    if (profiles.length > 0 || reader.hasMocksOrSpies(testInstance)) {
+    if (profiles.length > 0
+        || injectTest.map(InjectTest::scopePerMethod).orElse(false)
+        || reader.hasMocksOrSpies(testInstance)) {
       // need to build a BeanScope for this using baseBeans() as the parent
       final BeanScopeBuilder builder = BeanScope.builder();
       if (parent != null) {
diff --git a/inject-test/src/test/java/org/example/injectextension/ScopePerMethodTest.java b/inject-test/src/test/java/org/example/injectextension/ScopePerMethodTest.java
new file mode 100644
index 00000000..0890d179
--- /dev/null
+++ b/inject-test/src/test/java/org/example/injectextension/ScopePerMethodTest.java
@@ -0,0 +1,26 @@
+package org.example.injectextension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+import io.avaje.inject.test.InjectTest;
+import jakarta.inject.Inject;
+
+@InjectTest(scopePerMethod = true)
+class ScopePerMethodTest {
+  @Inject ƎNA ena;
+  static ƎNA ayna;
+
+  @Test
+  void one() {
+    assertThat(ena).isNotSameAs(ayna);
+    ayna = ena;
+  }
+
+  @Test
+  void two() {
+    assertThat(ena).isNotSameAs(ayna);
+    ayna = ena;
+  }
+}