Bootstrap

【JavaWeb的从0到1构建知识体系(七)】JUnit和JUL日志系统

使用JUnit可进行单元测试

首先一问:我们为什么需要单元测试?

随着我们的项目逐渐变大,比如我们之前编写的图书管理系统,我们都是边在写边在测试,而我们当时使用的测试方法,就是直接在主方法中运行测试,但是,在很多情况下,我们的项目可能会很庞大,不可能每次都去完整地启动一个项目来测试某一个功能,这样显然会降低我们的开发效率,因此,我们需要使用单元测试来帮助我们针对于某个功能或是某个模块单独运行代码进行测试,而不是启动整个项目。

同时,在我们项目的维护过程中,难免会涉及到一些原有代码的修改,很有可能出现改了代码导致之前的功能出现问题(牵一发而动全身),而我们又不一定能立即察觉到,因此,我们可以提前保存一些测试用例,每次完成代码后都可以跑一遍测试用例,来确保之前的功能没有因为后续的修改而出现问题。

我们还可以利用单元测试来评估某个模块或是功能的耗时和性能,快速排查导致程序运行缓慢的问题,这些都可以通过单元测试来完成,可见单元测试对于开发的重要性。

尝试JUnit

首先需要导入JUnit依赖,我们在这里使用Junit4进行介绍,最新的Junit5放到Maven板块一起讲解,同时IDEA需要安装JUnit插件(默认是已经捆绑安装的,因此无需多余配置)

现在我们创建一个新的类,来编写我们的单元测试用例

public class TestMain {
    @Test
    public void method(){
        System.out.println("我是测试用例1");
    }

    @Test
    public void method2(){
        System.out.println("我是测试用例2");
    }
}

我们可以点击类前面的测试按钮,或是单个方法前的测试按钮,如果点击类前面的测试按钮,会执行所有的测试用例。

运行测试后,我们发现控制台得到了一个测试结果,显示为绿色表示测试通过。

只需要通过打上@Test注解,即可将一个方法标记为测试案例,我们可以直接运行此测试案例,但是我们编写的测试方法有以下要求:

  • 方法必须是public的
  • 不能是静态方法
  • 返回值必须是void
  • 必须是没有任何参数的方法

对于一个测试案例来说,我们肯定希望测试的结果是我们所期望的一个值,因此,如果测试的结果并不是我们所期望的结果,那么这个测试就应该没有成功通过!

我们可以通过断言工具类来进行判定:

public class TestMain {
    @Test
    public void method(){
        System.out.println("我是测试案例!");
        Assert.assertEquals(1, 2);    //参数1是期盼值,参数2是实际测试结果值
    }
}

通过运行代码后,我们发现测试过程中抛出了一个错误,并且IDEA给我们显示了期盼结果和测试结果,那么现在我们来测试一个案例,比如我们想查看冒泡排序的编写是否正确

@Test
public void method(){
    int[] arr = {0, 4, 5, 2, 6, 9, 3, 1, 7, 8};

    //错误的冒泡排序
    for (int i = 0; i < arr.length - 1; i++) {
        for (int j = 0; j < arr.length - 1 - i; j++) {
            if(arr[j] > arr[j + 1]){
                int tmp = arr[j];
                arr[j] = arr[j+1];
                // arr[j+1] = tmp;
            }
        }
    }

    Assert.assertArrayEquals(new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, arr);
}

通过测试,我们发现得到的结果并不是我们想要的结果,因此现在我们需要去修改为正确的冒泡排序,修改后,测试就能正确通过了。我们还可以再通过一个案例来更加深入地了解测试,现在我们想测试从数据库中取数据是否为我们预期的数据:

@Test
public void method(){
    try (SqlSession sqlSession = MybatisUtil.getSession(true)){
        TestMapper mapper = sqlSession.getMapper(TestMapper.class);
        Student student = mapper.getStudentBySidAndSex(1, "男");

        Assert.assertEquals(new Student().setName("小明").setSex("男").setSid(1), student);
    }
}

那么如果我们在进行所有的测试之前需要做一些前置操作该怎么办呢,一种办法是在所有的测试用例前面都加上前置操作,但是这样显然是很冗余的,因为一旦发生修改就需要挨个进行修改,因此我们需要更加智能的方法,我们可以通过@Before注解来添加测试用例开始之前的前置操作:

public class TestMain {

    private SqlSessionFactory sqlSessionFactory;
    @Before
    public void before(){
        System.out.println("测试前置正在初始化...");
        try {
            sqlSessionFactory = new SqlSessionFactoryBuilder()
                    .build(new FileInputStream("mybatis-config.xml"));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println("测试初始化完成,正在开始测试案例...");
    }

    @Test
    public void method1(){
        try (SqlSession sqlSession = sqlSessionFactory.openSession(true)){
            TestMapper mapper = sqlSession.getMapper(TestMapper.class);
            Student student = mapper.getStudentBySidAndSex(1, "男");

            Assert.assertEquals(new Student().setName("小明").setSex("男").setSid(1), student);
            System.out.println("测试用例1通过!");
        }
    }

    @Test
    public void method2(){
        try (SqlSession sqlSession = sqlSessionFactory.openSession(true)){
            TestMapper mapper = sqlSession.getMapper(TestMapper.class);
            Student student = mapper.getStudentBySidAndSex(2, "女");

            Assert.assertEquals(new Student().setName("小红").setSex("女").setSid(2), student);
            System.out.println("测试用例2通过!");
        }
    }
}

同理,在所有的测试完成之后,我们还想添加一个收尾的动作,那么只需要使用@After注解即可添加结束动作:

@After
public void after(){
    System.out.println("测试结束,收尾工作正在进行...");
}

有关JUnit的使用我们就暂时只介绍这么多。

JUL日志系统

首先一问:我们为什么需要日志系统?

我们之前一直都在使用System.out.println来打印信息,但是,如果项目中存在大量的控制台输出语句,会显得很凌乱,而且日志的粒度是不够细的,假如我们现在希望,项目只在debug的情况下打印某些日志,而在实际运行时不打印日志,采用直接输出的方式就很难实现了,因此我们需要使用日志框架来规范化日志输出。

JDK为我们提供了一个自带的日志框架,位于java.util.logging包下,我们可以使用此框架来实现日志的规范化打印,使用起来非常简单:

public class Main {
    public static void main(String[] args) {
      	// 首先获取日志打印器
        Logger logger = Logger.getLogger(Main.class.getName());
      	// 调用info来输出一个普通的信息,直接填写字符串即可
        logger.info("我是普通的日志");
    }
}

我们可以在主类中使用日志打印,得到日志的打印结果:

十一月 15, 2021 12:55:37 下午 com.test.Main main
信息: 我是普通的日志

我们发现,通过日志输出的结果会更加规范。

JUL日志讲解

日志分为7个级别,详细信息我们可以在Level类中查看:

  • SEVERE(最高值)- 一般用于代表严重错误
  • WARNING - 一般用于表示某些警告,但是不足以判断为错误
  • INFO (默认级别) - 常规消息
  • CONFIG
  • FINE
  • FINER
  • FINEST(最低值)

我们之前通过info方法直接输出的结果就是使用的默认级别的日志,我们可以通过log方法来设定该条日志的输出级别:

public static void main(String[] args) {
    Logger logger = Logger.getLogger(Main.class.getName());
    logger.log(Level.SEVERE, "严重的错误", new IOException("我就是错误"));
    logger.log(Level.WARNING, "警告的内容");
    logger.log(Level.INFO, "普通的信息");
    logger.log(Level.CONFIG, "级别低于普通信息");
}

运行结果:

十一月 10, 2022 2:53:49 下午 javaweb.day02.Main main
严重: 严重的错误
java.io.IOException: 我就是错误
	at javaweb.day02.Main.main(Main.java:10)

十一月 10, 2022 2:53:49 下午 javaweb.day02.Main main
警告: 警告的内容
十一月 10, 2022 2:53:49 下午 javaweb.day02.Main main
信息: 普通的信息

Process finished with exit code 0

我们发现,级别低于默认级别的日志信息,无法输出到控制台,我们可以通过设置来修改日志的打印级别:

public static void main(String[] args) {
    Logger logger = Logger.getLogger(Main.class.getName());

    //修改日志级别,这样级别低于配置的信息就无法输出到控制台,当然同时还要进行下面的自定义日志处理器设置(两者缺一不可)
    logger.setLevel(Level.CONFIG);
    //不使用父日志处理器
    logger.setUseParentHandlers(false);
    //使用自定义日志处理器
    ConsoleHandler handler = new ConsoleHandler();
    handler.setLevel(Level.CONFIG);
    logger.addHandler(handler);

    logger.log(Level.SEVERE, "严重的错误", new IOException("我就是错误"));
    logger.log(Level.WARNING, "警告的内容");
    logger.log(Level.INFO, "普通的信息");
    logger.log(Level.CONFIG, "级别低于普通信息");
}

运行结果:

十一月 10, 2022 3:00:51 下午 javaweb.day02.Main main
严重: 严重的错误
java.io.IOException: 我就是错误
	at javaweb.day02.Main.main(Main.java:21)

十一月 10, 2022 3:00:51 下午 javaweb.day02.Main main
警告: 警告的内容
十一月 10, 2022 3:00:51 下午 javaweb.day02.Main main
信息: 普通的信息
十一月 10, 2022 3:00:51 下午 javaweb.day02.Main main
配置: 级别低于普通信息

Process finished with exit code 0

每个Logger都有一个父日志打印器,我们可以通过getParent()来获取:

public static void main(String[] args) throws IOException {
    Logger logger = Logger.getLogger(Main.class.getName());
    System.out.println(logger.getParent().getClass());
}

我们发现,得到的是java.util.logging.LogManager$RootLogger这个类,它默认使用的是ConsoleHandler,且日志级别为INFO,由于每一个日志打印器都会直接使用父类的处理器,因此我们之前需要关闭父类然后使用我们自己的处理器。

我们通过使用自己日志处理器来自定义级别的信息打印到控制台,当然,日志处理器不仅仅只有控制台打印,我们也可以使用文件处理器来处理日志信息,我们继续添加一个处理器:

//添加输出到本地文件 与控制台处理器对应的是文件处理器
FileHandler fileHandler = new FileHandler("test.log");
fileHandler.setLevel(Level.WARNING);
logger.addHandler(fileHandler);
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
  <date>2022-11-10T17:03:31</date>
  <millis>1668071011466</millis>
  <sequence>0</sequence>
  <logger>javaweb.day02.Main</logger>
  <level>SEVERE</level>
  <class>javaweb.day02.Main</class>
  <method>main</method>
  <thread>1</thread>
  <message>严重的错误</message>
  <exception>
    <message>java.io.IOException: 我就是错误</message>
    <frame>
      <class>javaweb.day02.Main</class>
      <method>main</method>
      <line>24</line>
    </frame>
  </exception>
</record>
<record>
  <date>2022-11-10T17:03:31</date>
  <millis>1668071011490</millis>
  <sequence>1</sequence>
  <logger>javaweb.day02.Main</logger>
  <level>WARNING</level>
  <class>javaweb.day02.Main</class>
  <method>main</method>
  <thread>1</thread>
  <message>警告的内容</message>
</record>
</log>

控制台:

十一月 10, 2022 5:03:31 下午 javaweb.day02.Main main
严重: 严重的错误
java.io.IOException: 我就是错误
	at javaweb.day02.Main.main(Main.java:24)

十一月 10, 2022 5:03:31 下午 javaweb.day02.Main main
警告: 警告的内容
十一月 10, 2022 5:03:31 下午 javaweb.day02.Main main
信息: 普通的信息
十一月 10, 2022 5:03:31 下午 javaweb.day02.Main main
配置: 级别低于普通信息

Process finished with exit code 0

注意,这个时候就有两个日志处理器了,因此控制台和文件的都会生效。如果日志的打印格式我们不喜欢,我们还可以自定义打印格式,比如我们控制台处理器就默认使用的是SimpleFormatter,而文件处理器则是使用的XMLFormatter,我们可以自定义:

//使用自定义日志处理器(控制台)
ConsoleHandler handler = new ConsoleHandler();
handler.setLevel(Level.CONFIG);
handler.setFormatter(new XMLFormatter());//将控制台的打印格式转变为XMLFormatter。
logger.addHandler(handler);

我们可以直接配置为想要的打印格式,如果这些格式还不能满足你,那么我们也可以自行实现:

public static void main(String[] args) throws IOException {
    Logger logger = Logger.getLogger(Main.class.getName());
    logger.setUseParentHandlers(false);

    //为了让颜色变回普通的颜色,通过代码块在初始化时将输出流设定为System.out
    ConsoleHandler handler = new ConsoleHandler(){{
        setOutputStream(System.out);
    }};
    //创建匿名内部类实现自定义的格式
    handler.setFormatter(new Formatter() {
        @Override
        public String format(LogRecord record) {
            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
            String time = format.format(new Date(record.getMillis()));  //格式化日志时间
            String level = record.getLevel().getName();  // 获取日志级别名称
            // String level = record.getLevel().getLocalizedName();   // 获取本地化名称(语言跟随系统)
            String thread = String.format("%10s", Thread.currentThread().getName());  //线程名称(做了格式化处理,留出10格空间)
            long threadID = record.getThreadID();   //线程ID
            String className = String.format("%-20s", record.getSourceClassName());  //发送日志的类名
            String msg = record.getMessage();   //日志消息

          //\033[33m作为颜色代码,30~37都有对应的颜色,38是没有颜色,IDEA能显示,但是某些地方可能不支持
            return "\033[38m" + time + "  \033[33m" + level + " \033[35m" + threadID
                    + "\033[38m --- [" + thread + "] \033[36m" + className + "\033[38m : " + msg + "\n";
        }
    });
    logger.addHandler(handler);

    logger.info("我是测试消息1...");
    logger.log(Level.INFO, "我是测试消息2...");
    logger.log(Level.WARNING, "我是测试消息3...");
}

日志可以设置过滤器,如果我们不希望某些日志信息被输出,我们可以配置过滤规则:

public static void main(String[] args) throws IOException {
    Logger logger = Logger.getLogger(Main.class.getName());

    //自定义过滤规则
    logger.setFilter(record -> !record.getMessage().contains("普通"));

    logger.log(Level.SEVERE, "严重的错误", new IOException("我就是错误"));
    logger.log(Level.WARNING, "警告的内容");
    logger.log(Level.INFO, "普通的信息");//被过滤了
}

运行结果:

十一月 10, 2022 6:42:01 下午 javaweb.day02.Main main
严重: 严重的错误
java.io.IOException: 我就是错误
	at javaweb.day02.Main.main(Main.java:16)

十一月 10, 2022 6:42:01 下午 javaweb.day02.Main main
警告: 警告的内容

Process finished with exit code 0

实际上,整个日志的输出流程如下:
img
暂停更新一段时间,下次开始前先复习一下。

;