简体   繁体   中英

How do you mock an output stream?

By 'output steam' i mean any object which receives a sequence of bytes, or characters or whatever. So, java.io.OutputStream, but also java.io.Writer, javax.xml.stream.XMLStreamWriter's writeCharacters method, and so on.

I'm writing mock-based tests for a class whose main function is to write a stream of data to one of these (the XMLStreamWriter, as it happens).

The problem is that the stream of data is written in a series of calls to the write method, but what matters is not the calls, but the data. For example, given an XMLStreamWriter out , these:

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

Are equivalent to this:

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

It really doesn't matter (for my purposes) which happens. There will be some particular sequence of calls, but i don't care what it is, so i don't want to write expectations for that particular sequence. I just want to expect a certain stream of data to be written any which way.

One option would be to switch to state-based testing. I could accumulate the data in a buffer, and make assertions about it. But because i'm writing XML, that would mean making some fairly complex and ugly assertions. Mocking seems a much better way of dealing with the larger problem of writing XML.

So how do i do this with a mock?

I'm using Moxie for mocking, but i'm interested in hearing about approaches with any mocking library.

A fairly elegant strategy to test output or input streams is to use PipedInputStream and PipedOutputStream classes. You can wire them together in the set up of the test, and then check what has been written after the target method is executed.

You can work the other direction preparing some input and then let the test read this prepared data from the input stream as well.

In your case, you could just mock that "out" variable with a PipedOutputStream, and plug a PipedInputStream to it this way:

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

I'll admit that I'm probably partial to using a ByteArrayOutputStream as the lowest level OutputStream, fetching the data after execution and peforming whatever assertions that are needed. (perhaps using SAX or other XML parser to read in the data and dive through the structure)

If you want to do this with a mock, I'll admit I'm somewhat partial to Mockito , and I think you could accomplish what you're looking to do with a custom Answer which when the user invokes writeCharacters on your mock, would simply append their argument to a Buffer, and then you can make assertions on it afterwards.

Here's what I have in my head (hand written, and haven't executed so syntax issues are to be expected :) )

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

(Disclaimer: I'm the author of Moxie.)

I assume you want to do this using logic embedded in the mock so that calls that violate your expectation fail fast. Yes, this is possible - but not elegant/simple in any mocking library I know of. (In general mock libraries are good at testing the behavior of method calls in isolation/sequence, but poor at testing more complex interactions between calls over the lifecycle of the mock.) In this situation most people would build up a buffer as the other answers suggest - while it doesn't fail fast, the test code is simpler to implement/understand.

In the current version of Moxie, adding custom parameter-matching behavior on a mock means writing your own Hamcrest matcher. (JMock 2 and Mockito also let you use custom Hamcrest matchers; EasyMock lets you specify custom matchers that extend a similar IArgumentMatcher interface.)

You'll want a custom matcher that will verify that the string passed to writeCharacters forms the next part of the sequence of text you expect to be passed into that method over time, and which you can query at the end of the test to make sure it's received all of the expected input. An example test following this approach using Moxie is here:

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

I've reproduced the code below:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM