Junit4单元测试

Junit4单元测试

官方文档

第一部分 用法

1.1 常见功能

典型配置:

/*用于配置spring Boot中测试的环境*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MyBlogApplication.class)
/* 开启事务,测试完成默认自动回滚,不会弄脏数据库 */
@Transactional

public class WhoHaveWhatTagsMapperTest {

    @BeforeClass
    public static void beforeClass() {
    }

    @Before
    public void setUp() throws Exception {
    }

    @After
    public void tearDown() throws Exception {
    }

    @Test
    public void insertWhoHaveWhatTags() throws Exception {
    }

    @Test
    public void selectBlogByTag() throws Exception {
    }

    @Test
    public void deleteWhoHaveWhatTags() throws Exception {
    }

}
  • @Test:把一个方法标记为测试方法
    两个属性:
    excepted;表示测试在执行中期望抛出的异常类型,如果不抛出,反而报错。
    timeout:超时抛出异常。单位毫秒
    @Test(timeout = 2000)
    @Test(expected = Exception.class)
    public void testFactorialException() throws Exception {
        new Math().factorial(-1);
        fail("factorial参数为负数没有抛出异常");
    }
  • @Before:每一个测试方法执行前自动调用一次

  • @After:每一个测试方法执行完自动调用一次

  • @BeforeClass:所有测试方法执行前执行一次,在测试类还没有实例化就已经被加载,所以用static修饰

  • @AfterClass:所有测试方法执行完执行一次,在测试类还没有实例化就已经被加载,所以用static修饰

  • @Ignore:暂不执行该测试方法

  • setup方法主要实现测试前的初始化工作

  • teardown方法主要实现测试完成后垃圾回收工作!

setup方法主要实现测试前的初始化工作,teardown方法主要实现测试完成后垃圾回收工作!
测试方法的声明要求:名字可以随便取,没有任何限制,但是返回值必须为void,而且不能有任何参数。

  • 参数化测试
    你可能遇到过这样的函数,它的参数有许多特殊值,或者说他的参数分为很多个区域。比如测试一下“计算一个数的平方”这个函数,暂且分三类:正数、0、负数。测试代码如下:
public class CalculatorTest {
	private static Calculator calculator = new Calculator();//这个类要自己写

	@Before
	public void clearCalculator() {
		calculator.clear();
	}
	@Test
	public void square1() {
		calculator.square(2);
		assertEquals(4, calculator.getResult());
	}
	@Test
	public void square2() {
		calculator.square(0);
		assertEquals(0, calculator.getResult());
	}
	@Test
	public void square3() {
		calculator.square(-3);
		assertEquals(9, calculator.getResult());
	}
}

为了简化类似的测试,JUnit4提出了“参数化测试”的概念,只写一个测试函数,把这若干种情况作为参数传递进去,一次性的完成测试。代码如下:

@RunWith(Parameterized.class)
public class SquareTest {
	private static Calculator calculator = new Calculator();
	private int param;
	private int result;

	@Parameters
        //输入的参数和预期的结果
	public static Collection data() {
		return Arrays.asList(new Object[][] { { 2, 4 }, { 0, 0 }, { -3, 9 }, });
	}
	// 构造函数,对变量进行初始化
	public SquareTest(int param, int result) {
		this.param = param;
		this.result = result;
	}
	@Test
	public void square() {
		calculator.square(param);
		assertEquals(result, calculator.getResult());
	}
}
  • 打包测试
    在一个项目中,只写一个测试类是不可能的,我们会写出很多很多个测试类。可是这些测试类必须一个一个的执行,也是比较麻烦的事情。鉴于此,JUnit为我们提供了打包测试的功能,将所有需要运行的测试类集中起来,一次性的运行完毕,大大的方便了我们的测试工作。具体代码如下:
import org.junit.runner.RunWith;
import org.junit.runners.Suite;

@RunWith(Suite.class)
@Suite.SuiteClasses({CalculatorTest.class, SquareTest.class})
public class AllCalculatorTests{}

从上面可以看到,这个功能也需要使用一个特殊的Runner,因此我们需要向@RunWith标注传递一个参数Suite.class。同时,我们还需要另外一个标注@Suite(SuiteClasses),来表明这个类是一个打包测试类。我们把需要打包的类作为参数传递给该标注就可以了。有了这两个标注之后,就已经完整的表达了所有的含义,因此下面的类已经无关紧要,随便起一个类名,内容全部为空既可。

  • Assume 对待测方法的参数进行合法性校验的,如果校验不合格则直接抛异常,而不执行测试。
    Assume提供的校验规则如下:
      assumeTrue/assumeFalse、 assumeNotNull、 assumeThat、 assumeNoException 

例如:(通过下述代码也可以看到,要使用参数,则应使用@Theory注解)

@Theory
public void printAge(String name, int age){
        Assume.assumeTrue(age > 0);//如果参数age<=0,会抛AssumptionViolatedException异常
        System.out.println(String.format("%s's Name is %s.", name, age));
}
  • Assert 是Junit提供的断言,与Assume不同,Assert是对测试结果的校验,它提供的检验规则如下:
    AssertTrue、AssertFalse:结果的true、false。
    AssertThat:使用Matcher做自定义的校验。
    AssertEquals、AssertNotEquals:判断两个对象是否相等。
    AssertNull、AssertNotNull:判断对象是否为空。
    AssertSame:判断两个对象是否为同一个,不同于equals这里是使用“==”判断。
    AssertArrayEquals:判断两个数组是否相等。

  • 多线程测试
    JUnit4的Test写好以后,对于一些集成度比较高的测试用例,还希望完成并发访问情况下的测试,但是,JUnit4缺省情况没有提供,可以通过自己写一个main函数,然后创建几个线程,在几个线程中同时运行测试用例进行测试,来模拟并发访问的情况,具体例子:

public class TestExample {

    @Test
    public void testMethod() {
    System.out.println("test success!");
    }
}

public class PerfomanceTest {
    public static void main(String[] args) {
        new Thread() {
            public void run() { 
                // JUnitCore.runClasses(new Class[] { TestExample.class });           (1)
                // new JUnitCore().run(Request.method(TestExample.class, "testMethod"));        (2)
            }
        }.start();
    }
}

注:标志1或标志2中只要用一种就可以测试。

1.2 Spring的@Transactional注解用法

参考:http://www.cnblogs.com/yepei/p/4716112.html
事务管理对于企业应用来说是至关重要的,即使出现异常情况,它也可以保证数据的一致性。
Spring Framework对事务管理提供了一致的抽象,其特点如下:

  • 为不同的事务API提供一致的编程模型,比如JTA(Java Transaction API), JDBC, Hibernate, JPA(Java Persistence API和JDO(Java Data Objects)
  • 支持声明式事务管理,特别是基于注解的声明式事务管理,简单易用
  • 提供比其他事务API如JTA更简单的编程式事务管理API
  • 与spring数据访问抽象的完美集成

事务管理方式
spring支持编程式事务管理和声明式事务管理两种方式。
编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。

声明式事务管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。

显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。

声明式事务管理也有两种常用的方式,一种是基于tx和aop名字空间的xml配置文件,另一种就是基于@Transactional注解。显然基于注解的方式更简单易用,更清爽。

默认情况下,数据库处于自动提交模式。每一条语句处于一个单独的事务中,在这条语句执行完毕时,如果执行成功则隐式的提交事务,如果
执行失败则隐式的回滚事务。

对于正常的事务管理,是一组相关的操作处于一个事务之中,因此必须关闭数据库的自动提交模式。不过,spring会将底层连接的自动提交特性设置为false。
连接关闭时默认的策略是回滚任何未提交的事务

MyBatis自动参与到spring事务管理中,无需额外配置,只要org.mybatis.spring.SqlSessionFactoryBean引用的数据源与DataSourceTransactionManager引用的数据源一致即可,否则事务管理会不起作用。

spring事务特性

spring所有的事务管理策略类都继承自org.springframework.transaction.PlatformTransactionManager接口

public interface PlatformTransactionManager {
 
  TransactionStatus getTransaction(TransactionDefinition definition)
    throws TransactionException;
 
  void commit(TransactionStatus status) throws TransactionException;
 
  void rollback(TransactionStatus status) throws TransactionException;
}

其中TransactionDefinition接口定义以下特性:

事务隔离级别

隔离级别是指若干个并发的事务之间的隔离程度。TransactionDefinition 接口中定义了五个表示隔离级别的常量:
•TransactionDefinition.ISOLATION_DEFAULT:这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是TransactionDefinition.ISOLATION_READ_COMMITTED。
•TransactionDefinition.ISOLATION_READ_UNCOMMITTED:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。比如PostgreSQL实际上并没有此级别。
•TransactionDefinition.ISOLATION_READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。
•TransactionDefinition.ISOLATION_REPEATABLE_READ:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读。
•TransactionDefinition.ISOLATION_SERIALIZABLE:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

事务传播行为

所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在TransactionDefinition定义中包括了如下几个表示传播行为的常量:
•TransactionDefinition.PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。
•TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
•TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
•TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
•TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
•TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
•TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

事务超时

所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒。
默认设置为底层事务系统的超时值,如果底层数据库事务系统没有设置超时值,那么就是none,没有超时限制。

事务只读属性

只读事务用于客户代码只读但不修改数据的情形,只读事务用于特定情景下的优化,比如使用Hibernate的时候。
默认为读写事务。

spring事务回滚规则

指示spring事务管理器回滚一个事务的推荐方法是在当前事务的上下文内抛出异常。spring事务管理器会捕捉任何未处理的异常,然后依据规则决定是否回滚抛出异常的事务。

默认配置下,spring只有在抛出的异常为运行时unchecked异常时才回滚该事务,也就是抛出的异常为RuntimeException的子类(Errors也会导致事务回滚),而抛出checked异常则不会导致事务回滚。
可以明确的配置在抛出那些异常时回滚事务,包括checked异常。也可以明确定义那些异常抛出时不回滚事务。

还可以编程性的通过setRollbackOnly()方法来指示一个事务必须回滚,在调用完setRollbackOnly()后你所能执行的唯一操作就是回滚。

@Transactional注解

属性 类型 描述
value String 可选的限定描述符,指定使用的事务管理器
propagation enum: Propagation 可选的事务传播行为设置
isolation enum: Isolation 可选的事务隔离级别设置
readOnly boolean 读写或只读事务,默认读写
timeout int (in seconds granularity) 事务超时时间设置
rollbackFor Class对象数组,必须继承自Throwable 导致事务回滚的异常类数组
rollbackForClassName 类名数组,必须继承自Throwable 导致事务回滚的异常类名字数组
noRollbackFor Class对象数组,必须继承自Throwable 不会导致事务回滚的异常类数组
noRollbackForClassName 类名数组,必须继承自Throwable 不会导致事务回滚的异常类名字数组

@Transactional 可以作用于接口、接口方法、类以及类方法上。当作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。
虽然 @Transactional 注解可以作用于接口、接口方法、类以及类方法上,但是 Spring 建议不要在接口或者接口方法上使用该注解,因为这只有在使用基于接口的代理时它才会生效。另外, @Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。如果你在 protected、private 或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常。

第二部分 快速开始

2.1 在intellij idea中快速生成测试代码

  1. 将鼠标放到类的任意位置,摁下Ctrl+Shift+T,然后Create a new Test即可。

第三部分 原理

JUnit4为了保证每个测试方法都是单元测试,是独立的互不影响。所以每个测试方法执行前都会重新实例化测试类。

3.1 为什么Junit没有main()方法就能运行

Junit4可以直接运行我们的某个方法,没有main入口函数是断然不行的。其实在org.junit.runner包下,有个JUnitCore.class,其中就有一个 标准的main方法,这就是JUnit入口函数。

Runner只是一个抽象类,表示用于运行Junit测试用例的工具,通过它可以运行测试并通知Notifier运行的结果。通常我们可以在待测方法所在的类之上使用@RunWith注解来为这个测试类指定一个特定的Runner。Junit的默认Runnner------BlockJunit4ClassRunner。当我们不为测试类添加@RunWith注解的时候,其实使用的就是这个Runner,它作为默认Runner只为我们提供了基本的基于Junit生命周期的测试注解。下面列出一些比较有用的Runner。

  1. Suit------它可以一次生执行全面在多个类中的测试用例,例如:
@RunWith(Suite.class)
@SuiteClasses({Person.class, People.class})
public class TestSuitMain{
  //虽然这个类是空的,但依然可以运行Junit测试,运行时,它会将Person.class和//People.class中的所有测试用命都执行一遍!
}
  1. Parameterized------在普通的单元测试中被@Test注解标注的测试方法只能是public void的,且不能有任何输入参数。而这时常会给我们造成困扰,因为有时候我们需要为测试方法输入参数,甚至是批量指定多个待测参数。这时Parameterized这个Runner就能满足我们的要求,用法如下:
@RunWith(Parameterized.class)
public class TestGenerateParams{
    private String greeting;
    public TestGenerateParams(String greeting){
        super();
        this.greeting = greeting;
    }
    @Test
    public void testParams(){       
        System.out.println(greeting);
    }

    /**
     * 这里的返回的应该是一个可迭代数组,且方法必须是public static
     * @return
     */
    @Parameters
    public static List getParams(){
        return Arrays.asList(new String[][]{{"hello"},{"hi"},{"good morning"},{"how are you"}});
    }
}
  1. Theories------提供一组参数的排列组合值作为待测方法的输入参数。同时注意到在使用Theories这个Runner的时候,我们的待测方法可以拥有输入参数,而这在其它的Runner中的测试方法是不成的。下面是一个例子:
@RunWith(Theories.class)public class TheoriesTest{
    @DataPoint
    public static String nameValue1 = "Tony";
    @DataPoint
    public static String nameValue2 = "Jim";
    @DataPoint    public static int ageValue1 = 10;
    @DataPoint
    public static int ageValue2 = 20;
    @Theory
    public void testMethod(String name, int age){
        System.out.println(String.format("%s's age is %s", name, age));
    }
}

上面的代码的意思是,将”Tony”、”Jim”、10、20四个参数以类型合法的排列组合传给待没方法。因此输出的结果必然也有2x2=4种:

    Tony's age is 10 

    Tony's age is 20 

    Jim's age is 10 

    Jim's age is 20 

不过,为了简单,我们除了可以使用@DataPoint注解来提供参数之外,还可以通过@DataPoints注解来提供参数,参照上述代码,只需要将@DataPoint注解标注的四个字段参数替换为如下的两个即可:

@DataPoints
public static String[] names = {"Tony", "Jim"};
@DataPoints
public static int[] ageValue1 = {10, 20};

3.2 基本过程

首先明确概念:
1.TestCase
代表一个测试用例,每一个TestCase实例都对应一个测试,这个测试通过这个TestCase实例的名字标志,以便在测试结果中指明哪个测试出现了问题。
即每个@Test注解的方法分别实例化,而非每个@RunWith注解的类

2.TestSuite
代表需要测试的一组测试用例。

3.TestFixtrue
TestFixtrue代表一个测试环境。它用于组合一组测试用例,这组测试用例需要共同的测试运行环境。

过程:

初始化阶段(创建 Testcase 及 TestSuite)
首先创建一个 TestRunner 实例

public static void main (String[] args) {
  junit.textui.TestRunner.run (suite());
 }

然后,构造TestSuite:
TestSuite 采用了Composite 设计模式。在该模式下,可以将 TestSuite 比作一棵树,树中可以包含子树(其它 TestSuite),也可以包含叶子 (TestCase),以此向下递归,直到底层全部落实到叶子为止。
然后将待测试的类(class文件)作为参数传入TestSuite() 方法, TestSuite(Class theclass) 方法为 TestSuite 类的构造方法,它能自动分析 theclass 所描述的类的内部有哪些方法需要测试,并利用反射转化为TestCase对象(注意每一个TestCase都是待测试类的一次重新实例化,故互不影响,即:一个TestCase类中可以定义很多test方法,但一个TestCase实例只对应一个测试方法。),加入到新构造的 TestSuite 中。

运行阶段(运行所有的TestCase
对 TestSuite 中的整个“树结构”递归遍历运行其中的节点和叶子。

结果捕捉阶段
运行测试的结果在TestResult实例中记录,所以我们抛出Assert中的异常时,不会影响下面的测试继续运行。

3.3 Spring测试框架+junit4单元测试原理

Spring的主要测试框架的核心是TestContext,TestContextManager,TestExcutionListener接口,我们每次启动测试的时候都会创建TestContextManager,它实际上是管理了一个TestContext来负责持有一个当前测试的上下文,可以实现测试实例的依赖注入。TestContextManager还负责在测试中更新TestContext的状态并代理到TestExecutionListener,它是用来监控实际的执行(如依赖注入,管理实务等等)。

@RunWith(SpringJUnit4ClassRunner.class)  //使用junit4进行测试  
@ContextConfiguration   ({"/spring/app*.xml","/spring/service/app*.xml"}) //加载配置文件  
@Transactional  
...

第四部分 常见问题

4.1 同一个测试类内部或者不同测试类之间的@Test执行顺序

JUnit4.11之后提供了MethodSorters,在测试类上加注解@FixMethodOrder(value)可以有三种方式对test执行顺序进行指定,如下:
默认(MethodSorters.DEFAULT),按方法名(MethodSorters.NAME_ASCENDING)和JVM(MethodSorters.JVM)

  • 默认顺序由方法名hashcode值来决定,如果hash值大小一致,则按名字的字典顺序确定,不同操作系统可能顺序不同;
  • 按方法名称的进行排序,由于是按字符的字典顺序,所以以这种方式指定执行顺序会始终保持一致; 不过这种方式需要对测试方法有一定的命名规则,如 测试方法均以testNNN开头(NNN表示测试方法序列号 001-999)
    单元测试的目的就是测试最小单位的正确性,隔离和其他部分的关联,自然也不能有依赖,不然,一定测试通不过,你无法知道是单元内部的问题,还是外部环境的问题。所以我们仅仅在blog表的测试中使用了这种排序规则
  • 按JVM返回的方法名的顺序执行,此种方式下测试方法的执行顺序是不可预测的,即每次运行的顺序可能都不一样(JDK7里尤其如此).

实际上 Junit里是通过反射机制得到某个Junit里的所有测试方法,并生成一个方法的数组,然后依次执行数组里的这些测试方法;
而当用annotation指定了执行顺序,Junit在得到测试方法的数组后,会根据指定的顺序对数组里的方法进行排序;

4.2 不同的测试类之间有重复的操作,如何保证测试数据不互相影响

由于Junit4不同测试(即每一个@Test都是一个单独的单元测试,每个测试方法执行前都会重新实例化测试类)的默认执行顺序是按照方法名的hash值排序,没有并行测试。
所以可以用@Transactional 注解每个测试类,测试类内部如果没有设置事务,则默认和类相同。那么在测试中,只要我们不提交事务,Spring默认会测试完毕回滚,因此不同的测试单元之前数据互不影响。
特别注意:在test中,Spring默认测试结束就会回滚,如果不想回滚,可以用@Rollback(false)注解;
而在一般的Java类中,Spring默认只有在抛出的异常为运行时unchecked异常时才回滚该事务,也就是抛出的异常为RuntimeException的子类(Errors也会导致事务回滚),而抛出checked异常则不会导致事务回滚,我们可以用@Transactional注解的rollbackFor属性设置其他的

4.3 DAO层的测试一般insert在最前面,delete在最后,不同的测试单元之间数据需要互相使用,怎么办?

解决1(不推荐):利用@FixMethodOrder(MethodSorters.NAME_ASCENDING)注解设定按照方法名字典顺序执行测试,可以按照下面的命名方式:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MyBlogApplication.class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class BlogMapperTest1 {
    @Autowired
    private BlogMapper blogMapper;

    @Autowired
    private UserMapper userMapper;

    @Test
    public void test1InsertBlog() throws Exception {

    }

    @Test
    public void test2SelectBlogByUserUuid() throws Exception {
      
    }

    @Test
    public void test3DeleteBlogByBlogUuid() throws Exception {
    }
}

解决2: 每个单元测试都重新构造数据。。。当增删改查很多时,为了保证测试类的清晰,推荐这种方法。

解决3: 把你需要共享数据所有操作放到一个@Test注解的方法中,比较适合操作比较少的测试。

posted @ 2017-05-06 21:45  何必等明天  阅读(2592)  评论(0编辑  收藏  举报