在使用PowerMocktio的Java中,当在类私有嵌套类中实例化时,如何模拟类的构造函数?

时间:2021-12-26 05:05:37

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.

现在,我知道在PrepareForTest注释中我需要包含实例化我正在模拟构造函数的对象的类。问题是我不能放MyClass,因为那不是创建它的对象,我不能放置NestedClass,因为测试无法看到它。我已经尝试将MyClass.class.getDeclaredClasses [1]用于检索正确的类,但遗憾的是PowerMocktio需要一个常量才能在注释中,这根本不起作用。

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.

注意:我无法对我正在测试的代码进行任何更改。这是因为代码现在可以正常工作,它已经过手动测试,我正在编写这段代码,以便将来的项目可以使用这个自动化测试框架。

3 个解决方案

#1


0  

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

我不确定您是否正在运行与Mockito集成的测试,如果您这样做,则可以使用此代码:

@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);
    }
}

#2


0  

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. 编写一个涵盖此代码的大型集成测试,以便您拥有安全网,以防万一您的更改可能会破坏某些内容。此测试可能会创建内存数据库/后端,并使连接器连接到该数据库/后端。

  3. Make a small, safe change that creates a seam for testing
  4. 做一个小的,安全的更改,创建一个接缝进行测试

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.

我将展示选项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.

然后,您可以使用MyClass的部分模拟来测试您的代码。

@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.

请注意,assertEquals期望第一个参数是期望值。

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.

另请注意,此测试使用的是Mockito,而不是PowerMock。这很好,因为使用PowerMock的测试可能很脆弱,并且在测试代码的微小变化下会出现破坏。请注意,使用部分嘲笑也可能很脆弱。我们很快就会解决这个问题

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

通过测试后,您可以重构代码,以便调用者传入Connector工厂:

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.

使用MyClass的代码最好使用像Guice或Spring这样的依赖注入框架。如果这不是一个选项,你可以创建一个传入一个真正的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().

假设测试仍然通过,您可以通过将测试更改为模拟ConnectorFactory而不是对MyClass进行部分模拟来使测试不那么脆弱。当这些测试通过时,您可以内联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).

在将来,尝试在编写代码时编写测试(或者至少在花费大量时间进行手动测试之前)。

#3


-1  

mockConnector = mock(Connection.class);

mockConnector = mock(Connection.class);

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

未声明此对象。无论如何,无论如何你必须使用@Mock anotation。

#1


0  

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

我不确定您是否正在运行与Mockito集成的测试,如果您这样做,则可以使用此代码:

@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);
    }
}

#2


0  

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. 编写一个涵盖此代码的大型集成测试,以便您拥有安全网,以防万一您的更改可能会破坏某些内容。此测试可能会创建内存数据库/后端,并使连接器连接到该数据库/后端。

  3. Make a small, safe change that creates a seam for testing
  4. 做一个小的,安全的更改,创建一个接缝进行测试

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.

我将展示选项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.

然后,您可以使用MyClass的部分模拟来测试您的代码。

@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.

请注意,assertEquals期望第一个参数是期望值。

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.

另请注意,此测试使用的是Mockito,而不是PowerMock。这很好,因为使用PowerMock的测试可能很脆弱,并且在测试代码的微小变化下会出现破坏。请注意,使用部分嘲笑也可能很脆弱。我们很快就会解决这个问题

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

通过测试后,您可以重构代码,以便调用者传入Connector工厂:

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.

使用MyClass的代码最好使用像Guice或Spring这样的依赖注入框架。如果这不是一个选项,你可以创建一个传入一个真正的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().

假设测试仍然通过,您可以通过将测试更改为模拟ConnectorFactory而不是对MyClass进行部分模拟来使测试不那么脆弱。当这些测试通过时,您可以内联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).

在将来,尝试在编写代码时编写测试(或者至少在花费大量时间进行手动测试之前)。

#3


-1  

mockConnector = mock(Connection.class);

mockConnector = mock(Connection.class);

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

未声明此对象。无论如何,无论如何你必须使用@Mock anotation。