Как вы издеваетесь над потоком вывода?

Под "выходным паром" понимается любой объект, который получает последовательность байтов, или символов или что-то еще. Итак, java.io.OutputStream, но также java.io.Writer, javax.xml.stream.XMLStreamWriter метод writeCharacters и т.д.

Я пишу mock-based тесты для класса, основной функцией которого является запись потока данных в один из них (XMLStreamWriter, как это бывает).

Проблема заключается в том, что поток данных записывается в ряд вызовов метода записи, но главное - это не вызовы, а данные. Например, учитывая XMLStreamWriter out, они:

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

Соответствует этому:

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

Это действительно не имеет значения (для моих целей), что происходит. Будет определенная последовательность вызовов, но мне все равно, что это такое, поэтому я не хочу писать ожидания для этой конкретной последовательности. Я просто хочу ожидать, что какой-то поток данных будет написан любым способом.

Один из вариантов - переключиться на тестирование на основе состояния. Я мог бы накапливать данные в буфере и делать утверждения об этом. Но поскольку я пишу XML, это означало бы создание довольно сложных и уродливых утверждений. Mocking кажется гораздо лучшим способом справиться с большей проблемой написания XML.

Итак, как мне это сделать с макетом?

Я использую Moxie для насмешки, но мне интересно узнать о подходах с любой насмешливой библиотекой.

Ответ 1

Довольно элегантная стратегия тестирования выходных или входных потоков заключается в использовании классов PipedInputStream и PipedOutputStream. Вы можете связать их вместе в настройке теста, а затем проверить, что было написано после выполнения целевого метода.

Вы можете работать в другом направлении, готовя некоторый ввод, а затем дайте тесту прочитать эти подготовленные данные и из входного потока.

В вашем случае вы можете просто высмеять эту переменную "out" с помощью PipedOutputStream и подключите к ней 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());
    }

Ответ 2

Я согласен с тем, что я, вероятно, частично использовал ByteArrayOutputStream как самый низкий уровень OutputStream, получая данные после выполнения и формируя любые утверждения, которые необходимы. (возможно, используя 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

(Отказ от ответственности: я автор Мокси.)

Я предполагаю, что вы хотите сделать это, используя логику, встроенную в mock, чтобы вызовы, которые нарушают ваше ожидание, быстро заканчиваются. Да, это возможно - но не изящно/просто в любой насмешливой библиотеке, о которой я знаю. (В общем, макетные библиотеки хороши при тестировании поведения вызовов методов в изоляции/последовательности, но плохо при тестировании более сложных взаимодействий между вызовами по жизненному циклу макета.) В этой ситуации большинство людей будут создавать буфер, поскольку другие ответы предлагайте - хотя это не так быстро, тестовый код проще реализовать/понять.

В текущей версии 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/info/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 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 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());
    }
}