简体   繁体   中英

SpringBoot - Mock stateful object created via “new” keyword in integration test

I have an SpringBoot application that consists of a Controller layer and a Service layer.

MyController has access to MyService via @Autowired , while MyService has a method that creates a new instance of MyClass , which is imported from an external dependency.

import externaldependency.MyClass;

@Service
public class MyService {

    public void myMethod() {

        MyClass c = new MyClass();
        c.doStuff();
        c.doOtherStuff();
        c.doMoreStuff();

    }
}

I use new to create the instance because MyClass holds state; it has several methods that change its state during the execution of myMethod until I get the desired result, therefore I shouldn't autowire it nor inject it in the constructor, since that would use a single instance of this class for every call to myMethod . I understand that "Prototype" beans exists, but as far as I know, even if I declare MyClass as a prototype bean and inject it to MyService via @Autowired , the service would still use the same instance of MyClass during execution, so ultimately I decided to just use new .

Recently I've been trying to do an integration test, calling my Controller layer, which in turn will call my Service layer, which in turn will create an instance of MyClass . The problem is that one of the many methods of MyClass internally calls an external service, which shouldn't be part of the test itself, so I would like to mock this class.

I understand that mocking is done via dependency injection, but in this case I can't do that. Is there an alternative way to mock MyClass , or is it simply not possible with this setup? If not, then how could I refactor my code to make mocking possible in this particular case?

Many thanks in advance.

I'll answer my own question.

Since MyClass holds state, it shouldn't be autowired to the service nor injected via its constructor, but rather new instances should be created as needed. However, whan can be autowired is a "factory" which creates these instances:

@Component
class MyClassFactory {

    public MyClass getInstance() {
        return new MyClass();
    }

}

Therefore, the service becomes:

@Service
public class MyService {

    @Autowired
    private MyClassFactory myClassFactory;

    public void myMethod() {

        // MyClass c = new MyClass();
        MyClass c = myClassFactory.getInstance();

        c.doStuff();
        c.doOtherStuff();
        c.doMoreStuff();

    }

}

In practice, using a factory is the same thing as just using new ; I'm getting a new instance either way. The benefit comes during testing; now I can mock what the factory returns, since the factory is part of Spring's application context:

@SpringBootTest
public class MyTest {

    @MockBean
    private MyClass myClassMock;

    @MockBean
    private MyClassFactory myClassFactoryMock;

    @Test
    public void myTests() {

        // Make a mock of MyClass, replacing the return
        // values of its methods as needed.

        given(
            myClassMock.doStuff()
        ).willReturn(
            "Something useful for testing"
        );

        // Then make a mock of the factory, so that it returns
        // the mock of the class instead of a real instance.

        given(
            myClassFactoryMock.getInstance()
        ).willReturn(
            myClassMock
        );

        // Do the tests as normal.

    }

}

Probably not the most elegant solution, but at least solved my current problem.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM