目录
- 第 0 步 - 将代码重构为可行、可编译的示例
- 第 1 步 - 重构以进行依赖注入
- 第 2 步 - 将 java.util.Scanner 包装到您自己的委托中
- 第 3 步 - 创建一个通用测试
- 第 4 步 - 从循环中提取处理单次尝试
- 第 5 步 - 为游戏创建单元测试
- 第 6 步 - 为 SingleGuessHandler 创建单元测试
- 最后的笔记
第 0 步 - 将代码重构为可行、可编译的示例
public class Main {
public static void main(String[] args) {
new Game().run();
}
}
public class Game {
public void run() {
Scanner scan = new Scanner(System.in);
int number = new Random().nextInt(100);
for (int i = 1; i <= 10; i++) {
System.out.println(" guess " + i + ":");
int guess = scan.nextInt();
if (guess > number) {
System.out.println("Clue: lower");
} else if (guess < number) {
System.out.println("Clue: Higher");
} else if (guess == number) {
System.out.println("Correct answer after only " + i + " guesses – Excellent!");
}
}
}
}
第 1 步 - 重构以进行依赖注入
所有这些操作都应该通过 IDE 重构来完成 - 移动方法、提取到字段、创建构造函数、将初始化程序移动到构造函数、提取到参数等。
public class Main {
public static void main(String[] args) {
Game game = new Game(new Scanner(System.in), System.out, new Random().nextInt(100));
game.run();
}
}
public class Game {
private final Scanner scanner;
private final PrintStream printStream;
private final int number;
public Game(Scanner scanner, PrintStream out, int number) {
this.scanner = scanner;
this.printStream = out;
this.number = number;
}
public void run() {
for (int i = 1; i <= 10; i++) {
printStream.println(" guess " + i + ":");
int guess = scanner.nextInt();
if (guess > number) {
printStream.println("Clue: lower");
} else if (guess < number) {
printStream.println("Clue: Higher");
} else if (guess == number) {
printStream.println("Correct answer after only " + i + " guesses – Excellent!");
}
}
}
}
第 2 步 - 将 java.util.Scanner 包装到您自己的委托中
尝试模拟java.util.Scanner,您很快就会发现该类是final,因此我们创建了一个简单的包装器,并将java.util.Scanner 替换为Main 和Game 类中的包装器。
public class MyScanner {
private final Scanner scanner = new Scanner(System.in);
public int nextInt() {
return scanner.nextInt();
}
}
第 3 步 - 创建一个通用测试
import org.junit.Test;
import org.mockito.InOrder;
import java.io.PrintStream;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class GameTest {
private final MyScanner scanner = mock(MyScanner.class);
private final PrintStream printStream = mock(PrintStream.class);
private final Game game = new Game(scanner, printStream, 15);
@Test
public void returnsCorrectOutputWhenNumberGuessedAfterThreeAttempts() {
when(scanner.nextInt()).thenReturn(10, 20, 15);
game.run();
InOrder inOrder = inOrder(scanner, printStream);
inOrder.verify(printStream).println(" guess 1:");
inOrder.verify(scanner).nextInt();
inOrder.verify(printStream).println("Clue: Higher");
inOrder.verify(printStream).println(" guess 2:");
inOrder.verify(scanner).nextInt();
inOrder.verify(printStream).println("Clue: lower");
inOrder.verify(printStream).println(" guess 3:");
inOrder.verify(scanner).nextInt();
inOrder.verify(printStream).println("Correct answer after only 3 guesses – Excellent!");
}
}
第 4 步 - 从循环中提取处理单次尝试
在循环中有 3 个可能的流程,我们有 10 次迭代。这总共给出了 3^10 ~ 60.000 个可能的流。而且我们不想编写 60,000 个测试。所以我们再次重构(仅使用 IDE 重构选项)以达到以下状态。注意,所有都应该编译并且测试应该仍然是绿色的。
public class Main {
public static void main(String[] args) {
Game game = new Game(new SingleGuessHandler(new MyScanner(), System.out), new Random().nextInt(100));
game.run();
}
}
public class Game {
private final SingleGuessHandler singleGuessHandler;
private final int number;
public Game(SingleGuessHandler singleGuessHandler, int number) {
this.singleGuessHandler = singleGuessHandler;
this.number = number;
}
public void run() {
for (int i = 1; i <= 10; i++) {
singleGuessHandler.runSingleGuess(i, number);
}
}
}
public class SingleGuessHandler {
private final MyScanner scanner;
private final PrintStream printStream;
public SingleGuessHandler(MyScanner scanner, PrintStream printStream) {
this.scanner = scanner;
this.printStream = printStream;
}
public void runSingleGuess(int attempt, int number) {
printStream.println(" guess " + attempt + ":");
int guess = scanner.nextInt();
if (guess > number) {
printStream.println("Clue: lower");
} else if (guess < number) {
printStream.println("Clue: Higher");
} else if (guess == number) {
printStream.println("Correct answer after only " + attempt + " guesses – Excellent!");
}
}
}
public class MyScanner {
private final Scanner scanner = new Scanner(System.in);
public int nextInt() {
return scanner.nextInt();
}
}
public class GameTest {
private final MyScanner scanner = mock(MyScanner.class);
private final PrintStream printStream = mock(PrintStream.class);
private final Game game = new Game(new SingleGuessHandler(scanner, printStream), 15);
@Test
public void returnsCorrectOutputWhenNumberGuessedAfterThreeAttempts() {
when(scanner.nextInt()).thenReturn(10, 20, 15);
game.run();
InOrder inOrder = inOrder(scanner, printStream);
inOrder.verify(printStream).println(" guess 1:");
inOrder.verify(scanner).nextInt();
inOrder.verify(printStream).println("Clue: Higher");
inOrder.verify(printStream).println(" guess 2:");
inOrder.verify(scanner).nextInt();
inOrder.verify(printStream).println("Clue: lower");
inOrder.verify(printStream).println(" guess 3:");
inOrder.verify(scanner).nextInt();
inOrder.verify(printStream).println("Correct answer after only 3 guesses – Excellent!");
}
}
第 5 步 - 为游戏创建单元测试
我们现有的GameTest 一起测试了几个类,但现在我们只想测试Game 和SingleGuessHandler 的交互。在这一点上,我已经意识到您的原始代码不包含任何逻辑来处理在正确猜测的情况下打破循环。这使得下面的单元测试更简单,否则runSingleGuess 将不得不返回真/假值(指示是否再次调用它)并且我们必须再编写 1 个测试来检查它是否返回false,那么它是没有再叫。简而言之:
- 1 次失败测试 - 循环迭代 10 次
- 1 测试成功 - 循环提前终止(我不会这样做,因为它不在您的原始代码中)
@Test
public void callsSingleGuessHandlerTenTimes() {
SingleGuessHandler singleGuessHandler = mock(SingleGuessHandler.class);
Game game = new Game(singleGuessHandler, 17);
game.run();
InOrder inOrder = inOrder(singleGuessHandler);
inOrder.verify(singleGuessHandler).runSingleGuess(1, 17);
inOrder.verify(singleGuessHandler).runSingleGuess(2, 17);
inOrder.verify(singleGuessHandler).runSingleGuess(3, 17);
inOrder.verify(singleGuessHandler).runSingleGuess(4, 17);
inOrder.verify(singleGuessHandler).runSingleGuess(5, 17);
inOrder.verify(singleGuessHandler).runSingleGuess(6, 17);
inOrder.verify(singleGuessHandler).runSingleGuess(7, 17);
inOrder.verify(singleGuessHandler).runSingleGuess(8, 17);
inOrder.verify(singleGuessHandler).runSingleGuess(9, 17);
inOrder.verify(singleGuessHandler).runSingleGuess(10, 17);
inOrder.verifyNoMoreInteractions();
}
第 6 步 - 为 SingleGuessHandler 创建单元测试
public class SingleGuessHandlerTest {
private final MyScanner scanner = mock(MyScanner.class);
private final PrintStream printStream = mock(PrintStream.class);
private final SingleGuessHandler singleGuessHandler = new SingleGuessHandler(scanner, printStream);
@Test
public void printsLowerClue() {
when(scanner.nextInt()).thenReturn(5);
singleGuessHandler.runSingleGuess(99, 4);
InOrder inOrder = inOrder(scanner, printStream);
inOrder.verify(printStream).println(" guess 99:");
inOrder.verify(scanner).nextInt();
inOrder.verify(printStream).println("Clue: lower");
inOrder.verifyNoMoreInteractions();
}
@Test
public void printsHigherClue() {
when(scanner.nextInt()).thenReturn(16);
singleGuessHandler.runSingleGuess(2, 100);
InOrder inOrder = inOrder(scanner, printStream);
inOrder.verify(printStream).println(" guess 2:");
inOrder.verify(scanner).nextInt();
inOrder.verify(printStream).println("Clue: Higher");
inOrder.verifyNoMoreInteractions();
}
@Test
public void printsSuccessfulGuessMessage() {
when(scanner.nextInt()).thenReturn(65);
singleGuessHandler.runSingleGuess(8, 65);
InOrder inOrder = inOrder(scanner, printStream);
inOrder.verify(printStream).println(" guess 8:");
inOrder.verify(scanner).nextInt();
inOrder.verify(printStream).println("Correct answer after only 8 guesses – Excellent!");
inOrder.verifyNoMoreInteractions();
}
}
结语
请注意,MyScanner 和 Main 类未经测试。测试它们需要使用真实的java.util.Scanner 和真实的System.out 打印流。这是一种验收测试(一切都是真实的,没有伪造或模拟),在这种情况下,最好通过将 java 应用程序作为单独的进程运行并为其提供数字并检查其输出来实现。
最好测试被猜测数字的范围。我会通过将new Random().nextInt(100) 提取到一个类中来做到这一点,例如RandomNumberProvider。那么剩下的就是:
- 将其作为依赖项添加到
Game
- 为
Game 添加一个测试,Game 传递给SingleGuessHandler 无论提供者返回什么
- 为
RandomNumberProvider 添加测试 - 属性测试或模拟Random
还要注意,当不同的测试调用相同的方法时,会使用不同的值。它可以防止在实现中对这些值进行意外硬编码。