Chapter 15. Junit单元测试

15.1 软件测试方法

  • 白盒测试(结构测试):测试者清楚待测试对象内部工作机制,该测试是开发者测试自己设计编码的代码。而接下来要讨论的 Junit 即是基于白盒测试
  • 黑盒测试(功能测试):通过测试检验是否每个功能都能正常工作,检查程序功能是否按规格说明书的规定正常使用,这是在程序接口进行的测试。

15.2 Junit使用步骤

  1. 定义一个测试类,命名建议如下:

    • 测试类的名称:被测试的类名Test.java,如CalculatorTest
    • 包名称:xxx.xxx.xx.test
  2. 定义测试方法(能够在IDE中独立运行的模块),其定义建议如下:

    • 方法名:test被测试的方法名称
    • 返回值:void
    • 参数列表:空参
  3. 给方法加上 @Test 的注解

    此外,在这个测试类中,还能对另一些方法,加上这些注解:

    • @Before:常用于资源的申请,修饰的方法会在测试方法之前被自动执行
    • @After:与上相反的情况。
  4. 导入 Junit 依赖环境

    IDEA中鼠标指向红线处,即能够导入 Junit 各种依赖环境了。

  5. 点击你需要测试的方法的首行右侧绿色按钮,便能够单独地运行该方法。

  6. 通过 断言语句 来判断测试结果与期望的结果是否一致。

    1
    Assert.assertEquals(期望的结果,运算的结果);

案例演示

项目结构:

1
2
3
4
5
6
JoyDee
├─ LearningJunit
| └─ Calculator.java
|
└─ test
└─ CalculatorTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package JoyDee.LearningJunit;

public class Calculator {
public int addNums(int a, int b){
return a + b;
}
public int subNums(int a, int b){
int tmp = a / 0; /* 此处作为错误演示 */
return a - b;
}
public int mulNums(int a, int b){
return a * a;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package JoyDee.test;

import JoyDee.LearningJunit.Calculator;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test; /* 4.导入junit依赖 */

public class CalculatorTest { /* 2.定义测试方法 */
@Test /* 3.为方法添加@Test注解 */
public void testAddNums(){
Calculator c = new Calculator();
int res = c.addNums(1,2);
System.out.println(res);
}

@Test
public void testSubNums(){
Calculator c = new Calculator();
int res = c.subNums(1, 2);
System.out.println(res);
}

@Test
public void testMulNums(){
Calculator c = new Calculator();
int res = c.mulNums(2, 3);
Assert.assertEquals(6, res); /* 期望值 与 测试值 进行比较 */
}

@Before /*所有测试方法执行之前,必须先执行该方法*/
public void init(){
System.out.println("Initializing...");
}

@After /*所有测试方法执行之后,必须先执行该方法*/
public void close(){
System.out.println("Closing the resource...");
}

}

Chapter 16. 反射

16.1 Java类的加载

如上图,Java类的完整加载机制分上面的七个阶段,现在讨论的是“加载”,是Java类的完整加载机制的第一个阶段。

参考了"学习java应该如何理解反射?"——Kira的知乎回答,假定你需要运行下面这个代码:

1
Object o = new Object();

首先这段代码通过 javac 编译成一个 .class 的二进制文件,然后通过 类加载器(ClassLoader)加载进JVM的内存中(此时,类 Object 加载到内存的方法区中)。

接着,创建了 Object 类的**Class 对象**(类型对象,每个类只有一个 Class 对象,作为方法区类的数据结构的接口。故并不是你 new 出来的实例对象!)

JVM 创建一个实例对象前,会先检查这个对象所属的类是否被加载,寻找该类对应的 Class 对象。若已经加载好,则会为你需要的对象分配内存,初始化(即 new Object()

16.2 反射概述

Java的反射(reflection)机制,是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。

这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动态语言的关键。

此外,通过反射,我们能够解耦,提高程序的可扩展性。

下面的.java文件、.class文件、Class类的关系图,源自bravo1988的回答

Class 类对反射的支持写了内部类,里面的字段与 .class 文件的内容形成映射

16.3 Class对象及其内部成员的获取方法

获取 Class 对象的方法共三个:(获取的 Class 对象,都是一样的!)

  1. 将字节码文件(.class)加载进内存,返回 Class 对象:Class.forName("全类名")(全类名,即" 包名.类名 "),举例如下:

    1
    Class cls1 = Class.forName("top.JoyDee.Person");

    多用于配置文件,将类名定义在配置文件中,读入文件,加载类。

  2. 通过类名的属性 class 来获取:类名.class,举例如下:

    1
    Class cls2 = Person.class;

    多用于参数的传递。

  3. 对象继承了 Object 类定义的 getClass() 方法:对象.getClass

    1
    2
    Person a = new Person();
    Class cls3 = a.getClass();

    多用在,有了对象,要获取其对应的字节码文件对象。

通过 Class 对象的方法,获取其内部成员:

一、获取成员变量们:

  • 获取所有 public 修饰的成员变量:Field[] getFields()

    1
    2
    3
    4
    5
    Class personCls = Person.class; //获取Person的Class对象
    Field[] fs = personCls.getFields(); //获取所有public修饰的成员变量
    for(Field fe : fs){
    System.out.println(fe);
    }
  • 获取指定名称的 public修饰的成员变量:Field getField(String name)
  • 获取所有的成员变量(包括privateprotectedpublic及默认),不考虑修饰符:Field[] getDeclaredFields()
  • 获取指定名称的 且不考虑修饰 的成员变量:Field getDeclaredField(String name)

二、获取构造器们:

  • Constructor<?>[] getConstructors()

  • Constructor<T> getConstructor(类<?>... parameterTypes) (括号中是某个构造器的 参数类型 的 Class 对象列表)

    1
    2
    Class personCls = Person.class;
    Constructor c = personCls.getConstructor(String.class, int.class);
  • Constructor<T> getDeclaredConstructor(类<?>... parameterTypes)

  • Constructor<?>[] getDeclaredConstructors()

三、获取成员方法们:

  • 获取本类及父类或父接口的所有公共方法:Method[] getMethods()

  • Method getMethod(String name, 类<?>... parameterTypes)

    注意,与 获取构造器 不同在于,获取成员方法,需要传入(方法名,参数类型的Class对象列表)

  • 获取本类中的所有方法(包括privateprotectedpublic及默认)Method[] getDeclaredMethods()

  • Method getDeclaredMethod(String name, 类<?>... parameterTypes)

四、获取全类名:String getName()

1
2
3
Class personCls = Person.class;
String className = personCls.getName();
System.out.println(className); /* JoyDee.LearningReflection.Person */

16.4 成员变量、构造器、成员方法相关API

Field:成员变量

  • 设置值:void set(Object obj, Object value)

  • 获取值:get(Object obj)

  • 忽略访问权限修饰符的安全检查:成员变量.setAccessible(true)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Person p = new Person();

    Field ne = personCls.getField("name"); //获取相应字段的成员变量
    Object value = ne.get(p); //将p对象传进去,使其返回值
    ne.set(p, "Luffy"); //修改

    Field ie = personCls.getField("id");
    is.setAccessible(true); //暴力反射
    Object value2 = ie.get(p);

Constructor:构造器

  • 创建其构造的对象:T newInstance(Object... initargs)

    1
    2
    3
    Class personCls = Person.class;
    Constructor c = personCls.getConstructor(String.class, int.class);
    Object person = c.newInstance("Luffy", 20);

Method:成员方法

  • 执行其方法:Object invoke(Object obj, Object... args)

    1
    2
    3
    4
    Class personCls = Person.class;
    Person p = new Person();
    Method eat_method = personCls.getMethod("eat", String.class); //获取指定名称的方法
    eat_method.invoke(p, "饭"); //指向方法,传入 指定对象及方法参数列表
  • 获取方法名称:String getName()

  • 忽略访问权限修饰符的安全检查:

    1
    mymethod.setAccessible(true);

16.5 综合案例

  • 需求:写一个"框架",不能改变该类的任何代码的前提下,可以帮我们创建任意类的对象,并且执行其中任意方法。

  • 实现:

    • 配置文件
    • 反射
  • 步骤:

    1. 将需要创建的对象的全类名和需要执行的方法定义在配置文件
    2. 在程序中加载读取配置文件
    3. 使用反射技术来加载类文件进内存
    4. 创建实例对象
    5. 执行方法

项目文件:

pro.properties 配置文件:

1
2
className = JoyDee.LearningReflection.Person 
methodName = eat

Person.java 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package JoyDee.LearningReflection;

public class Person {
private int id;
private String name;
public Person() {
}
public Person(int id, String name) {
this.id = id;
this.name = name;
}
public void eat(){
System.out.println("I am EATING!!!");
}
}

ReflectDemo.java 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package JoyDee.LearningReflection;

import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Properties;

public class ReflectDemo {
public static void main(String[] args) throws Exception{
//1.1 创建属性集对象(可参看IO流章节)
Properties pro = new Properties();
//1.2 通过类加载器获取配置文件
ClassLoader clsRoader = ReflectDemo.class .getClassLoader();
InputStream is = clsRoader.getResourceAsStream("pro.properties"); //字节输入流
pro.load(is); //从字节输入流读入键值对

//2.获取配置文件中定义的数据
String className = pro.getProperty("className"); //通过键找到值
String methodName = pro.getProperty("methodName");

//3. 加载该类进内存
Class cls = Class.forName(className); //传入类名

//4. 创建实例对象
Constructor c = cls.getConstructor(); //获取空参构造器
Object obj = c.newInstance(); //执行该空参构造器来创建实例

//5.获取方法对象
Method method = cls.getMethod(methodName); //找到配置文件对应的方法名
method.invoke(obj); //需要传入“实例”对象
}
}

Chapter 17. 注解

17.1 注解概述

Java注解 (Annotation)又称Java标注,是 Java 语言 5.0 版本开始支持加入源代码的特殊语法 元数据 。

注解与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。(注解,不是程序的一部分)

和 Javadoc 不同,Java标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java虚拟机可以保留标注内容,在运行时可以获取到标注内容。 当然它也支持自定义Java标注。

按照作用来分类:

  • 根据 Annotation 生成帮助文档:加上 @Documente 标签能使该 Annotation 标签的内容出现在 javadoc中;

  • 让编译器进行编译检查:例如 @Override

  • 代码分析:在反射中解析并使用 Annotation,或者,使用别人写的 测试类框架 对每个需要测试的方法加上诸如 @Check 的注解

17.2 内置的注解

Annotation 接口的架构:

Java 定义了一套注解,共有 7 个,3 个在 java.lang 中,剩下 4 个在 java.lang.annotation 中。从 Java 7 开始,额外添加了新 3 个注解(本文暂不讨论)

作用在代码的注释

  • @Override:检查该方法是否有覆写方法。若发现其父类或引用的接口中并没有该方法,则会报编译错误。

  • @Deprecated:标记过时的方法,若使用该方法,则会报编译警告

  • @SuppressWarnings:指示编译器去忽略注释中声明的警告。

    1
    2
    @SuppressWarnings("all")
    /* 一般传递参数all */

作用在其他注解的注解(又称 元注解)

  • @Retention:标识这个注解如何保存,是只在代码中,还是编入 .class 文件中,或者是在运行时可通过反射访问。(缺省情况下,默认是 RetentionPolicy.CLASS

    可以选择:SOURCECLASSRUNTIME (一般而言,我们会取 RUNTIME 值)

    1
    2
    @Retention(RetentionPolicy.RUNTIME)
    /* 当前被描述的注解,会保留到class字节码文件中,并被JVM读取到 */
  • @Documented:标记这些注解是否包含在用户API文档中。(缺省情况下不出现在 javadoc)

  • @Target:标记这个注解应该是哪种 Java 成员,它一般指定Annotation类型属性 ElementType 取值):(缺省情况下可指定任何地方)

    源码中,Target 类中有一成员,为枚举类数组ElementType[],名称为 value

    • TYPE:标记该注解只能作用在类上
    • METHOD:标记该注解只能作用在方法上
    • FIELD:标记该注解只能作用在成员变量上
    1
    2
    3
    4
    @Target(value={ElementType.TYPE, ElementType.METHOD})
    public @interface MyAnno3{

    }
  • @Inherited:标记这个注解是继承于哪个注释类,一般指定 Annotation 的策略属性

17.3 自定义注解

一、通用格式:

1
2
3
4
@元注解
public @interface 注解名称{
//...
}

通过反汇编得到其本质:

1
2
3
public interface 注解名称 extends java.lang.annotation.Annotation{
//...
}

二、举例:

  • 定义 自定义的注解类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Documented // 表示它可以出现在 javadoc 中。
    @Target(ElementType.TYPE) //意味着MyAnnotation是来修饰"类、接口(包括注释类型)或枚举声明"的注解。
    @Retention(RetentionPolicy.RUNTIME) //编译器会将该Annotation信息保留在.class文件中,并且能被虚拟机读取。
    public @interface MyAnnotation{
    // Annotation 接口的实现细节都由编译器完成。

    public int age(); //定义抽象方法,它是这个注解的属性
    public String() default "Luffy";
    public MyAnno2 anno2(); //注解类型
    public String[] strs();
    }
  • 使用自定义注解类:

    1
    2
    3
    4
    @MyAnnotation(age = 20, anno2 = @MyAnno2, strs = {"Luffy", "Zoro"})
    public class Worker{

    }

三、属性:接口中的抽象方法。其要求有:

  • 属性的返回值类型只能为:基本数据类型、String、枚举、注解、以上类型的数组

    因而,属性的返回值不能是自定义的类

  • 一旦定义了属性,在使用时需给属性进行赋值

    • 定义时如果用 default设置默认值,就无需再赋值(见上面的代码)

    • 如果只有一个属性需要赋值,则只需要这样写即可:

      1
      2
      3
      4
      @MyAnnotation(20)
      public class Worker{
      //...
      }

17.4 解析注解

在程序中使用(解析)注解,就是获取注解中定义的属性值。步骤如下:

  1. 获取你注解类 定义的 的 类型对象(如 ClassMethodField

  2. 调用 类型对象 的方法,来获取指定的 注解对象 :

    1
    getAnnotation(Class) //其实就是在内存中生成了一个该注解接口的子类实现对象
  3. 调用注解对象中的方法,来获取配置的属性值

案例一

现在通过注解的方式,简化 16.5 综合案例中 的简单框架配置

MyAnno.java 文件:描述需要执行的类名、方法名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package JoyDee.LearningReflection;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnno {
String className();
String methodName();
}

/**
* MyAnno a1 = refCls.getAnnotation(MyAnno.class);
* 其实就相当于给这个接口实现一个类:
* public class ProImpl implements Pro{
* public String className(){
* return "JoyDee.LearningReflection.Person";
* }
* public String methodName(){
* return "eat";
* }
* }
*/

Person.java文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package JoyDee.LearningReflection;

public class Person {
private int id;
private String name;
public Person() {
}
public Person(int id, String name) {
this.id = id;
this.name = name;
}
public void eat(){
System.out.println("I am EATING!!!");
}
}

ReflectDemo.java 文件:框架类(无需属性集类、IO流类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package JoyDee.LearningReflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

@MyAnno(className = "JoyDee.LearningReflection.Person", methodName = "eat")
public class ReflectDemo {
public static void main(String[] args) throws Exception{
// 一.解析注解
Class<ReflectDemo> refCls = ReflectDemo.class; //获取该类的字节码文件对象
MyAnno a = refCls.getAnnotation(MyAnno.class); //传入注解接口,得到实例对象
String className = a.className(); //由此得到注解上面写的"JoyDee.LearningReflection.Person"
String methodName = a.methodName(); //由此得到"eat"

//二. 就是之前所写的代码了
//3. 加载该类进内存
Class cls = Class.forName(className); //传入类名

//4. 创建实例对象
Constructor c = cls.getConstructor(); //获取空参构造器
Object obj = c.newInstance(); //执行该空参构造器来创建实例

//5.获取方法对象
Method method = cls.getMethod(methodName); //找到配置文件对应的方法名
method.invoke(obj); //需要传入“实例”对象
}
}

案例二:设计测试框架

Calculator.java 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package JoyDee.LearningReflection;

public class Calculator {
@Check
public int addNums(){
return 1 + 1;
}

public int subNums(){
return 1 - 2;
}

@Check
public int mulNums(){
String str = null;
str.toString(); /* 此处作为错误演示 */
return 2 * 3;
}

@Check
public int divNums(){
return 3 / 0; /* 此处作为错误演示 */
}
}

TestCheck.java 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package JoyDee.LearningReflection;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Method;

public class TestCheck {
public static void main(String[] args) throws IOException {
//创建计算器类的对象
Calculator cal = new Calculator();
//获取字节码文件对象
Class cls = cal.getClass();
//获取所有方法
Method[] methods = cls.getMethods();
int cnt = 0; //统计出现异常的次数
//将异常具体信息写入文件中
BufferedWriter bw = new BufferedWriter(new FileWriter("bug.txt"));
for (Method method : methods) {
if(method.isAnnotationPresent(Check.class)){
//判断方法上是否有Check注解,若有则检查异常
try {
method.invoke(cal);
} catch (Exception e) {
cnt ++;
Throwable tmp = e.getCause();
bw.write(method.getName()+ " 方法出异常了");
bw.newLine();
if(tmp != null) {
//注意,要避免tmp是个null(我在教程视频中发现的错误)
bw.write("异常的名称:"
+ tmp.getClass().getSimpleName() + "\n");
bw.write("异常的原因:"
+ tmp.getMessage() + "\n");
}
bw.write("--------------------------\n");
}
}
}
bw.write("本次测试一共出现了 " + cnt + " 次的异常");
bw.flush();
bw.close();
}
}

运行结果:写入了bug.txt文件中:

1
2
3
4
5
6
7
8
9
mulNums 方法出异常了
异常的名称:NullPointerException
异常的原因:Cannot invoke "String.toString()" because "str" is null
--------------------------
divNums 方法出异常了
异常的名称:ArithmeticException
异常的原因:/ by zero
--------------------------
本次测试一共出现了 2 次的异常

`