Java单元测试实战:应对System.in交互式输入的挑战

Java单元测试实战:应对System.in交互式输入的挑战

本文详细阐述了如何在java单元测试中处理依赖`system.in`进行用户交互的方法。通过重定向标准输入流`system.in`为`bytearrayinputstream`,我们可以模拟用户输入,并结合`bytearrayoutputstream`捕获标准输出,从而实现对交互式逻辑的自动化测试,有效解决测试阻塞问题,确保代码质量。

引言:测试System.in交互的挑战

在Java应用程序开发中,我们经常会遇到需要与用户进行交互的场景,例如通过命令行读取用户输入。Scanner类通常与System.in结合使用来获取这些输入。然而,当我们需要对包含System.in交互逻辑的方法进行单元测试时,会面临一个普遍的挑战:直接运行测试会导致程序在等待用户输入时无限期阻塞,使自动化测试无法完成。本文将深入探讨如何优雅地解决这一问题,通过重定向标准输入输出流来模拟用户交互,从而实现对这类方法的有效单元测试。

问题分析:为什么直接测试会阻塞?

考虑以下服务方法addBill,它使用Scanner从System.in读取用户选项:

@Overridepublic void addBill(Bill bill, Menu menu) {    Scanner scanner = new Scanner(System.in);    // ...    System.out.print("Insert menu you want to be served: ");    option = scanner.nextLine(); // 此处等待用户输入    // ...}

当你为addBill方法编写单元测试时,如果直接调用它,测试执行将会在scanner.nextLine()这一行停滞,因为System.in默认连接到键盘,而单元测试环境并没有提供键盘输入。这就会导致测试用例长时间处于“加载”状态,最终可能因超时而失败,或者需要手动干预才能继续。

核心策略:重定向标准输入输出流

解决System.in阻塞问题的核心策略是重定向Java的System.in和System.out流。我们可以将System.in替换为一个ByteArrayInputStream,该流可以预先加载我们希望模拟的用户输入数据。同时,为了验证方法的输出,我们可以将System.out替换为ByteArrayOutputStream,以便捕获方法打印到控制台的信息。

立即学习“Java免费学习笔记(深入)”;

模拟用户输入:ByteArrayInputStreamByteArrayInputStream允许我们从一个字节数组中读取数据,就像从文件中读取一样。通过将我们想要模拟的用户输入字符串转换为字节数组,并用它初始化ByteArrayInputStream,然后通过System.setIn()方法将这个流设置为新的标准输入流,即可实现模拟输入。

捕获系统输出:ByteArrayOutputStreamByteArrayOutputStream是一个输出流,它将数据写入一个内部的字节数组缓冲区。通过将System.out替换为一个PrintStream,而这个PrintStream又连接到ByteArrayOutputStream,我们就可以在测试结束后检查ByteArrayOutputStream中捕获到的内容,从而验证方法是否打印了预期的信息。

实战演练:逐步实现单元测试

我们将通过一个简化的账单服务示例来演示如何应用上述策略。

待测试服务类示例

为了简化,我们使用一个不依赖其他服务的BillingService,它直接处理用户输入和输出。

青柚面试 青柚面试

简单好用的日语面试辅助工具

青柚面试 57 查看详情 青柚面试

import java.io.InputStream;import java.util.Scanner;public class BillingService {    public void addBill() {        Scanner scanner = new Scanner(System.in);        System.out.print("Insert menu you want to be served: ");        var option = scanner.nextLine(); // 第一次输入        String menuOption = ""; // 初始化 menuOption        if (!"e".equals(option)) { // 避免在退出时再次读取            System.out.print("Choose menu you want to eat/drink: ");            menuOption = scanner.nextLine(); // 第二次输入        }        try {            switch (option) {                case "1":                    System.out.println("Food Menu Selected.");                    switch (menuOption) {                        case "1" -> System.out.println("Breakfast selected.");                        case "2" -> System.out.println("Lunch selected.");                        case "3" -> System.out.println("Dinner selected.");                        default -> System.out.println("No food option found !!!");                    }                    break;                case "2":                    System.out.println("Drink Menu Selected.");                    switch (menuOption) {                        case "1" -> System.out.println("Alcohol selected.");                        case "2" -> System.out.println("Soft Drink selected.");                        default -> System.out.println("No drink option found !!!");                    }                    break;                case "e":                    System.out.println("Exiting bill service.");                    break;                default:                    System.out.println("No main option found !!!");                    break;            }        } catch (NullPointerException exception) {            System.out.println("Error: " + exception.getMessage());        } finally {            // 在实际应用中,不应关闭System.in关联的Scanner            // 但在测试中,由于我们替换了System.in,这里通常可以忽略或由测试框架处理        }    }}

测试环境搭建:@BeforeEach 和 @AfterEach

我们将使用JUnit 5和MockitoExtension。在每个测试方法执行前后,我们需要设置和恢复System.in和System.out。

import org.junit.jupiter.api.AfterEach;import org.junit.jupiter.api.BeforeEach;import org.junit.jupiter.api.Test;import org.junit.jupiter.api.extension.ExtendWith;import org.mockito.junit.jupiter.MockitoExtension;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.InputStream;import java.io.PrintStream;import static org.junit.jupiter.api.Assertions.assertTrue;@ExtendWith(MockitoExtension.class)class BillingServiceTest {    private BillingService billingService;    // 保存原始的System.in和System.out    private final InputStream systemIn = System.in;    private final PrintStream systemOut = System.out;    // 用于模拟输入的流    private ByteArrayInputStream testIn;    // 用于捕获输出的流    private ByteArrayOutputStream testOut;    @BeforeEach    public void init() {        billingService = new BillingService();        // 在每个测试开始前,重定向System.out到testOut        testOut = new ByteArrayOutputStream();        System.setOut(new PrintStream(testOut));    }    @AfterEach    public void restoreSystemInputOutput() {        // 在每个测试结束后,恢复原始的System.in和System.out        System.setIn(systemIn);        System.setOut(systemOut);    }    // 辅助方法:提供模拟输入    private void provideInput(String data) {        testIn = new ByteArrayInputStream(data.getBytes());        System.setIn(testIn);    }    @Test    void whenChooseFoodMenuAndBreakfast_thenOutputCorrectMessage() {        // 模拟用户输入 "1" (选择食物菜单) 和 "1" (选择早餐)        provideInput("1n1");        billingService.addBill();        // 验证输出是否包含预期的消息        String actualOutput = testOut.toString();        assertTrue(actualOutput.contains("Food Menu Selected."), "Output should contain 'Food Menu Selected.'");        assertTrue(actualOutput.contains("Breakfast selected."), "Output should contain 'Breakfast selected.'");    }    @Test    void whenChooseDrinkMenuAndSoftDrink_thenOutputCorrectMessage() {        // 模拟用户输入 "2" (选择饮料菜单) 和 "2" (选择软饮)        provideInput("2n2");        billingService.addBill();        // 验证输出是否包含预期的消息        String actualOutput = testOut.toString();        assertTrue(actualOutput.contains("Drink Menu Selected."), "Output should contain 'Drink Menu Selected.'");        assertTrue(actualOutput.contains("Soft Drink selected."), "Output should contain 'Soft Drink selected.'");    }    @Test    void whenChooseExitOption_thenOutputExitMessage() {        // 模拟用户输入 "e" (退出)        provideInput("en"); // 即使只输入一个,也应提供换行符        billingService.addBill();        // 验证输出是否包含预期的消息        String actualOutput = testOut.toString();        assertTrue(actualOutput.contains("Exiting bill service."), "Output should contain 'Exiting bill service.'");    }    @Test    void whenChooseInvalidOption_thenOutputErrorMessage() {        // 模拟用户输入 "x" (无效选项) 和任意后续输入        provideInput("xnany_inputn");        billingService.addBill();        String actualOutput = testOut.toString();        assertTrue(actualOutput.contains("No main option found !!!"), "Output should contain 'No main option found !!!'");    }}

注意事项与最佳实践

流的恢复至关重要: 在@AfterEach(或JUnit 4的@After)方法中恢复原始的System.in和System.out是极其重要的。如果忘记恢复,可能会影响后续的测试用例,甚至导致测试运行器或其他依赖System.in/System.out的组件行为异常。

提供完整的输入: 模拟输入字符串时,确保包含所有预期的换行符(n),以模拟用户在每次输入后按下回车键的行为。例如,如果方法需要两次输入,那么provideInput的参数应该像”option1noption2n”这样。

对于复杂依赖的模拟: 示例中的BillingService是独立的。在原始问题中,BillServiceImpl依赖于billItemServices和menuPrinter。对于这些外部依赖,应继续使用Mockito等模拟框架进行模拟,例如:

@Mockprivate BillItemServices billItemServices;@Mockprivate MenuPrinter menuPrinter;// ... 在测试中注入这些Mock对象

这样可以将对System.in的测试与对其他业务逻辑的测试解耦。

设计可测试的代码: 虽然上述方法解决了测试System.in的问题,但从长远来看,最佳实践是设计更易于测试的代码。这意味着将I/O操作与核心业务逻辑分离。例如,可以引入一个InputProvider接口:

public interface InputProvider {    String readLine();}public class ConsoleInputProvider implements InputProvider {    private final Scanner scanner = new Scanner(System.in);    @Override    public String readLine() {        return scanner.nextLine();    }}public class BillServiceImpl {    private final InputProvider inputProvider;    public BillServiceImpl(InputProvider inputProvider) {        this.inputProvider = inputProvider;    }    // ...    option = inputProvider.readLine(); // 使用依赖注入的InputProvider    // ...}

在测试时,可以提供一个MockInputProvider,它返回预设的字符串,而无需直接修改System.in。这种设计模式(依赖注入)大大提高了代码的可测试性和灵活性。

总结

对包含System.in交互逻辑的Java方法进行单元测试,需要巧妙地利用Java的I/O重定向机制。通过将System.in替换为ByteArrayInputStream来模拟用户输入,并将System.out替换为ByteArrayOutputStream来捕获输出,我们可以有效地对这类交互式逻辑进行自动化测试。结合@BeforeEach和@AfterEach(或JUnit 4的@Before和@After)来管理测试环境的设置和恢复,可以确保测试的隔离性和稳定性。同时,我们也应该认识到,更好的设计模式(如依赖注入)可以从根本上提高代码的可测试性,减少对这种I/O重定向技巧的依赖。

以上就是Java单元测试实战:应对System.in交互式输入的挑战的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/298743.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年11月4日 23:59:51
下一篇 2025年11月5日 00:05:02

相关推荐

  • 解决Laravel中外键约束错误1005:表创建失败问题

    本教程旨在解决Laravel数据库迁移中常见的“外键约束格式不正确”(errno: 150)错误,特别是当涉及自引用外键时。文章将详细解释错误原因,并提供通过明确外键引用表和延迟自引用外键创建的有效解决方案,确保数据库结构正确建立。 理解Laravel中的外键约束错误1005 (errno: 150…

    2025年12月11日
    000
  • 在Laravel Resource中优雅地处理嵌套集合的分页链接

    本文旨在深入探讨如何在Laravel API Resources中正确地为嵌套集合(如父资源中的子项列表)生成并显示分页链接。我们将详细分析Laravel ResourceCollection 的工作原理,指出在嵌套场景下常见的陷阱,并提供一套基于 JsonResource 和 ResourceCo…

    2025年12月11日
    000
  • 为 WooCommerce 订单管理页面添加可编辑的自定义字段

    本文将详细介绍如何在 WooCommerce 订单管理页面中添加一个可编辑的自定义字段,用于记录订单完成时的总里程数。通过本文,你将学会如何在订单详情页面添加输入框,并将用户输入的数据保存到数据库,最后在订单管理页面中显示该数据。 添加自定义字段到订单详情页面 首先,我们需要在 WooCommerc…

    2025年12月11日
    000
  • 为 WooCommerce 订单管理页面添加可编辑的自定义里程字段

    本教程旨在指导开发者如何在 WooCommerce 订单管理页面添加一个可编辑的自定义字段,用于记录车辆服务的总里程数。我们将详细讲解如何添加输入框、保存用户输入的数据到数据库,以及如何在订单详情页显示该自定义字段的值,从而实现订单里程信息的记录和管理。 在 WooCommerce 网站中,有时需要…

    2025年12月11日
    000
  • 基于MySQL查询结果动态改变字体颜色

    本文将介绍一种实用技巧,用于根据从MySQL数据库获取的数据动态改变网页上的字体颜色。摘要:通过结合PHP的动态处理能力和CSS的样式控制,可以实现根据数据库中的状态值,为网页元素赋予不同的颜色,从而直观地展示信息。 在实际的Web开发中,经常需要根据数据的状态来改变其在页面上的显示效果,例如,将“…

    2025年12月11日
    000
  • PHP:根据 MySQL 数据动态改变字体颜色

    本文将介绍如何使用 PHP 和 CSS,根据从 MySQL 数据库检索到的值,动态地改变网页上特定文本的颜色。通过为不同的状态值定义 CSS 类,并使用 PHP 根据数据库值应用相应的类,可以轻松实现此功能。 实现步骤 以下是实现动态字体颜色更改的详细步骤: 定义 CSS 类: 首先,在你的 CSS…

    2025年12月11日
    000
  • 基于MySQL数据动态改变字体颜色

    本文将介绍如何基于从MySQL数据库检索到的值,使用PHP动态地改变网页元素的字体颜色。通过使用CSS类和PHP条件判断,可以根据不同的数据库值,将字体颜色设置为不同的颜色,例如将 “Pending” 状态显示为黄色,”Approved” 状态显示为绿…

    2025年12月11日
    000
  • 基于 MySQL 数据值的 PHP 字体颜色动态调整教程

    本教程旨在指导开发者如何根据从 MySQL 数据库检索到的值,动态地改变 PHP 页面中特定文本的字体颜色。通过使用 CSS 类和 PHP 的条件判断,可以轻松实现根据不同状态值显示不同颜色的效果,例如将 “Pending” 状态显示为黄色,”Approved&#…

    2025年12月11日
    000
  • WooCommerce 后台订单页添加、保存与显示可编辑自定义字段

    本教程详细介绍了如何在 WooCommerce 后台订单详情页添加可编辑的自定义字段,并确保其数据能够正确保存到数据库以及在后续访问时显示出来。通过使用 WooCommerce 提供的特定钩子和函数,您将学会如何扩展订单管理功能,以满足特定的业务需求,例如记录车辆总里程等信息,从而提升订单数据管理的…

    2025年12月11日
    000
  • 根据MySQL值动态改变字体颜色

    本文旨在指导开发者如何根据从MySQL数据库检索到的值,使用PHP动态地改变网页元素的字体颜色。通过结合PHP的逻辑判断和CSS样式,可以实现不同状态对应不同颜色的显示效果,例如将“Pending”状态显示为黄色,“Approved”状态显示为绿色,“Rejected”状态显示为红色。 要实现根据M…

    2025年12月11日
    000
  • 解决 Laravel 外键约束删除问题的教程

    本文旨在解决 Laravel 应用中因外键约束导致的删除操作失败问题,特别是 SQLSTATE[23000]: Integrity constraint violation: 1451 错误。文章将详细阐述该错误产生的原因,并提供两种主流的解决方案:使用 onDelete(‘cascad…

    2025年12月11日
    000
  • 在WooCommerce后台订单页面添加自定义可编辑字段并实现数据持久化

    本教程详细指导如何在WooCommerce后台订单详情页添加一个自定义的可编辑字段,并确保其数据能够被正确保存至数据库并在后续编辑时显示。文章涵盖了字段的创建、数据的保存机制以及已保存数据的显示方法,旨在帮助开发者扩展WooCommerce订单管理功能,提升后台操作的灵活性和效率。 在woocomm…

    2025年12月11日
    000
  • 解决 Laravel 中外键约束导致的删除或更新失败问题

    本文深入探讨了在 Laravel 应用中,当尝试删除或更新具有外键关联的父记录时,可能遇到的“完整性约束违反”错误。我们将详细解释此错误的原因,即子记录对外键的依赖,并提供三种主要的解决方案:利用数据库层面的级联删除(onDelete(‘cascade’))、设置外键为 NU…

    2025年12月11日
    000
  • 爬虫如何编写?DOM解析与数据抓取

    爬虫编写核心是两步:抓取网页内容和解析提取数据。1. 发起http请求获取html,可用requests库实现;2. 使用解析工具如beautifulsoup或lxml进行dom解析,并通过css选择器或xpath定位并提取目标数据。对于动态内容,需用selenium或playwright模拟浏览器…

    2025年12月11日 好文分享
    000
  • CodeIgniter表单验证:处理数组类型输入字段的“无法找到验证规则”错误

    本文旨在解决CodeIgniter框架中,当对通过AJAX或表单提交的数组类型输入字段进行验证时,可能出现的“Unable to find validation rules”错误。核心问题在于$this->form_validation->set_rules()方法中指定的字段名与服务器…

    2025年12月11日
    000
  • CodeIgniter 嵌套数组表单验证规则设置指南

    本文旨在解决CodeIgniter框架中,对通过AJAX提交的嵌套数组数据进行表单验证时,出现“Unable to find validation rules”错误的问题。我们将深入探讨CodeIgniter form_validation库处理数组输入的机制,并提供正确的 set_rules 配置…

    2025年12月11日
    000
  • 如何在 Laravel 中处理外键约束导致的删除或更新失败

    本文旨在深入探讨 Laravel 应用中因外键约束引发的删除或更新操作失败问题,特别是常见的“Integrity constraint violation: 1451”错误。我们将详细介绍三种有效的解决方案:利用数据库层面的级联删除、设置外键为 NULL,以及通过 Eloquent 模型手动管理关联…

    2025年12月11日
    000
  • 在WooCommerce后台订单页添加可编辑自定义字段并保存显示

    本教程详细指导如何在WooCommerce后台订单编辑页面添加自定义可编辑字段。通过利用WordPress和WooCommerce提供的动作钩子,您将学习如何创建输入框、安全地将用户输入的数据保存到数据库,并在后续订单查看时正确地显示这些信息。这对于需要扩展订单数据,如记录车辆总里程等特定业务场景至…

    2025年12月11日 好文分享
    000
  • OpenCart 3.x.x storage 目录配置与常见错误解决方案

    本文详细介绍了OpenCart 3.0.x.x版本中storage目录的正确配置方法,旨在解决因路径定义不当导致的日志写入失败等常见问题。通过明确DIR_STORAGE常量的定义规则,无论是默认位置还是自定义路径,用户都能确保系统正常运行,避免文件访问权限或目录不存在的错误,从而保障OpenCart…

    2025年12月11日
    000
  • Opencart 3.x 存储目录配置与常见错误解析

    本文深入探讨 Opencart 3.x 版本中 storage 目录的关键作用、推荐的配置方法及常见的错误解决方案。通过详细分析 DIR_STORAGE 路径定义、目录结构完整性和文件权限等核心要素,旨在帮助用户正确配置存储路径,有效解决“文件或目录不存在”等运行时错误,确保 Opencart 系统…

    2025年12月11日
    000

发表回复

登录后才能评论
关注微信