文章目录
- 使用JUnit进行单元测试
- 原因
- 测试
- 断言工具类
- 案例一:错误冒泡排序
- 案例二:从数据库获取数据
- @Before注解
- @After
- JUL日志系统
- 使用
- JUL日志
- 修改日志的打印级别
- 文件处理器
- 控制打印格式
- 日志设置过滤器
- Properties配置文件
- 编写日志配置文件
- 使用Lombok快速开启日志
- Mybatis日志系统
使用JUnit进行单元测试
原因
首先一问:我们为什么需要单元测试?
随着项目逐渐变大,不可能每次都去完整地启动一个项目来测试某一个功能,这样显然会降低开发效率,因此,需要使用单元测试来帮助我们针对于某个功能或是某个模块单独运行代码进行测试,而不是启动整个项目
同时,在项目的维护过程中,难免会涉及到一些原有代码的修改,很有可能出现改了代码导致之前的功能出现问题(牵一发而动全身),又不一定能立即察觉到,因此,可以提前保存一些测试用例,每次完成代码后都可以跑一遍测试用例,来确保之前的功能没有因为后续的修改而出现问题。
可以利用单元测试来评估某个模块或是功能的耗时和性能,快速排查导致程序运行缓慢的问题,这些都可以通过单元测试来完成,可见单元测试对于开发的重要性。
测试
首先需要导入JUnit依赖,这里使用Junit4进行介绍
创建一个新的类,来编写单元测试用例:
通过打上@Test
注解,即可将一个方法标记为测试案例,可以直接运行此测试案例,但是编写的测试方法有以下要求:
- 方法必须是public的
- 不能是静态方法
- 返回值必须是void
- 必须是没有任何参数的方法
package com.test.test;
import org.junit.Test;
public class MainTest {
@Test
public void method() {
System.out.println("I'm the one");
}
@Test
public void method1() {
System.out.println("I'm the second");
}
}
通过点击类前面的测试按钮,或是单个方法前的测试按钮来进行测试;如果点击类前面的测试按钮,会执行所有的测试用例。
运行测试后,我们发现控制台得到了一个测试结果,显示为绿色表示测试通过。
对于一个测试案例来说,如果测试的结果并不是所期望的结果,那么这个测试就应该没有成功通过!
断言工具类
可以通过断言工具类来进行判定:
public class MainTest {
@Test
public void method() {
System.out.println("I'm the one");
Assert.assertEquals("断言工具类",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;
}
}
}
System.out.println(Arrays.toString(arr));
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注解
可以通过@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
注解即可添加结束动作:
@After
public void after(){
System.out.println("测试结束,收尾工作正在进行...");
}
JUL日志系统
原因:
- 如果项目中存在大量的控制台输出语句,会显得很凌乱
- 日志的粒度是不够细的,假如我们现在希望,项目只在debug的情况下打印某些日志,而在实际运行时不打印日志,采用直接输出的方式就很难实现了
因此需要使用日志框架来规范化日志输出
使用
而JDK为我们提供了一个自带的日志框架,位于java.util.logging
包下,我们可以使用此框架来实现日志的规范化打印,使用起来非常简单:
public class Main {
public static void main(String[] args) {
Logger logger = Logger.getLogger(Main.class.getName()); //获取日志打印器
logger.info("我是一个日志啊?"); //调用info输出一个普通信息
}
}
可以在主类中使用日志打印,得到日志的打印结果:
通过日志输出的结果会更加规范
JUL日志
日志分为7个级别,详细信息可以在Level.class中查看:
- 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("It's me, hi! I'm the problem! It's me"));
logger.log(Level.WARNING, "警告的内容");
logger.log(Level.INFO, "普通的信息");
logger.log(Level.CONFIG, "级别低于普通信息");
}
输出:
修改日志的打印级别
级别低于默认级别的日志信息,无法输出到控制台,可以通过设置来修改日志的打印级别
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, "级别低于普通信息");
}
每个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,由于每一个日志打印器都会直接使用父类的处理器,因此需要先关闭父类然后使用自定义处理器
文件处理器
通过使用自己日志处理器来自定义级别的信息打印到控制台,当然,日志处理器不仅仅只有控制台打印,也可以使用文件处理器来处理日志信息,我们继续添加一个处理器:
try {
FileHandler handler = new FileHandler("test.log");
handler.setLevel(Level.INFO);
logger.addHandler(handler);
} catch (IOException e) {
throw new RuntimeException(e);
}
输出:会发现Config没被写进去
控制打印格式
可以自定义打印格式,比如我们控制台处理器就默认使用的是SimpleFormatter
,而文件处理器则是使用的XMLFormatter
,我们可以自定义:
//使用自定义日志处理器(控制台)
ConsoleHandler handler = new ConsoleHandler();
handler.setLevel(Level.CONFIG);
handler.setFormatter(new 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, "普通的信息");
}
实际上,整个日志的输出流程如下:
Properties配置文件
本质:跟Map一样结构
Properties文件是Java的一种配置文件:
name=Test
desc=Description
该文件配置很简单,格式为配置项=配置值
,可以直接通过Properties
类来将其读取为一个类似于Map一样的对象:
public static void main(String[] args) throws IOException {
Properties properties = new Properties();
properties.load(new FileInputStream("test.properties"));
System.out.println(properties);
}
输出
Properties
类是继承自Hashtable
,而Hashtable
是实现的Map接口,也就是说,Properties
本质上就是一个Map一样的结构,它会把所有的配置项映射为一个Map,这样就可以快速地读取对应配置的值了
可以将已经存在的Properties对象放入输出流进行保存,这里就不保存文件了,而是直接打印到控制台,只需要提供输出流即可:
public static void main(String[] args) throws IOException {
Properties p = new Properties();
p.setProperty("test","hahah");
p.load(new FileInputStream("name.properties"));
p.store(System.out, "Test message");
p.storeToXML(System.out, "hahha"); //转换为XML格式
}
输出
可以通过System.getProperties()
获取系统的参数,来看看:
public static void main(String[] args) throws IOException {
System.getProperties().store(System.out, "系统信息:");
}
编写日志配置文件
通过进行配置文件来规定日志打印器的一些默认值:
# RootLogger 的默认处理器为
handlers= java.util.logging.ConsoleHandler
# RootLogger 的默认的日志级别
.level= CONFIG
使用配置文件来进行配置:
public static void main(String[] args) throws IOException {
//获取日志管理器
LogManager manager = LogManager.getLogManager();
//读取我们自己的配置文件
manager.readConfiguration(new FileInputStream("logging.properties"));
//再获取日志打印器
Logger logger = Logger.getLogger(Main.class.getName());
logger.log(Level.CONFIG, "我是一条日志信息"); //通过自定义配置文件,我们发现默认级别不再是INFO了
}
可以去修改ConsoleHandler
的默认配置:
# 指定默认日志级别
java.util.logging.ConsoleHandler.level = ALL
# 指定默认日志消息格式
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# 指定默认的字符集
java.util.logging.ConsoleHandler.encoding = UTF-8
阅读ConsoleHandler
的源码就会发现,它就是通过读取配置文件来进行某些参数设置:
// Private method to configure a ConsoleHandler from LogManager
// properties and/or default values as specified in the class
// javadoc.
private void configure() {
LogManager manager = LogManager.getLogManager();
String cname = getClass().getName();
setLevel(manager.getLevelProperty(cname +".level", Level.INFO));
setFilter(manager.getFilterProperty(cname +".filter", null));
setFormatter(manager.getFormatterProperty(cname +".formatter", new SimpleFormatter()));
try {
setEncoding(manager.getStringProperty(cname +".encoding", null));
} catch (Exception ex) {
try {
setEncoding(null);
} catch (Exception ex2) {
// doing a setEncoding with null should always work.
// assert false;
}
}
}
使用Lombok快速开启日志
如果现在需要全面使用日志系统,而不是传统的直接打印,那么就需要在每个类都去编写获取Logger的代码,这样显然是很冗余的,能否简化一下这个流程呢?
前面学习了Lombok,也体会到Lombok给我们带来的便捷,可以通过一个注解快速生成构造方法、Getter和Setter,同样的,Logger也是可以使用Lombok快速生成的。
@Log
public class Main {
public static void main(String[] args) {
System.out.println("自动生成的Logger名称:"+log.getName());
log.info("我是日志信息");
}
}
只需要添加一个@Log
注解即可,添加后,可以直接使用一个静态变量log,而它就是自动生成的Logger。也可以手动指定名称:
@Log(topic = "打工是不可能打工的")
public class Main {
public static void main(String[] args) {
System.out.println("自动生成的Logger名称:"+log.getName());
log.info("我是日志信息");
}
}
输出
Mybatis日志系统
Mybatis也有日志系统,它详细记录了所有的数据库操作等,来监控所有的数据库操作,要开启日志系统,需要进行配置:
<setting name="logImpl" value="STDOUT_LOGGING" />
logImpl
包括很多种配置项,包括 SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING,而默认情况下是未配置,也就是说不打印。
将其设定为STDOUT_LOGGING表示直接使用标准输出将日志信息打印到控制台,编写一个测试案例来看看效果:
public class TestMain {
private SqlSessionFactory sqlSessionFactory;
@Before
public void before(){
try {
sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(new FileInputStream("mybatis-config.xml"));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
@Test
public void test(){
try(SqlSession sqlSession = sqlSessionFactory.openSession(true)){
TestMapper mapper = sqlSession.getMapper(TestMapper.class);
System.out.println(mapper.getStudentBySidAndSex(1, "男"));
System.out.println(mapper.getStudentBySidAndSex(1, "男"));
}
}
}
我们发现,两次获取学生信息,只有第一次打开了数据库连接,而第二次并没有。
现在我们学习了日志系统,那么我们来尝试使用日志系统输出Mybatis的日志信息:
<setting name="logImpl" value="JDK_LOGGING" />
将其配置为JDK_LOGGING表示使用JUL进行日志打印,因为Mybatis的日志级别都比较低,因此我们需要设置一下logging.properties
默认的日志级别:
handlers= java.util.logging.ConsoleHandler
.level= ALL
java.util.logging.ConsoleHandler.level = ALL
代码编写如下:
@Log
public class TestMain {
private SqlSessionFactory sqlSessionFactory;
@Before
public void before(){
try {
sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(new FileInputStream("mybatis-config.xml"));
LogManager manager = LogManager.getLogManager();
manager.readConfiguration(new FileInputStream("logging.properties"));
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void test(){
try(SqlSession sqlSession = sqlSessionFactory.openSession(true)){
TestMapper mapper = sqlSession.getMapper(TestMapper.class);
log.info(mapper.getStudentBySidAndSex(1, "男").toString());
log.info(mapper.getStudentBySidAndSex(1, "男").toString());
}
}
}
但们发现,这样的日志信息根本没法看,因此需要修改一下日志的打印格式,创建一个格式化类:
public class TestFormatter extends 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())); //格式化日志时间
return time + " : " + record.getMessage() + "\n";
}
}
现在再来修改一下默认的格式化实现:
handlers= java.util.logging.ConsoleHandler
.level= ALL
java.util.logging.ConsoleHandler.level = ALL
java.util.logging.ConsoleHandler.formatter = com.test.TestFormatter
现在就好看多了,当然,我们还可以继续为Mybatis添加文件日志