简体   繁体   中英

In Java with PowerMocktio, how do you mock the constructor of a class when it's instantiated inside a private nested class?

I have a class which contains several methods I'd like to test, as well as a private nested class that is used in a few of these methods. Inside this private nested class it creates an object that attempts to form a connection to either a website or a database. I would like to seperate the tests for connecting to this outside resource and for the processing that happens to the retrieved information. This way we can choose to not have the test environment connected to a functional 'outside' source, greatly simplifying the setup we need for these tests.

To that end I am writing a test which mocks the constructor for the object that attempts to form these connections. I don't want it to do anything when the nested private class attempts to form the connection, and when it tries to retrieve information I want it to just return a predefined string of data. At the moment I have something that looks similar to this:

public class MyClass {

    public int mainMethod() {
        //Some instructions...

        NestedClass nestedClass = new NestedClass();
        int finalResult = nestedClass.collectAndRefineData();
    }

    private class NestedClass {

        public NestedClass() {
            Connector connect = new Connector();
        }

        public int collectAndRefineData() {
            //Connects to the outside resource, like a website or database

            //Processes and refines data into a state I want

            //Returns data
        }
}

The test class looks something like this:

@RunWith(PowerMockRunner.class)
@PrepareForTest({Connector.class})
public class MyClassTests {

    @Test
    public void testOne() {
        mockConnector = mock(Connection.class);
        PowerMockito.whenNew(Connector.class).withNoArguments().thenReturn(mockConnector);

        MyClass testClass = new MyClass();
        int result = testClass.mainMethod();

        Assert.equals(result, 1);
    }
}

Now, I do know that inside the PrepareForTest annotation that I need to include the class that instantiates the object that I'm mocking the constructor for. The problem is that I can't put MyClass, because that's not the object that creates it, and I can't put NestedClass, because it can't be seen by the test. I have tried putting MyClass.class.getDeclaredClasses[1] to retrieve the correct class, but unfortunately PowerMocktio requires a constant to be in the annotation and this simply will not work.

Can anyone think of a way to get this mock constructor to work?


Note: I am unable to make any alterations to the code I am testing. This is because the code does work at the moment, it has been manually tested, I am writing this code so that future projects will have this automated testing framework to use.

I'm not sure if you are running your test integrated with Mockito, if you do then you can use this code:

@RunWith(PowerMockRunner.class)
@PrepareForTest({Connector.class})
public class MyClassTests {

    @Mock
    Connector mockConnector;

    @InjectMocks
    MyClass testClass;

    @Test
    public void testOne() {
        PowerMockito.whenNew(Connector.class).withNoArguments().thenReturn(mockConnector);

        int result = testClass.mainMethod();

        Assert.equals(result, 1);
    }
}

I suspect you will have to modify the code under test. You have two options:

  1. Write a large integration test that covers this code so you have a safety net just in case your changes might break something. This test could possibly create an in-memory database/backend and have the connector connect to that.
  2. Make a small, safe change that creates a seam for testing

It's preferable to do both. See the book Working Effectively with Legacy Code for more details.

I'll show an example of option 2.

First, you can create a "seam" that allows the test to be able to change the way the Connector is created:

public class MyClass {

    public int mainMethod() {
        // Some instructions...

        NestedClass nestedClass = new NestedClass();
        return nestedClass.CollectAndRefineData();
    }

    // visible for testing
    Connector createConnector() {
        return new Connector();
    }

    private class NestedClass {
        private final Connector connector;

        public NestedClass() {
            connector = createConnector();
        }

        ...
    }
}

You can then use a partial mock of MyClass to test your code.

@RunWith(JUnit4.class)
public class MyClassTests {

    @Test
    public void testOne() {
        MyClass testClass = spy(MyClass.class);
        Connector mockConnector = mock(Connector.class);
        when(testClass.createConnection())
            .thenReturn(mockConnector);

        int result = testClass.mainMethod();

        Assert.assertEquals(1, result);
    }
}

Note that assertEquals expects the first parameter to be the expected value.

Also note that this test uses Mockito, not PowerMock . This is good because tests that use PowerMock may be brittle and subject to breakage with small changes to the code under test. Be warned that using partial mocks can be brittle too. We will fix that soon.

After you get the tests passing, you can refactor the code so the caller passes in a Connector factory:

public class MyClass {
    private final ConnectorFactory connectorFactory;

    @Inject
    MyClass(ConnectorFactory factory) {
        this.connectorFactory = factory;
    }

    // visible for testing
    Connector createConnector() {
        return connectorFactory.create();
    }

    private class NestedClass {
        private final Connector connector;

        public NestedClass() {
            connector = createConnector();
        }

        ...
    }
}

The code using MyClass would preferably use a dependency injection framework like Guice or Spring. If that isn't an option, you can make a second no-arg constructor that passes in a real ConnectorFactory .

Assuming the tests still pass, you can make the test less brittle by changing your test to mock ConnectorFactory instead of doing a partial mock of MyClass . When those tests pass, you can inline createConnector() .

In the future, try to write tests as you write your code (or at least before spending a lot of time on manual testing).

mockConnector = mock(Connection.class);

This object is not declared. Whatever, anyway you have to use the @Mock anotation.

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