You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为含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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 00:45:44