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