如何为含System输入与switch分支的方法编写单元测试?
解决依赖控制台输入的服务类单元测试问题
你的测试卡住是因为addBill方法直接硬编码使用Scanner(System.in)读取控制台输入,单元测试运行时没有人工输入,导致程序一直等待输入无法继续。要解决这个问题,核心思路是把输入逻辑从业务逻辑中解耦,然后用Mock工具模拟输入行为。
1. 重构原代码,解耦输入依赖
首先定义一个输入接口,让服务类依赖这个接口而非直接使用Scanner:
// 定义输入提供者接口 public interface InputProvider { String getInput(); } // 生产环境下基于Scanner的实现 public class ConsoleInputProvider implements InputProvider { private final Scanner scanner = new Scanner(System.in); @Override public String getInput() { return scanner.nextLine(); } }
接下来修改BillServiceImpl,通过构造方法注入InputProvider以及其他依赖(比如billItemServices、menuPrinter,这些也应该注入而非硬编码创建):
public class BillServiceImpl implements BillServices { private final InputProvider inputProvider; private final BillItemServices billItemServices; private final MenuPrinter menuPrinter; // 成员变量option、menuOption改为方法内局部变量,避免线程安全问题 // private String option; // private String menuOption; // 构造方法注入所有依赖 public BillServiceImpl(InputProvider inputProvider, BillItemServices billItemServices, MenuPrinter menuPrinter) { this.inputProvider = inputProvider; this.billItemServices = billItemServices; this.menuPrinter = menuPrinter; } @Override public void addBill(Bill bill, Menu menu) { List<BillItems> billItemsList = bill.getBillItemsList(); menu(); System.out.print("Insert menu you want to be served: "); String option = inputProvider.getInput(); // 替换原Scanner.nextLine() try { switch (option){ case "1": menuPrinter.printMenu(menu.getFoodMenu()); foodMenuOption(); System.out.print("Choose menu you want to eat: "); String menuOption = inputProvider.getInput(); // 同样替换 switch (menuOption) { case "1" -> { BreakfastMenu breakfastMenu = menu.getFoodMenu().getBreakfastMenu(); if(breakfastMenu == null){ throw new NullPointerException("Breakfast menu is blank !!!"); } billItemsList = billItemServices.addMenuItemsToBill(billItemsList, breakfastMenu); bill.setBillItemsList(billItemsList); } case "2" -> { LunchMenu lunchMenu = menu.getFoodMenu().getLunchMenu(); if(lunchMenu == null){ throw new NullPointerException("Lunch menu is blank !!!"); } billItemsList = billItemServices.addMenuItemsToBill(billItemsList, lunchMenu); bill.setBillItemsList(billItemsList); } case "3" -> { DinnerMenu dinnerMenu = menu.getFoodMenu().getDinnerMenu(); if(dinnerMenu == null){ throw new NullPointerException("Dinner menu is blank !!!"); } billItemsList = billItemServices.addMenuItemsToBill(billItemsList, dinnerMenu); bill.setBillItemsList(billItemsList); } default -> System.out.println("No option found !!!"); } break; case "2": menuPrinter.printMenu(menu.getDrinkMenu()); drinkMenuOption(); System.out.print("Choose menu you want to drink: "); menuOption = inputProvider.getInput(); switch (menuOption) { case "1" -> { Alcohol alcohol = menu.getDrinkMenu().getAlcohol(); if(alcohol == null){ throw new NullPointerException("Alcohol menu is blank !!!"); } billItemsList = billItemServices.addMenuItemsToBill(billItemsList, alcohol); bill.setBillItemsList(billItemsList); } case "2" -> { SoftDrinks softDrinks = menu.getDrinkMenu().getSoftDrinks(); if(softDrinks == null){ throw new NullPointerException("Soft drink menu is blank !!!"); } billItemsList = billItemServices.addMenuItemsToBill(billItemsList, softDrinks); bill.setBillItemsList(billItemsList); } default -> System.out.println("No option found !!!"); } break; case "e": break; default: System.out.println("No option found !!!"); break; } }catch (NullPointerException exception){ System.out.println(exception.getMessage()); } } // 假设menu()、foodMenuOption()、drinkMenuOption()是打印菜单的方法,保持原实现即可 private void menu() { /* ... */ } private void foodMenuOption() { /* ... */ } private void drinkMenuOption() { /* ... */ } }
2. 编写单元测试
使用Mockito模拟所有依赖,控制输入行为并验证业务逻辑:
@RunWith(MockitoJUnitRunner.class) public class BillServicesTest { @Mock private InputProvider mockInputProvider; // 模拟输入 @Mock private BillItemServices mockBillItemServices; // 模拟账单项服务 @Mock private MenuPrinter mockMenuPrinter; // 模拟菜单打印服务 private BillServices billServices; @Before public void init(){ // 注入模拟依赖到服务实现类 billServices = new BillServiceImpl(mockInputProvider, mockBillItemServices, mockMenuPrinter); } // 测试选择早餐菜单的场景 @Test public void whenSelectBreakfast_addsBreakfastToBill() { // 准备测试数据 Bill bill = new Bill(); bill.setBillItemsList(new ArrayList<>()); Menu menu = new Menu(); FoodMenu foodMenu = new FoodMenu(); BreakfastMenu breakfastMenu = new BreakfastMenu(); foodMenu.setBreakfastMenu(breakfastMenu); menu.setFoodMenu(foodMenu); // 模拟连续输入:先输入1(主菜单选食物),再输入1(选早餐) when(mockInputProvider.getInput()).thenReturn("1").thenReturn("1"); // 执行测试方法 billServices.addBill(bill, menu); // 验证菜单打印服务是否被调用 verify(mockMenuPrinter).printMenu(foodMenu); // 验证账单项服务是否添加了早餐菜单 verify(mockBillItemServices).addMenuItemsToBill(anyList(), eq(breakfastMenu)); // 验证账单的项列表已更新 assertNotNull(bill.getBillItemsList()); } // 测试输入无效选项的场景 @Test public void whenInputInvalidOption_printsErrorMessage() { // 捕获控制台输出,验证错误信息 ByteArrayOutputStream outContent = new ByteArrayOutputStream(); System.setOut(new PrintStream(outContent)); Bill bill = new Bill(); Menu menu = new Menu(); when(mockInputProvider.getInput()).thenReturn("99"); billServices.addBill(bill, menu); // 验证错误信息是否打印 assertTrue(outContent.toString().contains("No option found !!!")); // 恢复系统输出流 System.setOut(System.out); } // 测试菜单为空时的异常处理 @Test public void whenBreakfastMenuIsNull_printsError() { ByteArrayOutputStream outContent = new ByteArrayOutputStream(); System.setOut(new PrintStream(outContent)); Bill bill = new Bill(); Menu menu = new Menu(); FoodMenu foodMenu = new FoodMenu(); foodMenu.setBreakfastMenu(null); // 早餐菜单为空 menu.setFoodMenu(foodMenu); when(mockInputProvider.getInput()).thenReturn("1").thenReturn("1"); billServices.addBill(bill, menu); assertTrue(outContent.toString().contains("Breakfast menu is blank !!!")); System.setOut(System.out); } // 测试输入"e"退出的场景 @Test public void whenInputE_exitsWithoutAddingItems() { Bill bill = new Bill(); Menu menu = new Menu(); when(mockInputProvider.getInput()).thenReturn("e"); billServices.addBill(bill, menu); // 验证账单项服务从未被调用 verify(mockBillItemServices, never()).addMenuItemsToBill(anyList(), any()); } }
核心要点
- 依赖注入:将所有外部依赖(输入、服务、打印)通过构造方法注入,避免硬编码创建,方便测试时替换为Mock实现。
- 模拟输入:通过
when(mockInputProvider.getInput()).thenReturn(...)模拟用户的连续输入,覆盖不同分支逻辑。 - 交互验证:用
verify方法验证依赖是否被正确调用,确保业务逻辑按预期执行。 - 输出捕获:用
ByteArrayOutputStream捕获控制台输出,验证错误提示等信息是否正确打印。
内容的提问来源于stack exchange,提问作者Monach
相关产品推荐
相关产品推荐

