在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。 程序单元是应用的最小可测试部件。简单来说,就是测试数据的稳定性是否达到程序的预期。
黑盒测试又称功能测试。它通过测试来检验程序是否能正常使用。在测试过程中,我们把程序看作为一个打不开的盒子,黑黑的什么也看不见,内部代码怎么写的也不知道。也就是说完全不考虑任何内部结构和性能的情况下为程序传入(Input)参数,然后去查看程序输出(Output)是否在正常范围内,通常这时候我们需要多此测试才得出结论。
白盒测试又称结构测试。在这里白盒测试与黑盒测试不同,在测试过程中,我们可以把程序看作为一个可以看到的白色透明盒子,我们清楚盒子内部的代码和结构。我们在使用白盒测试的时候,测试人员必须检查程序的内部结构,而要从检查程序的逻辑开始,一步一步的检查传入参数(Input)并查看程序的运行过程和输出(Output)结果,最终得出测试数据。这也就是“白盒测试”为什么叫穷举路径测试的原因,再次强调,是因为我们清楚程序的内部结构和代码,从而检查所有结构的正确与否和预期值。
在这里我们忘掉单元测试,使用平时我们自己测试的方式来测试数据,看看它有什么缺点。首先,我先创建在一个计算器类,在其中随便创建两个运算方法,供我们模拟测试。
package cn.itcast.junit; /* 计算器类 */ public class Calculator { public int add (int a, int b){ return a + b; } public int sub (int a, int b){ return a - b; } }
然后我们再去编写测试类,创建对象,先去测试加法。
package cn.itcast.junit; public class CalculatorTest { public static void main(String[] args) { Calculator c = new Calculator(); int result = c.add(1, 2); System.out.println(result); } }
测试后,我们查看结果为正确的,然后进行下一步测试。因为我们有两条数据需要测试,平时在测试完一条数据后需要把测试过的数据注释掉,再进行接下来的测试。如下:
package cn.itcast.junit; public class CalculatorTest { public static void main(String[] args) { Calculator c = new Calculator(); int result1 = c.sub(1, 2); System.out.println(result1); } }
单元测试的编码规范有这几条,小伙伴们拿小本本记好了!
被测试类名Test
构成。例如:CalculatorTestxxx.xxx.xxx.test
包中。例如:package cn.itcast.test;test测试方法
和测试方法
。例如:testAdd和addvoid
。例如:public void add();空
。例如:例如:public void add();@Test
注解来完成测试,只要是加该注解的方法,可以单独运行此方法来完成测试。junit-4.13-rc-2
和hamcrest-core-1.3
。这里我使用的是Junit4,单元测试还有Junit5,版本差异我没有做了解。主要是可以完成测试才是硬道理!@Test
注解,此时IDEA显示的@Test注解是飘红的,这时候我们使用Alt + Enter
组合键来打开导入Junit单元测试列表,然后再选择Junit4或者Junit5确定即可导入成功!这时候再查看注解就没有飘红了!断言方法 | 描述 |
---|---|
assertNull(java.lang.Object object) | 检查对象是否为空 |
assertNotNull(java.lang.Object object) | 检查对象是否不为空 |
assertEquals(long expected, long actual) | 检查long类型的值是否相等 |
assertEquals(double expected, double actual, double delta) | 检查指定精度的double值是否相等 |
assertFalse(boolean condition) | 检查条件是否为假 |
assertTrue(boolean condition) | 检查条件是否为真 |
assertSame(java.lang.Object expected, java.lang.Object actual) | 检查两个对象引用是否引用同一对象(即对象是否相等) |
assertNotSame(java.lang.Object unexpected, java.lang.Object actual) | 检查两个对象引用是否不引用统一对象(即对象不等) |
首先,我们先去按照Junit单元测试规范来书写测试代码,如下:
然后我们会发现每一个需要测试的方法左边都有一个绿色的小三角,这是用来单元测试运行的。也就是说,我们可以只运行某一个方法去测试。现在我们去运行add()方法,结果如下:
这时候,我们发现控制台是绿色的并输出的打印结果,这说明我们的程序没有问题。如果我再其中加入一个算数异常会有怎么样的结果呢?如下:
在这里我们会发现,控制台变为了红色,并给出来报错信息。这证明了我们的程序测试后出现了问题。这仅是程序正确与失败的关系。
如果我们需要一个预期值呢?那么测试的结果不是我想要的预期值,而程序还是绿色的,证明程序没有问题怎么办呢?有的小伙伴会说,我们已经查看了打印控制台的信息,打印结果不是预期值就说明程序有问题,需要去修改呗。对,其实这样说是没有任何毛病的。但是,我们在开发中,如果由于你的疏忽或者疲劳看到了绿色就觉得程序没有问题怎么办呢?所以面对这个问题,我们在单元测试的时候,尽量不要去打印预期值,需要注重观察是绿色和红色比较好,它可以直观的反映程序的是否准确性和达到预期值。
这时候,我们就需要引入一个对象的静态方法来断言是否为预期值。
Assert.assertEquals(预期值, 结果);
这时候我们发现Assert句点出来的方法可以既可以断言数组,也可以断言普通数据类型。所以这时候我们就来使用它断言预期值。如下:
注意: 我们使用断言的时候尽量不要去断言Double对象。对于双精度数,绝对有必要使用增量进行比较,以避免浮点舍入的问题。如果您使用assertEquals带有double参数的3参数版本。
assertEquals(double expected, double actual, double delta);
这样以来Double将被自动取消装箱,double并且一切正常,这样测试结果就不会失败。否则使用两个参数的来断言double类型,会有如下报错信息:
我们在上述,你是否会发现有一些重复操作呢?比如,我们每一个方法都需要去new对象。有些聪明的小伙伴会说,我们可以把它提到类的里面与方法同级。对,这个处理方式也是一个正解。
但是我们在Junit单元测试中,有一个@Before注解,是用作资源的申请。也就是被@Before注解修饰的的方法会在测试方法之前自动执行。所以,我们可以去定义一个init方法,去初始化这个创建对象的过程。这就是@Before注解的作用!
有些应用场景,比如IO流的读写操作。如果我们要测试此代码,是需要一个关闭流的过程,通过我们关闭流使用finally块来保证最后流的关闭操作。这时,我们在Junit单元测试中,有一个@After注解,是用作资源的关闭。也就是说被@After注解修饰的方法会在测试方法之后自定执行。所以,我们在特定需要保证最后关闭、销毁资源的时候,可以去定义一个close方法,去关闭或销毁资源。这就是@After注解的作用!
注意: @Before和@After注解在程序报错的时候,仍然可以保证数据的初始化和关闭销毁,两个方法是依旧执行的。这里有点像我们tomact服务器的初始阶段和销毁阶段,它们的执行不受任何影响。
import cn.itcast.junit.Calculator; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; public class CalculatorTest { /* 初始化方法: 用于资源申请,所有测试方法在执行之前都还先执行该方法 */ @Before public void init(){ System.out.println("init……"); } /* 释放资源方法: 在所有测试方法执行完后,都会自动执行该方法 */ @After public void close(){ System.out.println("close……"); } /* 测试add方法 */ @Test public void testAdd(){ //System.out.println("我被执行了!"); //1.创建计算器对象 Calculator c = new Calculator(); //2.调用add方法 int result = c.add(1, 2); System.out.println("testAdd……"); //3.断言 我断言这个结果的3 Assert.assertEquals(3, result); } }
框架设计的灵魂:反射
package cn.itcast.domain; public class Person { private String name; private int age; public String a; protected String b; String c; private String d; public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + ", a='" + a + '\'' + ", b='" + b + '\'' + ", c='" + c + '\'' + ", d='" + d + '\'' + '}'; } }
import cn.itcast.domain.Person; public class ReflectDemo01 { /* 获取Class对象的方式: 1. Class.forName("全类名"):将字节码文件加载进内存,返回Class对象 2. 类名.class:通类名的属性class获取 3. 对象.getClass():getClass()方法在Object类中定义着。返回此 Object的运行时类。 */ public static void main(String[] args) throws ClassNotFoundException { //1. Class.forName("全类名"):将字节码文件加载进内存,返回Class对象 Class<?> cls1 = Class.forName("cn.itcast.domain.Person"); System.out.println(cls1); //2. 类名.class:通类名的属性class获取 Class cls2 = Person.class; System.out.println(cls2); //3. 对象.getClass():getClass()方法在Object类中定义着。返回此 Object的运行时类。 Person p = new Person(); Class<? extends Person> cls3 = p.getClass(); System.out.println(cls3); //== 比较三个对象 System.out.println(cls1 == cls2);//true System.out.println(cls2 == cls3);//true } }
获取功能:
Field[] getFields();
返回包含一个数组 Field对象反射由此表示的类或接口的所有可访问的公共字段 类对象。Field getField(String name);
返回一个 Field对象,它反映此表示的类或接口的指定公共成员字段 类对象。Field getDeclaredField(String name);
返回一个 Field对象,它反映此表示的类或接口的指定已声明字段 类对象。Field[] getDeclaredFields();
返回的数组 Field对象反映此表示的类或接口声明的所有字段 类对象。void set(Object obj, Object value);
将指定对象参数上的此 Field对象表示的字段设置为指定的新值。Object get(Object obj);
返回该所表示的字段的值 Field ,指定的对象上。void setAccessible(boolean flag)
import cn.itcast.domain.Person; import java.lang.reflect.Field; public class ReflectDemo02 { public static void main(String[] args) throws Exception { //1.获取Person的Class对象 Class personClass = Person.class; /* */ //1.Field[] getFields()获取所有public修饰的成员变量 Field[] fields = personClass.getFields(); for (Field field : fields) { System.out.println(field);//public java.lang.String cn.itcast.domain.Person.a } System.out.println("======================"); Field a = personClass.getField("a"); //获取成员变量a的值 Person p = new Person(); Object value = a.get(p); System.out.println(value);//null //设置a的值 a.set(p,"张三"); System.out.println(p); System.out.println("======================"); //Field[] getDeclaredFields():获取所有的成员变量,不考虑修饰符 Field[] declaredFields = personClass.getDeclaredFields(); for (Field declaredField : declaredFields) { System.out.println(declaredField); } System.out.println("======================"); Field d = personClass.getDeclaredField("d"); //忽略访问权限修饰符的安全检查 d.setAccessible(true);//暴力反射 Object value2 = d.get(p); System.out.println(value2);//null } }
Constructor<T> getConstructor(Class<?>... parameterTypes);
返回一个 Constructor对象,该对象反映 Constructor对象表示的类的指定的公共 类函数。Constructor<?>[] getConstructors();
返回包含一个数组 Constructor对象反射由此表示的类的所有公共构造 类对象。Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes);
返回一个 Constructor对象,该对象反映 Constructor对象表示的类或接口的指定 类函数。Constructor<?>[] getDeclaredConstructors();
返回一个反映 Constructor对象表示的类声明的所有 Constructor对象的数组 类 。T newInstance(Object... initargs);
使用此 Constructor对象表示的构造函数,使用指定的初始化参数来创建和初始化构造函数的声明类的新实例。如果使用空参数构造方法创建对象,操作可以简化,使用Class对象中的
T newInstance()
创建由此 类对象表示的类的新实例。
import cn.itcast.domain.Person; import java.lang.reflect.Constructor; public class ReflectDemo03 { public static void main(String[] args) throws Exception { //1.获取Person的Class对象 Class personClass = Person.class; //public Constructor<T> getConstructor(Class<?>... parameterTypes)...是可变参数 //Class<?>... parameterTypes是构造方法的参数列表 Constructor constructor = personClass.getConstructor(String.class, int.class); System.out.println(constructor); //创建对象 Object person = constructor.newInstance("张三", 23); System.out.println(person); System.out.println("======================="); Constructor constructor1 = personClass.getConstructor(); System.out.println(constructor1); //创建对象 Object person1 = constructor1.newInstance(); System.out.println(person1); Object o = personClass.newInstance(); System.out.println(o); } }
Method getDeclaredMethod(String name, Class<?>... parameterTypes);
返回一个方法对象,它反映此表示的类或接口的指定声明的方法类对象。Method[] getDeclaredMethods();
返回包含一个数组 方法对象反射的类或接口的所有声明的方法,通过此表示 类对象,包括公共,保护,默认(包)访问和私有方法,但不包括继承的方法。Method getMethod(String name, Class<?>... parameterTypes);
返回一个方法对象,它反映此表示的类或接口的指定公共成员方法 类对象。Method[] getMethods();
返回包含一个数组方法对象反射由此表示的类或接口的所有公共方法类对象,包括那些由类或接口和那些从超类和超接口继承的声明。Object invoke(Object obj, Object... args);
在具有指定参数的 方法对象上调用此 方法对象表示的底层方法。String getName();
返回由此方法对象表示的方法的名称,作为 String 。import cn.itcast.domain.Person; import java.lang.reflect.Method; public class ReflectDemo04 { public static void main(String[] args) throws Exception { //1.获取Person的Class对象 Class personClass = Person.class; //Method getMethod(String name, Class<?>... parameterTypes); //name:表示方法名称 //Class<?>... parameterTypes:表示指定方法的参数列表 Method eat = personClass.getMethod("eat"); System.out.println(eat); Person p = new Person(); //执行方法 eat.invoke(p); Method eat1 = personClass.getMethod("eat", String.class); eat1.invoke(p, "吃饭"); System.out.println("======================="); //获取所有public修饰的方法 Method[] methods = personClass.getMethods(); for (Method method : methods) { System.out.println(method); String name = method.getName(); System.out.println(name); } System.out.println("========================"); String name = personClass.getName(); System.out.println(name);//cn.itcast.domain.Person } }
String getName();
返回由 类对象表示的实体(类,接口,数组类,原始类型或空白)的名称,作为 String。需求:写一个"框架",可以帮我们创建任意类的对象,并且执行其中任意方法
步骤:
实现:
className = cn.itcast.domain.Student methodName = sleep
import java.io.InputStream; import java.lang.reflect.Method; import java.util.Properties; public class ReflectTest { public static void main(String[] args) throws Exception { //可以创建任意类的对象,可以执行任意方法 /* 前提:不能改变该类的任何代码。可以创建任意类的对象,可以执行任意方法 */ //1.加载配置文件 //1.1创建Properties对象 Properties pro = new Properties(); //1.2加载配置文件,转换为一个集合 //1.2.1获取class目录下的配置文件(src目录下的配置文件) ClassLoader classLoader = ReflectTest.class.getClassLoader();//获取加载对象 InputStream is = classLoader.getResourceAsStream("pro.properties"); pro.load(is); //2.获取配置文件中定义的数据 String className = pro.getProperty("className"); String methodName = pro.getProperty("methodName"); //3.加载该类进内存 Class aClass = Class.forName(className); //4.创建对象 Object o = aClass.newInstance(); //5.获取方法对象 Method method = aClass.getMethod(methodName); //6.执行方法 method.invoke(o); } }
Java 注解(Annotation)
格式:
元注解 public @interface 注解名称 { 属性列表; }
注解本质上就是一个接口,该接口默认继承Annotation接口
public interface MyAnno extends java.lang.annotation.Annotation {}
属性:接口中的抽象方法
要求:
用于描述注解的注解
//其实就是在内存中生成了一个该注解接口的子类实现对象 /* public class ProImpl implements Pro{ public String className(){ return "cn.itcast.annotation.Demo1"; } public string methodName(){ return "show" ; } }
package cn.itcast.annotation.demo; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Check { } package cn.itcast.annotation.demo; public class Calculator { //加法 @Check public void add() { System.out.println("1 +0 =" + (1 + 0)); } //减法 @Check public void sub() { System.out.println("1 -0 =" + (1 - 0)); } //乘法 @Check public void mul() { System.out.println("1 * =" + (1 * 0)); } //除法 @Check public void div() { System.out.println("1 / e =" + (1 / 0)); } public void show() { System.out.println("永无bug..."); } } package cn.itcast.annotation.demo; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.lang.reflect.Method; /* 简单的测试框架 当主方法执行后,会自动执行被检测的所有方法(加了Check注解的方法),判断方法是否有异常,记录到文件中 */ public class TestCheck { public static void main(String[] args) throws IOException { //1.创建计算器对象 Calculator c = new Calculator(); //2.获取字节码文件对象 Class cls = c.getClass(); //3获取所有方法 Method[] methods = cls.getMethods(); int number = 0;//出现异常的次数 BufferedWriter bw = new BufferedWriter(new FileWriter("bug.txt")); for (Method method : methods) { //4.判断方法上是否有Check注解 if (method.isAnnotationPresent(Check.class)){ //5.有,执行 try{ method.invoke(c); }catch (Exception e){ //6.捕获异常 //记录到文件中 number++; bw.write(method + "方法出异常了"); bw.newLine(); bw.write("异常的名称" + e.getCause().getClass().getSimpleName()); bw.newLine(); bw.write("异常的原因:" + e.getCause().getClass()); bw.newLine(); bw.write("------------------------------"); bw.newLine(); } } } bw.write("本次测试一共出现" + number + "次"); bw.flush(); bw.close(); } }