/* GuessTheNumber.java = simple “guess the number” game RandomNumberGenerator.java = wrapper for random number generation GuessTheNumberTest.java = JUnit test for the game MockRandomNumberGenerator.java = mock implementation for single-use random number SystemInputOutputTester.java = Builder for console I/O testing MockStandardOutputStream.java = Capture Standard Output (or Error) MockInputStream.java = Provide input when needed. Calls back to SystemInputOutputTester to validate intervening output and to provide the input data. ExpectedInputOutputStepBase.java = common base class for input/output expectations ExpectedTextOutputStep.java = Expect that the given text output is written to Standard Output (or Error). ProvideLineInputStep.java = Provide line of Console Input at that point in the expectations list. */ // // File Name: GuessTheNumber.java // import java.util.Scanner; public class GuessTheNumber { protected static RandomNumberGenerator _randomNumberGenerator = new RandomNumberGenerator(); 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 int numberToGuess = _randomNumberGenerator.nextInt(100); final var scanner = new Scanner(System.in); while (true) { System.out.println("What is your guess?"); final var userGuess = scanner.nextInt(); 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!"); break; } } } } // // File Name: RandomNumberGenerator.java // import java.util.Random; public class RandomNumberGenerator { private final Random _randomNumberGenerator = new Random(); public int nextInt(final int bound) { return _randomNumberGenerator.nextInt(bound); } } // // File Name: GuessTheNumberTest.java // import org.junit.Test; public class GuessTheNumberTest { @Test public void test() { final var oldRandomNumberGenerator = GuessTheNumber._randomNumberGenerator; GuessTheNumber._randomNumberGenerator = new MockRandomNumberGenerator(33); try { 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!") // .assertThatTheMethodReturnsHere(); } finally { GuessTheNumber._randomNumberGenerator = oldRandomNumberGenerator; } } } // // File Name: MockRandomNumberGenerator.java // public class MockRandomNumberGenerator extends RandomNumberGenerator { private int _theFixedRandomNumberValue; public MockRandomNumberGenerator(final int theFixedRandomNumberValue) { _theFixedRandomNumberValue = theFixedRandomNumberValue; } @Override public int nextInt(final int bound) { final var returnValue = _theFixedRandomNumberValue; _theFixedRandomNumberValue = -1; // Clear value, as it's only good for one use. return returnValue; } } // // File Name: SystemInputOutputTester.java // import junit.framework.AssertionFailedError; import java.io.PrintStream; import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class SystemInputOutputTester { private final Runnable _callback; private final List _expectedInputOutputSteps = new ArrayList(); private Iterator _expectedInputOutputStepIterator = null; protected final MockInputStream _mockInputStream = new MockInputStream(this); protected final MockStandardOutputStream _mockStandardOutputStream = new MockStandardOutputStream(); //protected final MockStandardOutputStream _mockStandardErrorStream = new MockStandardOutputStream(this); public SystemInputOutputTester(final Runnable callback) { _callback = callback; } public static SystemInputOutputTester whenCallingThisMethod(final Runnable callback) { return new SystemInputOutputTester(callback); } public SystemInputOutputTester assertTextOutput(final String expectedOutputMessage) { _expectedInputOutputSteps.add(new ExpectedTextOutputStep(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(); _callback.run(); 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) { throw new AssertionFailedError("Program has exited, but the tests say that we should further input: <" + ((ProvideLineInputStep) step)._lineOfInputText + ">."); } } } 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; } } } } // // File Name: MockStandardOutputStream.java // import org.jetbrains.annotations.NotNull; import org.junit.Assert; import java.io.IOException; import java.io.OutputStream; 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(); Assert.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: MockInputStream.java // 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: ExpectedInputOutputStepBase.java // 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); } // // File Name: ExpectedTextOutputStep.java // import junit.framework.AssertionFailedError; public class ExpectedTextOutputStep extends ExpectedInputOutputStepBase { private final String _expectedOutputMessage; public ExpectedTextOutputStep(final String expectedOutputMessage) { _expectedOutputMessage = expectedOutputMessage; } @Override public boolean validate(final SystemInputOutputTester systemInputOutputTester) { final var stringBuilder = systemInputOutputTester._mockStandardOutputStream.getUpdatedStringBuilder(); final var currentStringValue = stringBuilder.toString(); final var foundAtIndex = currentStringValue.indexOf(_expectedOutputMessage); if (foundAtIndex >= 0) { stringBuilder.delete(0, foundAtIndex + _expectedOutputMessage.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: ProvideLineInputStep.java // public class ProvideLineInputStep extends ExpectedInputOutputStepBase { protected final String _lineOfInputText; public ProvideLineInputStep(final String lineOfInputText) { _lineOfInputText = lineOfInputText; } @Override public boolean validate(final SystemInputOutputTester systemInputOutputTester) { systemInputOutputTester._mockInputStream.provideLineOfInput(_lineOfInputText); return true; // *DID* provide new *input* data. } }