JeffGrigg icon

Untitled

JeffGrigg | PRO | 05/14/23 01:41:57 AM UTC | 0 ⭐ | 758 👁️ | Never ⏰ | []
Java 5 |

0 B

|

None

|

0 👍

/

0 👎

/*
GuessTheNumber.java = simple “guess the number” game
 
tests/GuessTheNumberTest.java = JUnit test for the game
tests/SystemInputOutputTesterTest.java = Test the main JUnit testing factory.
tests/RandomSourceTest.java = Test the "Random" class related functionality and mocks used by the "GuessTheNumber" class.
tests/ExpectedException.java = Independent exception (child of Error) to throw when the "code under test" was expected to throw an exception (typically an assertion error), but did not.
 
library/RandomSource.java = creates "Random" instances (or mock) for the Program Under Test
library/RandomCreatorInterface.java = interface to create "Random" instances (enables lambda expressions)
library/AbstractMockRandomSwapper.java = swaps mock Random subclasses in on construction and out on close
library/MockRandomSingleValue.java = mock implementation for single-use random number
 
library/SystemInputOutputTester.java = Builder for console I/O testing
library/CallbackThrowsException.java = interface for call back to "main" method -- to handle checked exceptions
library/MockStandardOutputStream.java = Capture Standard Output (or Error)
library/MockInputStream.java = Provide input when needed. Calls back to SystemInputOutputTester to validate intervening output and to provide the input data.
library/ExpectedInputOutputStepBase.java = common base class for input/output expectations
library/ExpectedTextOutputStep.java = Expect that the given text output is written to Standard Output (or Error).
library/ProvideLineInputStep.java = Provide line of Console Input at that point in the expectations list.
 
*/
 
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// File Name:   GuessTheNumber.java
//
package p20230510_TestConsoleIO;
 
import p20230510_TestConsoleIO.library.RandomSource;
 
import java.util.Scanner;
 
public class GuessTheNumber {
 
    public static void main(final String[] args) {
        System.out.println("Welcome to the number guessing program.");
        System.out.println("I am thinking of a number in the range of 1 to 100, inclusive.");
        System.out.println("You need to guess this number, with a minimum number of guesses.");
        System.out.println("Each time you guess, I will tell you if my number is HIGHER or LOWER than your guess.");
 
        final var random = RandomSource.create();
        final int numberToGuess = random.nextInt(100) + 1;
        final var scanner = new Scanner(System.in);
        var numberOfGuesses = 0;
 
        while (true) {
            System.out.println("What is your guess?");
            final var userGuess = scanner.nextInt();
            ++numberOfGuesses;
            if (userGuess < numberToGuess) {
                System.out.println("Your guess of " + userGuess + " is TOO LOW.");
            } else if (userGuess > numberToGuess) {
                System.out.println("Your guess of " + userGuess + " is TOO HIGH.");
            } else {
                System.out.println("Your guess of " + userGuess + " is CORRECT in "
                        + (numberOfGuesses == 1 ? "one guess!!!" : (numberOfGuesses + " guesses!")));
                break;
            }
        }
    }
 
}
 
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// File Name:   tests/GuessTheNumberTest.java
//
package p20230510_TestConsoleIO.tests;
 
//  https://mastodon.social/@GeePawHill/110341886456025504 = Hard to TDD "Hello World" and simple console I/O "games"
//  https://mastodon.social/@JeffGrigg/110347321176183414 = Jeff "Grigg says that "doing TDD through a User Interface is Always Difficult."
//  https://mastodon.social/@GeePawHill/110351924530710192 = GeePawHill proposes hiding complexity in a library to protect students
//  https://mastodon.social/@JeffGrigg/110356052836313274 = posting my implementation
//  https://mastodon.social/@JeffGrigg/110358610496829363 = my implementation, cleaned and improved -- "Cleaned it up a bit. Better interfaces and reduced coupling. Separate "library" and "tests" packages. Added tests."
 
import org.junit.Test;
import p20230510_TestConsoleIO.GuessTheNumber;
import p20230510_TestConsoleIO.library.MockRandomSingleValue;
import p20230510_TestConsoleIO.library.SystemInputOutputTester;
 
import java.io.IOException;
 
public class GuessTheNumberTest {
 
    @Test
    public void testHighsAndLowsTo33() throws IOException {
 
        final var systemNumberWeAreTryingToGuess = 33;
 
        try (final var mockRandomNumberGenerator = new MockRandomSingleValue(systemNumberWeAreTryingToGuess)) {
            SystemInputOutputTester
                    .whenCallingThisMethod(() -> {
                        GuessTheNumber.main(null);
                    })
                    .assertTextOutput("Welcome to the number guessing program.")
                    //
                    .assertTextOutput("I am thinking of a number in the range of 1 to 100, inclusive.")
                    .assertTextOutput("You need to guess this number, with a minimum number of guesses.")
                    .assertTextOutput("Each time you guess, I will tell you if my number is HIGHER or LOWER than your guess.")
                    //
                    .assertTextOutput("What is your guess?")
                    .provideLineInput("50")
                    .assertTextOutput("Your guess of 50 is TOO HIGH.")
                    //
                    .assertTextOutput("What is your guess?")
                    .provideLineInput("25")
                    .assertTextOutput("Your guess of 25 is TOO LOW.")
                    //
                    .assertTextOutput("What is your guess?")
                    .provideLineInput("37")
                    .assertTextOutput("Your guess of 37 is TOO HIGH.")
                    //
                    .assertTextOutput("What is your guess?")
                    .provideLineInput("31")
                    .assertTextOutput("Your guess of 31 is TOO LOW.")
                    //
                    .assertTextOutput("What is your guess?")
                    .provideLineInput("34")
                    .assertTextOutput("Your guess of 34 is TOO HIGH.")
                    //
                    .assertTextOutput("What is your guess?")
                    .provideLineInput("32")
                    .assertTextOutput("Your guess of 32 is TOO LOW.")
                    //
                    .assertTextOutput("What is your guess?")
                    .provideLineInput("33")
                    .assertTextOutput("Your guess of 33 is CORRECT in 7 guesses!")
                    //
                    .assertThatTheMethodReturnsHere();
        }
    }
 
    @Test
    public void testMaximumGuesses() throws IOException {
 
        final var systemNumberWeAreTryingToGuess = 100;
 
        try (final var mockRandomNumberGenerator = new MockRandomSingleValue(systemNumberWeAreTryingToGuess)) {
            SystemInputOutputTester
                    .whenCallingThisMethod(() -> {
                        GuessTheNumber.main(null);
                    })
                    .assertTextOutput("Welcome to the number guessing program.")
                    //
                    .assertTextOutput("I am thinking of a number in the range of 1 to 100, inclusive.")
                    .assertTextOutput("You need to guess this number, with a minimum number of guesses.")
                    .assertTextOutput("Each time you guess, I will tell you if my number is HIGHER or LOWER than your guess.")
                    //
                    .assertTextOutput("What is your guess?")
                    .provideLineInput("50")
                    .assertTextOutput("Your guess of 50 is TOO LOW.")
                    //
                    .assertTextOutput("What is your guess?")
                    .provideLineInput("75")
                    .assertTextOutput("Your guess of 75 is TOO LOW.")
                    //
                    .assertTextOutput("What is your guess?")
                    .provideLineInput("88")
                    .assertTextOutput("Your guess of 88 is TOO LOW.")
                    //
                    .assertTextOutput("What is your guess?")
                    .provideLineInput("94")
                    .assertTextOutput("Your guess of 94 is TOO LOW.")
                    //
                    .assertTextOutput("What is your guess?")
                    .provideLineInput("97")
                    .assertTextOutput("Your guess of 97 is TOO LOW.")
                    //
                    .assertTextOutput("What is your guess?")
                    .provideLineInput("99")
                    .assertTextOutput("Your guess of 99 is TOO LOW.")
                    //
                    .assertTextOutput("What is your guess?")
                    .provideLineInput("100")
                    .assertTextOutput("Your guess of 100 is CORRECT in 7 guesses!")
                    //
                    .assertThatTheMethodReturnsHere();
        }
    }
 
    @Test
    public void testFirstGuessIsCorrect() throws IOException {
 
        final var systemNumberWeAreTryingToGuess = 24;
 
        try (final var mockRandomNumberGenerator = new MockRandomSingleValue(systemNumberWeAreTryingToGuess)) {
            SystemInputOutputTester
                    .whenCallingThisMethod(() -> {
                        GuessTheNumber.main(null);
                    })
                    .assertTextOutput("Welcome to the number guessing program.")
                    //
                    .assertTextOutput("I am thinking of a number in the range of 1 to 100, inclusive.")
                    .assertTextOutput("You need to guess this number, with a minimum number of guesses.")
                    .assertTextOutput("Each time you guess, I will tell you if my number is HIGHER or LOWER than your guess.")
                    //
                    .assertTextOutput("What is your guess?")
                    .provideLineInput("24")
                    .assertTextOutput("Your guess of 24 is CORRECT in one guess!!!")
                    //
                    .assertThatTheMethodReturnsHere();
        }
    }
 
    @Test
    public void testSecondGuessIsCorrect() throws IOException {
 
        final var systemNumberWeAreTryingToGuess = 33;
 
        try (final var mockRandomNumberGenerator = new MockRandomSingleValue(systemNumberWeAreTryingToGuess)) {
            SystemInputOutputTester
                    .whenCallingThisMethod(() -> {
                        GuessTheNumber.main(null);
                    })
                    .assertTextOutput("Welcome to the number guessing program.")
                    //
                    .assertTextOutput("I am thinking of a number in the range of 1 to 100, inclusive.")
                    .assertTextOutput("You need to guess this number, with a minimum number of guesses.")
                    .assertTextOutput("Each time you guess, I will tell you if my number is HIGHER or LOWER than your guess.")
                    //
                    .assertTextOutput("What is your guess?")
                    .provideLineInput("86")
                    .assertTextOutput("Your guess of 86 is TOO HIGH.")
                    //
                    .assertTextOutput("What is your guess?")
                    .provideLineInput("33")
                    .assertTextOutput("Your guess of 33 is CORRECT in 2 guesses!")
                    //
                    .assertThatTheMethodReturnsHere();
        }
    }
 
}
 
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// File Name:   tests/SystemInputOutputTesterTest.java
//
package p20230510_TestConsoleIO.tests;
 
import junit.framework.AssertionFailedError;
import org.junit.Test;
import p20230510_TestConsoleIO.library.SystemInputOutputTester;
 
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
 
import static org.junit.Assert.assertEquals;
 
public class SystemInputOutputTesterTest {
 
    private static class DoNothingProgram {
        public static void main(String[] args) {
            // Intentionally do NOTHING here.
        }
    }
 
    @Test
    public void testDoNothingProgram() {
        SystemInputOutputTester
                .whenCallingThisMethod(() -> {
                    DoNothingProgram.main(null);
                })
                .assertThatTheMethodReturnsHere();
    }
 
    @Test
    public void testMissingMessage() {
        try {
            SystemInputOutputTester
                    .whenCallingThisMethod(() -> {
                        DoNothingProgram.main(null);
                    })
                    .assertTextOutput("This program does not and should not produce this output.")
                    .assertThatTheMethodReturnsHere();
            throw new ExpectedException("Expected AssertionFailedError to be thrown by the code above.");
        } catch (final AssertionFailedError ex) {
            assertEquals("Failed to find <This program does not and should not produce this output.> in <>", ex.getMessage());
        }
    }
 
    @Test
    public void testProgramTerminatesWithMoreInputToProcess() {
        try {
            SystemInputOutputTester
                    .whenCallingThisMethod(() -> {
                        DoNothingProgram.main(null);
                    })
                    .provideLineInput("Unused Input Line")
                    .assertThatTheMethodReturnsHere();
            throw new ExpectedException("Expected AssertionFailedError to be thrown by the code above.");
        } catch (final AssertionFailedError ex) {
            assertEquals("Program has exited, but the tests say that we should provide further input."
                    + System.lineSeparator() + "  ProvideLineInputStep('Unused Input Line')", ex.getMessage());
        }
    }
 
    @Test
    public void testHelloWorldProgram() {
        SystemInputOutputTester
                .whenCallingThisMethod(() -> {
                    HelloWorldProgram.main(null);
                })
                .assertTextOutput("Hello world!")
                .assertThatTheMethodReturnsHere();
    }
 
    private static class HelloWorldProgram {
        public static void main(String[] args) {
            System.out.println("Hello world!");
        }
    }
 
    @Test
    public void testHelloNameProgram() {
        SystemInputOutputTester
                .whenCallingThisMethod(() -> {
                    HelloNameProgram.main(null);
                })
                .assertTextOutput("What is your name? ")
                .provideLineInput("John")
                .assertTextOutput("Hello John!")
                .assertThatTheMethodReturnsHere();
    }
 
    private static class HelloNameProgram {
        public static void main(String[] args) throws IOException {
            System.out.println("What is your name? ");
            final var in = new BufferedReader(new InputStreamReader(System.in));
            final var name = in.readLine();
            System.out.println("Hello " + name + "!");
        }
    }
 
}
 
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// File Name:   tests/RandomSourceTest.java
//
package p20230510_TestConsoleIO.tests;
 
import org.junit.Test;
import p20230510_TestConsoleIO.library.MockRandomSingleValue;
import p20230510_TestConsoleIO.library.RandomSource;
 
import java.io.IOException;
import java.rmi.server.ExportException;
import java.util.Random;
import java.util.TreeSet;
 
import static org.junit.Assert.*;
 
public class RandomSourceTest {
 
    private static final double ODDS_OF_FALSE_TEST_FAILURE = 0.000001;   // -- PER number we're looking for.
    private static final String NL = System.lineSeparator();
 
    private final Random _random = RandomSource.create();
    private double _lowestConfidence = 1.0;
 
    @Test
    public void testCreateRandomNumberGenerator() throws IOException {
 
        // Initial (production) state:
        assertSame("Initial Java Random number generator class;", Random.class, _random.getClass());
 
        // Creating mock Random number generator swaps in a factory for it as a side-effect:
        try (final var mockRandomNumberGenerator = new MockRandomSingleValue(13)) {
            assertSame(mockRandomNumberGenerator, RandomSource.create());
            assertEquals("Mock Random number, as configured above;",
                    13, mockRandomNumberGenerator.nextInt(100) + 1);
            try {
                mockRandomNumberGenerator.nextInt(100);
                throw new ExportException("Expected AssertionError when MockRandomSingleValue instance is used more than once.");
            } catch (final AssertionError ex) {
                assertEquals("'Random' value must be in the range of 0 <= value < bound (of 100).  The value -1 is outside this range in class " + MockRandomSingleValue.class.getName() + ".",
                        ex.getMessage());
            }
        }
        // Restored to initial (production) state by ".close()" call at the end of the "try (with resource)" block, above.
        assertSame("Initial Random generator restored on 'close();", Random.class, _random.getClass());
    }
 
    @Test
    public void testD1() {
        assertEquals("1st;", 0, _random.nextInt(1));
        assertEquals("2nd;", 0, _random.nextInt(1));
        assertEquals("3rd;", 0, _random.nextInt(1));
        assertEquals("4th;", 0, _random.nextInt(1));
        assertEquals("5th;", 0, _random.nextInt(1));
    }
 
    @Test
    public void test2toN() {
 
        _lowestConfidence = 1.0;
 
        var increment = 1;
        for (var bound = 2; bound <= 200; bound += increment) {
            if (bound >= 10) {
                increment = 5;
            }
 
            final var firstAndLastToFind = new TreeSet<Integer>();
            firstAndLastToFind.add(0);
            firstAndLastToFind.add(bound - 1);
 
            verifyThatBoundryValueAreFound(bound, firstAndLastToFind);
        }
 
        System.out.println("Lowest Confidence Level = " + _lowestConfidence);
    }
 
    @Test
    public void testNegativeOneIsOutOfBounds() {
 
        final var bound = 5;
        final var firstAndLastToFind = new TreeSet<Integer>();
        firstAndLastToFind.add(-1);
 
        try {
            verifyThatBoundryValueAreFound(bound, firstAndLastToFind);
 
            throw new ExpectedException("Expected AssertionFailedError to be thrown by the code above.");
        } catch (final AssertionError ex) {
            final var exceptionMessage = ex.getMessage();
            final var expectedMessagePrefix = "Unable to find value(s) [-1] after ";
            final var expectedMessageSuffix = " tries." + NL
                    + "  Values found = [0, 1, 2, 3, 4]" + NL
                    + "  Giving up because the count of unique values found (5) is (at least) equal to the bound value of 5.";
            assertTrue("Expected message" + NL
                            + " <" + exceptionMessage + ">" + NL
                            + " to start with" + NL
                            + " <" + expectedMessagePrefix + ">.",
                    exceptionMessage.startsWith(expectedMessagePrefix));
            assertTrue("Expected message" + NL
                            + " <" + exceptionMessage + ">" + NL
                            + " to end with" + NL
                            + " <" + expectedMessageSuffix + ">.",
                    exceptionMessage.endsWith(expectedMessageSuffix));
        }
    }
 
    @Test
    public void testValueEqualsBoundNeverHappens() {
 
        final var bound = 20;
        final var firstAndLastToFind = new TreeSet<Integer>();
        firstAndLastToFind.add(bound);
 
        try {
            verifyThatBoundryValueAreFound(bound, firstAndLastToFind);
 
            throw new ExpectedException("Expected AssertionFailedError to be thrown by the code above.");
        } catch (final AssertionError ex) {
            final var exceptionMessage = ex.getMessage();
            final var expectedMessagePrefix = "Unable to find value(s) [20] after ";
            final var expectedMessageSuffix = " tries." + NL
                    + "  Values found = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]" + NL
                    + "  Giving up because the count of unique values found (20) is (at least) equal to the bound value of 20.";
            assertTrue("Expected message" + NL
                            + " <" + exceptionMessage + ">" + NL
                            + " to start with" + NL
                            + " <" + expectedMessagePrefix + ">.",
                    exceptionMessage.startsWith(expectedMessagePrefix));
            assertTrue("Expected message" + NL
                            + " <" + exceptionMessage + ">" + NL
                            + " to end with" + NL
                            + " <" + expectedMessageSuffix + ">.",
                    exceptionMessage.endsWith(expectedMessageSuffix));
        }
    }
 
    private void verifyThatBoundryValueAreFound(final int bound, final TreeSet<Integer> firstAndLastToFind) {
        final var valuesFound = new TreeSet<Integer>();
 
        int numberOfTries = 0;
        double oddsOfGettingThisFarWithNoMatch = 1.0;   // (100% = always gets here!)
 
        while (oddsOfGettingThisFarWithNoMatch > ODDS_OF_FALSE_TEST_FAILURE) {
 
            final var randomValue = _random.nextInt(bound);
 
            ++numberOfTries;
            final double oddsOfNotGettingAMatchThisTime = ((double) (bound - firstAndLastToFind.size())) / bound;
            oddsOfGettingThisFarWithNoMatch *= oddsOfNotGettingAMatchThisTime;
 
            if (oddsOfGettingThisFarWithNoMatch > 0.0 && oddsOfGettingThisFarWithNoMatch < _lowestConfidence) {
                _lowestConfidence = oddsOfGettingThisFarWithNoMatch;
            }
 
            final var isAUniqueNewNumber = valuesFound.add(randomValue);
            if (firstAndLastToFind.remove(randomValue)) {   // true if we did find and remove a value
                if (firstAndLastToFind.isEmpty()) {
                    break;  // Successfully hit first and last expected value. Test is successful.
                } else {
                    oddsOfGettingThisFarWithNoMatch = 1.0;  // Reset = odds of finding 2nd match.
                }
            }
            if (isAUniqueNewNumber) {
                oddsOfGettingThisFarWithNoMatch = 1.0;  // Restore optimisim, as long as we're seeing new numbers.
                final var numberOfValuesFound = valuesFound.size();
                if (numberOfValuesFound >= bound) {
                    fail("Unable to find value(s) " + firstAndLastToFind.toString() + " after " + numberOfTries + " tries."
                            + NL + "  Values found = " + valuesFound.toString()
                            + NL + "  Giving up because the count of unique values found (" + numberOfValuesFound
                            + ") is (at least) equal to the bound value of " + bound + ".");
                }
            }
        }
 
        // Failure case:   [NEVER happens when "break;" is taken from loop, above.]
        if (!firstAndLastToFind.isEmpty()) {
            fail("Unable to find value(s) " + firstAndLastToFind.toString() + " after " + numberOfTries + " tries."
                    + NL + "  Values found = " + valuesFound.toString()
                    + NL + "  [There's a small chance that this test failure may be in error, as it is testing randomness!]");
        }
    }
 
}
 
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// File Name:   tests/ExpectedException.java
//
package p20230510_TestConsoleIO.tests;
 
class ExpectedException extends Error {
    public ExpectedException(final String message) {
        super(message);
    }
}
 
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// File Name:   library/RandomSource.java
//
package p20230510_TestConsoleIO.library;
 
import java.util.Random;
 
abstract public class RandomSource {
 
    private RandomSource() {
        throw new IllegalStateException("Not expecting to create instances of this class.");
    }
 
    private static RandomCreatorInterface _creator = () -> {
        return new Random();
    };
 
    public static Random create() {
        return _creator.create();
    }
 
    public static RandomCreatorInterface swapRandomConstructor(final RandomCreatorInterface newRandomCreator) {
        final var oldRandomCreator = _creator;
        _creator = newRandomCreator;
        return oldRandomCreator;
    }
 
}
 
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// File Name:   library/RandomCreatorInterface.java
//
package p20230510_TestConsoleIO.library;
 
import java.util.Random;
 
public interface RandomCreatorInterface {
 
    Random create();
 
}
 
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// File Name:   library/AbstractMockRandomSwapper.java
//
package p20230510_TestConsoleIO.library;
 
import java.io.Closeable;
import java.io.IOException;
import java.util.Random;
 
import static org.junit.Assert.fail;
 
abstract public class AbstractMockRandomSwapper extends Random implements Closeable {
 
    private final RandomCreatorInterface _oldRandomConstructor;
 
    protected AbstractMockRandomSwapper() {
        _oldRandomConstructor = RandomSource.swapRandomConstructor(() -> {
            return this;
        });
    }
 
    @Override
    public void close() throws IOException {
        final var randomConstructorSwappedOut = RandomSource.swapRandomConstructor(_oldRandomConstructor);
    }
 
    @Override
    final public int nextInt(final int bound) {
        final var returnValue = nextIntImpl(bound);
        if (returnValue < 0 || returnValue >= bound) {
            fail("'Random' value must be in the range of 0 <= value < bound (of " + bound + ")."
                    + "  The value " + returnValue + " is outside this range in class " + this.getClass().getName() + ".");
        }
        return returnValue;
    }
 
    abstract public int nextIntImpl(final int bound);
 
}
 
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// File Name:   library/MockRandomSingleValue.java
//
package p20230510_TestConsoleIO.library;
 
public class MockRandomSingleValue extends AbstractMockRandomSwapper {
 
    private int _targetValueToGuess;
 
    public MockRandomSingleValue(final int targetValueToGuess) {
        _targetValueToGuess = targetValueToGuess;
    }
 
    @Override
    public int nextIntImpl(int bound) {
        final var returnValue = _targetValueToGuess - 1;
        _targetValueToGuess = 0;    // invalid value -- for any subsequent calls
        return returnValue;
    }
 
}
 
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// File Name:   library/SystemInputOutputTester.java
//
package p20230510_TestConsoleIO.library;
 
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
 
public class SystemInputOutputTester {
 
    private final CallbackThrowsException _callback;
    private final List<ExpectedInputOutputStepBase> _expectedInputOutputSteps = new ArrayList<ExpectedInputOutputStepBase>();
    private Iterator<ExpectedInputOutputStepBase> _expectedInputOutputStepIterator = null;
    private final MockInputStream _mockInputStream = new MockInputStream(this);
    private final MockStandardOutputStream _mockStandardOutputStream = new MockStandardOutputStream();
    //protected final MockStandardOutputStream _mockStandardErrorStream = new MockStandardOutputStream(this);
 
    public SystemInputOutputTester(final CallbackThrowsException callback) {
        _callback = callback;
    }
 
    public static SystemInputOutputTester whenCallingThisMethod(final CallbackThrowsException callback) {
        return new SystemInputOutputTester(callback);
    }
 
    public SystemInputOutputTester assertTextOutput(final String expectedOutputMessage) {
        _expectedInputOutputSteps.add(new ExpectedTextOutputStep(_mockStandardOutputStream, expectedOutputMessage));
        return this;
    }
 
    public SystemInputOutputTester provideLineInput(final String lineOfInputText) {
        _expectedInputOutputSteps.add(new ProvideLineInputStep(lineOfInputText));
        return this;
    }
 
    public void assertThatTheMethodReturnsHere() {
        final var oldInput = System.in;
        final var oldOutput = System.out;
        //final var oldError = System.err;
        try {
            System.setIn(_mockInputStream);
            System.setOut(new PrintStream(_mockStandardOutputStream));
            //System.setErr(new PrintStream(_mockStandardErrorStream));
 
            _expectedInputOutputStepIterator = _expectedInputOutputSteps.iterator();
 
            try {
                _callback.callback();
            } catch (final RuntimeException ex) {
                throw ex;
            } catch (final Exception ex) {
                throw new RuntimeException("Nested Exception " + ex.getClass().getName() + ": " + ex.getMessage(), ex);
            }
 
            System.out.flush();
            System.err.flush();
            while (_expectedInputOutputStepIterator.hasNext()) {
                final var step = _expectedInputOutputStepIterator.next();
                final var isAfterExit = true;
                final var didProvideNewUserInput = step.validate(this);
                if (didProvideNewUserInput) {
                    step.throwException("Program has exited, but the tests say that we should provide further input.");
                }
            }
 
        } finally {
            System.setIn(oldInput);
            System.setOut(oldOutput);
            //System.setErr(oldError);
        }
    }
 
    public void validateToAndIncludingNextInput() {
        System.out.flush();
        System.err.flush();
        while (_expectedInputOutputStepIterator.hasNext()) {
            final var step = _expectedInputOutputStepIterator.next();
            final var isAfterExit = false;
            final var didProvideNewUserInput = step.validate(this);
            if (didProvideNewUserInput) {
                break;
            }
        }
    }
 
    protected void provideLineOfInput(final String lineOfInputText) {
        _mockInputStream.provideLineOfInput(lineOfInputText);
    }
 
}
 
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// File Name:   library/CallbackThrowsException.java
//
package p20230510_TestConsoleIO.library;
 
public interface CallbackThrowsException {
 
    public void callback() throws Exception;
 
}
 
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// File Name:   library/MockStandardOutputStream.java
//
package p20230510_TestConsoleIO.library;
 
import org.jetbrains.annotations.NotNull;
 
import java.io.IOException;
import java.io.OutputStream;
 
import static org.junit.Assert.assertEquals;
 
public class MockStandardOutputStream extends OutputStream {
 
    private final StringBuilder _sequenceOfByteValues = new StringBuilder();
    private final StringBuilder _accumulatedOutput = new StringBuilder();
 
    @Override
    public void write(final int byteValue) throws IOException {
        _sequenceOfByteValues.append((char) byteValue);
    }
 
    public StringBuilder getUpdatedStringBuilder() {
 
        if (_sequenceOfByteValues.length() > 0) {
            final String stringValue = getStringValue();
            _accumulatedOutput.append(stringValue);
        }
 
        return _accumulatedOutput;
    }
 
    @NotNull
    private String getStringValue() {
        final var bytes = new byte[_sequenceOfByteValues.length()];
        final var charArray = _sequenceOfByteValues.toString().toCharArray();
        assertEquals(bytes.length, charArray.length);
        for (int idx = 0; idx < bytes.length; ++idx) {
            bytes[idx] = (byte) charArray[idx];
        }
        _sequenceOfByteValues.setLength(0);
        final var stringValue = new String(bytes);
        return stringValue;
    }
 
}
 
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// File Name:   library/MockInputStream.java
//
package p20230510_TestConsoleIO.library;
 
import java.io.IOException;
import java.io.InputStream;
 
public class MockInputStream extends InputStream {
 
    private final SystemInputOutputTester _systemInputOutputTester;
    private final StringBuilder _inputToProvide = new StringBuilder();
 
    public MockInputStream(final SystemInputOutputTester systemInputOutputTester) {
        _systemInputOutputTester = systemInputOutputTester;
    }
 
    @Override
    public int read() throws IOException {
 
        if (_inputToProvide.length() == 0) {
            _systemInputOutputTester.validateToAndIncludingNextInput();
        }
 
        if (_inputToProvide.length() == 0) {
            return -1;  // = End Of File
        } else {
            final var firstCharacter = _inputToProvide.charAt(0);
            _inputToProvide.delete(0, 1);
            return firstCharacter;
        }
    }
 
    @Override
    public int read(final byte byteArray[], final int offset, final int length) throws IOException {
 
        if (_inputToProvide.length() == 0) {
            _systemInputOutputTester.validateToAndIncludingNextInput();
        }
 
        if (_inputToProvide.length() == 0) {
            return -1;  // = End Of File
        } else {
            int charsRead = 0;
            for (; charsRead < length && _inputToProvide.length() > 0; ++charsRead) {
                final var firstCharacter = _inputToProvide.charAt(0);
                _inputToProvide.delete(0, 1);
                byteArray[offset + charsRead] = (byte) firstCharacter;
            }
            return charsRead;
        }
    }
 
    public void provideLineOfInput(final String lineOfInputText) {
        _inputToProvide.append(lineOfInputText);
        _inputToProvide.append('\n');
    }
 
}
 
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// File Name:   library/ExpectedInputOutputStepBase.java
//
package p20230510_TestConsoleIO.library;
 
import junit.framework.AssertionFailedError;
 
abstract public class ExpectedInputOutputStepBase {
 
    protected final AssertionFailedError _locationOfAssertionInitialization = new AssertionFailedError(
            "Location of assertion initialization. [Look up in the Test's chained method calls.]");
 
    protected ExpectedInputOutputStepBase() {
    }
 
    public abstract boolean validate(final SystemInputOutputTester systemInputOutputTester);
 
    public void throwException(final String message) {
        final var ex = new AssertionFailedError(message + System.lineSeparator() + "  " + this.toString());
        ex.initCause(_locationOfAssertionInitialization);
        throw ex;
    }
 
}
 
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// File Name:   library/ExpectedTextOutputStep.java
//
package p20230510_TestConsoleIO.library;
 
import junit.framework.AssertionFailedError;
 
public class ExpectedTextOutputStep extends ExpectedInputOutputStepBase {
 
    private final MockStandardOutputStream _mockOutputStream;
    private final String _expectedOutputMessage;
 
    public ExpectedTextOutputStep(final MockStandardOutputStream mockOutputStream, final String expectedOutputMessage) {
        _mockOutputStream = mockOutputStream;
        _expectedOutputMessage = expectedOutputMessage;
    }
 
    @Override
    public boolean validate(final SystemInputOutputTester systemInputOutputTester) {
        final var stringBuilder = _mockOutputStream.getUpdatedStringBuilder();
        final var currentStringValue = stringBuilder.toString();
        final var foundAtIndex = currentStringValue.indexOf(_expectedOutputMessage);
        if (foundAtIndex >= 0) {
            // Remove the string value we just found:
            stringBuilder.delete(0, foundAtIndex + _expectedOutputMessage.length());
            // If followed immediately by a new line, remove that too:
            final var newlineCharacterSequence = System.lineSeparator();
            if (stringBuilder.toString().startsWith(newlineCharacterSequence)) {
                stringBuilder.delete(0, newlineCharacterSequence.length());
            }
            return false;   // Did NOT provide new *input* data.
        } else {
            final var remainingOutputWithNewlinesVisible = currentStringValue
                    .replace("\r\n", "[NL]")
                    .replace("\r", "\\r")
                    .replace("\n", "\\n");
            final var failEx = new AssertionFailedError("Failed to find <" + _expectedOutputMessage + "> in <" + remainingOutputWithNewlinesVisible + ">");
            failEx.initCause(super._locationOfAssertionInitialization);
            throw failEx;
        }
    }
}
 
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// File Name:   library/ProvideLineInputStep.java
//
package p20230510_TestConsoleIO.library;
 
public class ProvideLineInputStep extends ExpectedInputOutputStepBase {
 
    private final String _lineOfInputText;
 
    public ProvideLineInputStep(final String lineOfInputText) {
        _lineOfInputText = lineOfInputText;
    }
 
    @Override
    public boolean validate(final SystemInputOutputTester systemInputOutputTester) {
        systemInputOutputTester.provideLineOfInput(_lineOfInputText);
        return true;   // *DID* provide new *input* data.
    }
 
    @Override
    public String toString() {
        return "ProvideLineInputStep('" + _lineOfInputText + "')";
    }
 
}
 

Comments

  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎