Java中的单元测试,使用Powermock,如何模拟/窥探xml解析器类的私有方法

nhaq1z21  于 2023-05-27  发布在  Java
关注(0)|答案(1)|浏览(134)

我想写一个调用另一个私有函数的函数的单元测试。这个私有函数负责从磁盘阅读XML文件。但是,我希望在单元测试期间避免对真实的文件的依赖。相反,我想使用存储在String变量中的XML内容。
尽管成功地模拟了ScenarioParser,但由于内部变量DocumentBuilder和DocumentFactory,我遇到了一个错误。因此,我正在考虑相应地扩展和修改单元测试来解决这个问题。
我使用PowerMockito和JUnit 4。
正在测试的类:

private static final Logger LOGGER = LoggerFactory.getLogger(ScenarioParser.class);
    
    private final String scenarioFile;
    
    /**
     * Constructs a ScenarioParser with the specified scenario file path.
     *
     * @param scenarioFile the path to the scenario XML file
     */
    public ScenarioParser(String scenarioFile) {
        LOGGER.debug("ScenarioParser Ctor");
        this.scenarioFile = scenarioFile;
    }
    
    /**
     * Parses the scenario XML file and returns a list of Scenario objects.
     *
     * @return the list of parsed Scenario objects
     */
    public List<Scenario> parse() {
        List<Scenario> scenarios = new ArrayList<>();
    
        try(InputStream inputStream = getResourceAsStream(scenarioFile))  
        {
            LOGGER.trace("getResourceAsStream is working for {0}", scenarioFile);
            if (inputStream != null) {
                Document document = parseDocument(inputStream);
                if (document != null) {
                    scenarios = parseScenarios(document);
                }
            } else {
                LOGGER.error("Scenario XML file {} is not found", scenarioFile);
            }
        } catch (IOException e) {
            LOGGER.error("Failed to read the scenario XML file: {}", e.getMessage());
        }
    
        return scenarios;
    }
    
    /**
     * Retrieves the input stream for the scenario XML file.
     *
     * @param scenarioFile the path to the scenario XML file
     * @return the input stream for the scenario XML file
     */
    private InputStream getResourceAsStream(String resourcePath) {
        return Thread.currentThread().getContextClassLoader().getResourceAsStream(resourcePath);
    }
    
    /**
     * Parses the XML document from the input stream.
     *
     * @param inputStream the input stream of the XML file
     * @return the parsed Document object
     */
    private Document parseDocument(InputStream inputStream) {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        try {
            DocumentBuilder builder = factory.newDocumentBuilder();
            return builder.parse(inputStream);
        } catch (ParserConfigurationException | SAXException | IOException e) {
            LOGGER.error("Failed to parse the scenario XML file: {}", e.getMessage());
        }
        return null;
    }
    
    /**
     * Parses the Document and returns a list of Scenario objects.
     *
     * @param document the scenario element
     * @return the list of parsed Scenario objects
     */
    private List<Scenario> parseScenarios(Document document) {
        List<Scenario> scenarios = new ArrayList<>();
        Element rootElement = document.getDocumentElement();
        NodeList scenarioNodes = rootElement.getElementsByTagName("scenario");
    
        for (int i = 0; i < scenarioNodes.getLength(); i++) {
            Node scenarioNode = scenarioNodes.item(i);
            if (scenarioNode.getNodeType() == Node.ELEMENT_NODE) {
                Element scenarioElement = (Element) scenarioNode;
                String name = scenarioElement.getAttribute("name");
                //...
            }
        }
    
        return scenarios;
    }
    
    /**
     * Parses the step elements within a scenario element and creates ScenarioStep
     * objects.
     *
     * @param scenarioElement the scenario element
     * @return the list of parsed ScenarioStep objects
     */
    private List<ScenarioStep> parseScenarioSteps(Element scenarioElement) {
        List<ScenarioStep> steps = new ArrayList<>();
        NodeList stepNodes = scenarioElement.getElementsByTagName("step");
    
        for (int j = 0; j < stepNodes.getLength(); j++) {
            Node stepNode = stepNodes.item(j);
    
            if (stepNode.getNodeType() == Node.ELEMENT_NODE) {
                Element stepElement = (Element) stepNode;
                //..
                steps.add(step);
            }
        }
    
        return steps;
    }
    
    /**
     * Parses the parameter elements within a step element and creates a map of
     * parameter key-value pairs.
     *
     * @param stepElement the step element
     * @return the map of parsed parameter key-value pairs
     */
    private Map<String, String> parseStepParameters(Element stepElement) {
        Map<String, String> parameters = new HashMap<>();
        NodeList parameterNodes = stepElement.getElementsByTagName("parameter");
    
        for (int k = 0; k < parameterNodes.getLength(); k++) {
            Node parameterNode = parameterNodes.item(k);
    
            if (parameterNode.getNodeType() == Node.ELEMENT_NODE) {
                Element parameterElement = (Element) parameterNode;
                String key = parameterElement.getAttribute("key");
                String value = parameterElement.getAttribute("value");
                parameters.put(key, value);
            }
        }
    
        return parameters;
     } 
}

以下是我的测试类:

@RunWith(PowerMockRunner.class)
@PowerMockIgnore({ "com.sun.org.apache.xerces.", "javax.xml.", "org.xml.",   "javax.management."})@PrepareForTest({ ScenarioParser.class, DocumentBuilderFactory.class, DocumentBuilder.class })public class ScenarioParserTest {

    @Mock
    private InputStream mockInputStream;
    
    @Spy
    private ScenarioParser scenarioParser = new ScenarioParser("sample.xml"); 
    
    @Before
    public void setup() throws Exception {
        MockitoAnnotations.initMocks(this);
       
    }
    
    @Test
    public void testParse_ValidInput_ReturnsScenarios() throws Exception {
        // Mock the input stream with a sample XML content
        String xmlContent = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
                "<scenarios>\n" +
                "     <scenario name=\"Scenario 1\" duration=\"10\" interval=\"2\">\n" +
                "       <step action=\"Action 1\">\n" +
                "            <parameter key=\"Param1\" value=\"Value1\" />\n" +
                "            <parameter key=\"Param2\" value=\"Value2\" />\n" +
                "        </step>\n" +
                "        <step action=\"Action 2\">\n" +
                "            <parameter key=\"Param3\" value=\"Value3\" />\n" +
                "        </step>\n" +
                "    </scenario>\n" +
                "    <scenario name=\"Scenario 2\" duration=\"20\" interval=\"3\">\n" +
                "        <step action=\"Action 3\">\n" +
                "            <parameter key=\"Param4\" value=\"Value4\" />\n" +
                "            <parameter key=\"Param5\" value=\"Value5\" />\n" +
                "            <parameter key=\"Param6\" value=\"Value6\" />\n" +
                "        </step>\n" +
                "        <step action=\"Action 4\" />\n" +
                "    </scenario>\n" +
                "    <scenario name=\"Scenario 3\" duration=\"30\" interval=\"4\">\n" +
                "        <step action=\"Action 5\">\n" +
                "            <parameter key=\"Param7\" value=\"Value7\" />\n" +
                "        </step>\n" +
                "    </scenario>\n" +
                "</scenarios>";
    
        // Mock DocumentBuilderFactory and DocumentBuilder
        //DocumentBuilderFactory documentBuilderFactoryMock =    
        PowerMockito.mock(DocumentBuilderFactory.class);
        //DocumentBuilder documentBuilderMock = PowerMockito.mock(DocumentBuilder.class); 
       
        //PowerMockito.whenNew(
        //DocumentBuilderFactory.class).withNoArguments().thenReturn(documentBuilderF
        //actoryMock);
    
        //PowerMockito.when(
        //documentBuilderFactoryMock.newDocumentBuilder()).thenReturn(documentBuilderMock);
    
        // Mock Document
        //Document documentMock = PowerMockito.mock(Document.class);
        //PowerMockito.
        //when(documentBuilderMock.parse(Mockito.any(InputStream.class))).
        //thenReturn(documentMock);
        // Mock the scenarioElement, scenarioNode, and other necessary objects for testing
    
        // Mock DocumentBuilderFactory and DocumentBuilder
        when(mockInputStream.read(Mockito.any(byte[].class), Mockito.anyInt(), Mockito.anyInt()))
                .thenAnswer(invocation -> {
                    byte[] buffer = invocation.getArgument(0);
                    byte[] contentBytes = xmlContent.getBytes();
                    int len = Math.min(buffer.length, contentBytes.length);
                    System.arraycopy(contentBytes, 0, buffer, 0, len);
                    return len;
                });
    
        // Create the scenario parser and parse the scenarios
        PowerMockito.when(scenarioParser, PowerMockito.method(ScenarioParser.class,     
        "getResourceAsStream", String.class)).withArguments(Mockito.anyString())
                .thenReturn(mockInputStream);
    
                
        //when(scenarioParser, "getResourceAsStream",    
        ArgumentMatchers.anyString()).thenReturn(mockInputStream);
        List<Scenario> scenarios = scenarioParser.parse();
    
        // Verify the parsed scenarios
        assertEquals(2, scenarios.size());
    
        Scenario scenario1 = scenarios.get(0);
        assertEquals("Scenario 1", scenario1.getName());
        assertEquals(10, scenario1.getDuration());
        assertEquals(2, scenario1.getInterval());
    
        List<ScenarioStep> steps1 = scenario1.getSteps();
        assertEquals(2, steps1.size());
        assertEquals("Action 1", steps1.get(0).getAction());
        assertEquals("Value1", steps1.get(1).getParameter("Param1"));
    
        Scenario scenario2 = scenarios.get(1);
        assertEquals("Scenario 2", scenario2.getName());
        assertEquals(20, scenario2.getDuration());
        assertEquals(3, scenario2.getInterval());
    
        List<ScenarioStep> steps2 = scenario2.getSteps();
        assertEquals(3, steps2.size());
        assertEquals("Action 3", steps2.get(0).getAction());
        assertEquals("Value4", steps2.get(1).getParameter("Param4"));
        assertEquals("Value5", steps2.get(2).getParameter("Param5"));
    }

}

这是pom.xml文件的一部分

<?xml version="1.0" encoding="UTF-8"?>...<artifactId>maven-surefire-plugin</artifactId><version>2.22.1</version><configuration><argLine>--illegal-access=permit--add-opens java.base/java.lang=ALL-UNNAMED--add-opens java.base/java.util=ALL-UNNAMED--add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED -Dillegal-access=permit
        --add-exports org.junit.platform.commons/org.junit.platform.commons.util=ALL-UNNAMED
        --add-exports org.junit.platform.commons/org.junit.platform.commons.logging=ALL-UNNAMED
        </argLine>
      </configuration>
    </plugin>
    ...

在执行测试时,无论是否使用Maven目标-Dmaven.surefire.debug test,我都会遇到以下错误:
java.lang.VerifyError:操作数堆栈上的类型错误Exception Details:Location:javax/xml/parsers/DocumentBuilderFactory.newDefaultNSInstance()Ljavax/xml/parsers/DocumentBuilderFactory;@119:invokestaticReason:Type 'com/sun/org/apache/xerces/internal/jaxp/DocumentBuilderFactoryImpl'(current frame,stack[0])is not assignable to 'javax/xml/parsers/DocumentBuilderFactory'...
如何解决这个问题!

r1wp621o

r1wp621o1#

你的代码已经使用了静态访问,因此与java运行时和第三方库中的各种类紧密耦合。您在创建模拟时遇到的困难是糟糕设计的明显标志,而使用PowerMock就是向它投降。
如果你不同意,请停止阅读。
要做的是将依赖项的示例化(instantiation of dependencies)(你想要模拟的对象)从你的业务逻辑中分离出来,并将它们注入到你的测试代码中(cut)。最简单的方法是将静态评估 Package 在 * 工厂类 * 中。这些类将 * 太简单而不会失败 *,所以它们不需要自己的单元测试。
如果调用的静态方法在你的cut中没有参数calculatest,你可以使用PowerMock之外的任何其他mocking框架直接模拟它的结果。
如果调用的静态方法确实有在cut中计算的参数,则需要为此工厂创建一个mock并注入它。当然,您需要配置工厂模拟以返回最终依赖项的模拟。

相关问题