你如何模拟输出流?

时间:2011-06-18 00:00:58

标签: java stream mocking outputstream

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

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

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

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

等同于:

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

发生这种情况无关紧要(出于我的目的)。将会有一些特定的调用序列,但我不在乎它是什么,因此我不想写出对该特定序列的期望。我只是希望以某种方式写出某个特定的数据流。

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

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

我正在使用Moxie进行模拟,但我很想听听任何模拟库的方法。

3 个答案:

答案 0 :(得分:16)

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

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

在您的情况下,您可以使用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());
    }

答案 1 :(得分:4)

我承认我可能不喜欢使用ByteArrayOutputStream作为最低级别的OutputStream,在执行后获取数据并执行所需的任何断言。 (可能使用SAX或其他XML解析器读入数据并浏览结构)

如果你想用模拟做这件事,我会承认我有点偏向Mockito,我认为你可以用自定义Answer来完成你想做的事情。当用户在模拟器上调用writeCharacters时,只需将其参数附加到Buffer,然后您就可以在其上进行断言。

这就是我的想法(手写,并没有执行所以语法问题是可以预料的:))

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

答案 2 :(得分:1)

(免责声明:我是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());
    }
}