【问题标题】:How do you mock an output stream?你如何模拟输出流?
【发布时间】:2011-09-17 14:07:12
【问题描述】:

“输出流”是指接收字节序列或字符或其他任何内容的任何对象。所以,java.io.OutputStream,还有java.io.Writer、javax.xml.stream.XMLStreamWriter的writeCharacters方法等等。

我正在为一个类编写基于模拟的测试,该类的主要功能是将数据流写入其中之一(XMLStreamWriter,碰巧)。

问题在于数据流是在一系列对 write 方法的调用中写入的,但重要的不是调用,而是数据。例如,给定一个 XMLStreamWriter out,这些:

out.writeCharacters("Hello, ");
out.writeCharacters("world!");

等价于:

out.writeCharacters("Hello, world!");

发生什么(就我的目的而言)真的无关紧要。会有一些特定的调用序列,但我不在乎它是什么,所以我不想为那个特定序列写期望。我只是希望以任何方式写入某个数据流。

一种选择是切换到基于状态的测试。我可以将数据累积在缓冲区中,并对其进行断言。但是因为我正在编写 XML,这将意味着做出一些相当复杂和丑陋的断言。模拟似乎是处理更大的 XML 编写问题的更好方法。

那么我如何使用模拟来做到这一点?

我正在使用 Moxie 进行模拟,但我有兴趣了解任何模拟库的方法。

【问题讨论】:

    标签: java stream mocking outputstream


    【解决方案1】:

    测试输出或输入流的一个相当优雅的策略是使用 PipedInputStreamPipedOutputStream 类。您可以在测试设置中将它们连接在一起,然后在目标方法执行后检查已写入的内容。

    您可以从另一个方向开始准备一些输入,然后让测试也从输入流中读取这些准备好的数据。

    在您的情况下,您可以使用 PipedOutputStream 模拟该 "out" 变量,然后以这种方式将 PipedInputStream 插入其中:

    private BufferedReader reader;
    
    @Before
    public void init() throws IOException {
        PipedInputStream pipeInput = new PipedInputStream();
        reader = new BufferedReader(
                new InputStreamReader(pipeInput));
        BufferedOutputStream out = new BufferedOutputStream(
                new PipedOutputStream(pipeInput))));
        //Here you will have to mock the output somehow inside your 
        //target object.
        targetObject.setOutputStream (out);
        }
    
    
    @Test
    public test() {
        //Invoke the target method
        targetObject.targetMethod();
    
        //Check that the correct data has been written correctly in 
        //the output stream reading it from the plugged input stream
        Assert.assertEquals("something you expects", reader.readLine());
        }
    

    【讨论】:

    • 作为一种测试流的方法,是的,这很好(我认为比写入 ByteArrayOutputStream 并对此做出断言更好)。但是(a)在这种特殊情况下,out 是一个XMLStreamWriter 而不是一个流,并且(b)它不是模拟的,我认为不能很好地模拟。
    • 这很好用,但是在我的情况下,我必须先关闭 bufferedOutputStream,然后才能在阅读器上执行 readLine()。如果我不这样做,我的代码就会挂起。
    【解决方案2】:

    我承认我可能偏向于使用ByteArrayOutputStream 作为最低级别的输出流,在执行后获取数据并执行所需的任何断言。 (可能使用 SAX 或其他 XML 解析器读取数据并深入了解结构)

    如果你想用一个模拟来做到这一点,我承认我有点偏爱Mockito,我认为你可以使用自定义Answer 来完成你想要做的事情,当用户在您的模拟上调用 writeCharacters,只需将它们的参数附加到缓冲区,然后您可以在之后对其进行断言。

    这是我脑子里的东西(手写,还没有执行,所以语法问题是可以预料的:))

    public void myTest() {
        final XMLStreamWriter mockWriter = Mockito.mock(XMLStreamWriter.class);
        final StringBuffer buffer = new StringBuffer();
        Mockito.when(mockWriter.writeCharacters(Matchers.anyString())).thenAnswer(
            new Answer<Void>() {
                Void answer(InvocationOnMock invocation) {
                    buffer.append((String)invocation.getArguments()[0]);
                    return null;
                }
            });
        //... Inject the mock and do your test ...
        Assert.assertEquals("Hello, world!",buffer.toString());
    }    
    

    【讨论】:

      【解决方案3】:

      (免责声明:我是 Moxie 的作者。)

      我假设您想使用嵌入在模拟中的逻辑来执行此操作,以便违反您期望的调用快速失败。是的,这是可能的——但在我所知道的任何模拟库中都不是优雅/简单的。 (一般来说,模拟库擅长以隔离/顺序测试方法调用的行为,但在测试模拟生命周期内调用之间更复杂的交互时表现不佳。)在这种情况下,大多数人会建立一个缓冲区作为其他答案建议 - 虽然它不会很快失败,但测试代码更易于实现/理解。

      在当前版本的 Moxie 中,在模拟上添加自定义参数匹配行为意味着编写您自己的 Hamcrest 匹配器。 (JMock 2 和 Mockito 还允许您使用自定义 Hamcrest 匹配器;EasyMock 允许您指定扩展类似 IArgumentMatcher 接口的自定义匹配器。)

      您将需要一个自定义匹配器,该匹配器将验证传递给writeCharacters 的字符串是否构成了您希望随着时间的推移传递给该方法的文本序列的下一部分,并且您可以在末尾查询测试以确保它收到了所有预期的输入。下面是使用 Moxie 遵循这种方法的示例测试:

      http://code.google.com/p/moxiemocks/source/browse/trunk/src/test/java/moxietests/StackOverflow6392946Test.java

      我已经复制了下面的代码:

      import moxie.Mock;
      import moxie.Moxie;
      import moxie.MoxieOptions;
      import moxie.MoxieRule;
      import moxie.MoxieUnexpectedInvocationError;
      import org.hamcrest.BaseMatcher;
      import org.hamcrest.Description;
      import org.junit.Assert;
      import org.junit.Rule;
      import org.junit.Test;
      
      import javax.xml.stream.XMLStreamException;
      import javax.xml.stream.XMLStreamWriter;
      
      // Written in response to... http://stackoverflow.com/questions/6392946/
      public class StackOverflow6392946Test {
      
          private static class PiecewiseStringMatcher extends BaseMatcher<String> {
              private final String toMatch;
              private int pos = 0;
      
              private PiecewiseStringMatcher(String toMatch) {
                  this.toMatch = toMatch;
              }
      
              public boolean matches(Object item) {
                  String itemAsString = (item == null) ? "" : item.toString();
                  if (!toMatch.substring(pos).startsWith(itemAsString)) {
                      return false;
                  }
                  pos += itemAsString.length();
                  return true;
              }
      
              public void describeTo(Description description) {
                  description.appendText("a series of strings which when concatenated form the string \"" + toMatch + '"');
              }
      
              public boolean hasMatchedEntirely() {
                  return pos == toMatch.length();
              }
          }
      
          @Rule
          public MoxieRule moxie = new MoxieRule();
      
          @Mock
          public XMLStreamWriter xmlStreamWriter;
      
          // xmlStreamWriter gets invoked with strings which add up to "blah blah", so the test passes.
          @Test
          public void happyPathTest() throws XMLStreamException{
              PiecewiseStringMatcher addsUpToBlahBlah = new PiecewiseStringMatcher("blah blah");
              Moxie.expect(xmlStreamWriter).anyTimes().on().writeCharacters(Moxie.argThat(addsUpToBlahBlah));
      
              xmlStreamWriter.writeCharacters("blah ");
              xmlStreamWriter.writeCharacters("blah");
      
              Assert.assertTrue(addsUpToBlahBlah.hasMatchedEntirely());
          }
      
          // xmlStreamWriter's parameters don't add up to "blah blah", so the test would fail without the catch clause.
          // Also note that the final assert is false.
          @Test
          public void sadPathTest1() throws XMLStreamException{
              // We've specified the deprecated IGNORE_BACKGROUND_FAILURES option as otherwise Moxie works very hard
              // to ensure that unexpected invocations can't get silently swallowed (so this test will fail).
              Moxie.reset(xmlStreamWriter, MoxieOptions.IGNORE_BACKGROUND_FAILURES);
      
              PiecewiseStringMatcher addsUpToBlahBlah = new PiecewiseStringMatcher("blah blah");
              Moxie.expect(xmlStreamWriter).anyTimes().on().writeCharacters(Moxie.argThat(addsUpToBlahBlah));
      
              xmlStreamWriter.writeCharacters("blah ");
              try {
                  xmlStreamWriter.writeCharacters("boink");
                  Assert.fail("above line should have thrown a MoxieUnexpectedInvocationError");
              } catch (MoxieUnexpectedInvocationError e) {
                  // as expected
              }
      
              // In a normal test we'd assert true here.
              // Here we assert false to verify that the behavior we're looking for has NOT occurred.
              Assert.assertFalse(addsUpToBlahBlah.hasMatchedEntirely());
          }
      
          // xmlStreamWriter's parameters add up to "blah bl", so the mock itself doesn't fail.
          // However the final assertion fails, as the matcher didn't see the entire string "blah blah".
          @Test
          public void sadPathTest2() throws XMLStreamException{
              PiecewiseStringMatcher addsUpToBlahBlah = new PiecewiseStringMatcher("blah blah");
              Moxie.expect(xmlStreamWriter).anyTimes().on().writeCharacters(Moxie.argThat(addsUpToBlahBlah));
      
              xmlStreamWriter.writeCharacters("blah ");
              xmlStreamWriter.writeCharacters("bl");
      
              // In a normal test we'd assert true here.
              // Here we assert false to verify that the behavior we're looking for has NOT occurred.
              Assert.assertFalse(addsUpToBlahBlah.hasMatchedEntirely());
          }
      }
      

      【讨论】:

        猜你喜欢
        • 2019-10-15
        • 2017-11-09
        • 1970-01-01
        • 2010-10-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多