Spring bean scope for "one object per test method"

oberlies :

I have a test utility for with I need to have a fresh instance per test method (to prevent that state leaks between tests). So far, I was using the scope "prototype", but now I want to be able to wire the utility into another test utility, and the wired instances shall be the same per test.

This appears to be a standard problem, so I was wondering if there is a "test method" scope or something similar?

This is the structure of the test class and test utilities:

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyTest {

    @Autowired
    private TestDriver driver;

    @Autowired
    private TestStateProvider state;

    // ... state
    // ... methods
}
@Component
@Scope("prototype") // not right because MyTest and TestStateProvider get separate instances
public class TestDriver {
    // ...
}
@Component
public class TestStateProvider {

    @Autowired
    private TestDriver driver;

    // ...
}

I'm aware that I could use @Scope("singleton") and @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) but this refreshes more than I need – a new TestDriver instance for each test would be enough. Also, this approach is error-prone because all tests using the TestDriver would need to know that they also need the @DirtiesContext annotation. So I'm looking for a better solution.

oberlies :

It is actually pretty easy to implement a testMethod scope:

public class TestMethodScope implements Scope {
    public static final String NAME = "testMethod";

    private Map<String, Object> scopedObjects = new HashMap<>();
    private Map<String, Runnable> destructionCallbacks = new HashMap<>();

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        if (!scopedObjects.containsKey(name)) {
            scopedObjects.put(name, objectFactory.getObject());
        }
        return scopedObjects.get(name);
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
        destructionCallbacks.put(name, callback);
    }

    @Override
    public Object remove(String name) {
        throw new UnsupportedOperationException();
    }

    @Override
    public String getConversationId() {
        return null;
    }

    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }

    public static class TestExecutionListener implements org.springframework.test.context.TestExecutionListener {

        @Override
        public void afterTestMethod(TestContext testContext) throws Exception {
            ConfigurableApplicationContext applicationContext = (ConfigurableApplicationContext) testContext
                    .getApplicationContext();
            TestMethodScope scope = (TestMethodScope) applicationContext.getBeanFactory().getRegisteredScope(NAME);

            scope.destructionCallbacks.values().forEach(callback -> callback.run());

            scope.destructionCallbacks.clear();
            scope.scopedObjects.clear();
        }
    }

    @Component
    public static class ScopeRegistration implements BeanFactoryPostProcessor {

        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException {
            factory.registerScope(NAME, new TestMethodScope());
        }
    }

}

Just register the test execution listener, and there will be one instance per test of all @Scope("testMethod") annotated types:

@RunWith(SpringRunner.class)
@SpringBootTest
@TestExecutionListeners(listeners = TestMethodScope.TestExecutionListener.class, 
        mergeMode = MergeMode.MERGE_WITH_DEFAULTS)
public class MyTest {

    @Autowired
    // ... types annotated with @Scope("testMethod")

}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=163272&siteId=1
Recommended