单元测试作用保障软甲质量和节省开发精力的重要手段,是每个码农必备的技能。
JUNIT是JAVA中最常用的单元测试框架,下面介绍其中的重要知识点。
1,Junit 核心概念:
Assert | 断言用来判断具体测试条件是否满足,满足则静默,反之抛出异常。 |
Test |
一个测试方法,用注解@Test定义,JUnit跑测试方法时,先创建Test Class的instance, 再调用@Test注解的测试方法。有几个测试方法,Test Class的instance就会被创建几遍。 |
Test Class | A test class is the container for @Test method. |
Suite | The Suite allows to group test classes together. |
Runner | The Runner class runs tests. |
2、断言
assertArrayEquals(expecteds, actuals) | 查看两个数组是否相等。 |
assertEquals(expected, actual) | 查看两个对象是否相等。类似于字符串比较使用的equals()方法 |
assertNotEquals(first, second) | 查看两个对象是否不相等。 |
assertNull(object) | 查看对象是否为空。 |
assertNotNull(object) | 查看对象是否不为空。 |
assertSame(expected, actual) | 查看两个对象的引用是否相等。类似于使用“==”比较两个对象 |
assertNotSame(unexpected, actual) | 查看两个对象的引用是否不相等。类似于使用“!=”比较两个对象 |
assertTrue(condition) | 查看运行结果是否为true。 |
assertFalse(condition) | 查看运行结果是否为false。 |
assertThat(actual, matcher) | 查看实际值是否满足指定的条件 |
fail() | 让测试失败 |
3、注解
@Before | 初始化方法 |
@After | 释放资源 |
@Test | 测试方法,在这里可以测试期望异常和超时时间 |
@Ignore | 忽略的测试方法 |
@BeforeClass | 针对所有测试,只执行一次,且必须为static void |
@AfterClass | 针对所有测试,只执行一次,且必须为static void |
@RunWith | 指定测试类使用某个运行器 |
@Parameters | 指定测试类的测试数据集合 |
@Rule | 允许灵活添加或重新定义测试类中的每个测试方法的行为 |
@FixMethodOrder | 指定测试方法的执行顺序 |
这里插一句,@FixMethodOrder的参数为 MethodSorters是个Enum,有三个值:
- MethodSorters.JVM
- MethodSorters.DEFAULT
- MethodSorters.NAME_ASCENDING
JVM和DEFAULT都没法自定义顺序,只有NAME_ASCENDING是有意义的,
但是一般需要修改test方法名来适应NAME_ASCENDING排序,个人感觉基本也没啥意义。
测试执行顺序:
一个测试类单元测试的执行顺序为:
@BeforeClass –> @Before –> @Test –> @After –> @AfterClass
每一个测试方法的调用顺序为:
@Before –> @Test –> @After
需要注意的是,实际打印到console中的顺序可能并不是上面的顺序,在打印信息中加入时间信息,就能修正这种错误的印象。
1 public class SumCalculator { 2 3 4 public Integer sum(List<Integer> intMembers) { 5 if (null == intMembers) { 6 return 0; 7 } else { 8 return intMembers.stream().reduce(0, (e0, e1) -> e0 + e1); 9 } 10 } 11 }
import java.time.LocalTime; import java.util.ArrayList; import java.util.Arrays; public class SumCalculatorTest { private SumCalculator calculator = new SumCalculator(); public SumCalculatorTest() { System.out.println("constructor called!"); } @BeforeClass public static void setUpClass() throws Exception { System.out.println(LocalTime.now() + ": BeforeClass"); } @AfterClass public static void tearDownClass() throws Exception { System.out.println(LocalTime.now() + ": AfterClass"); } @Before public void setUp() throws Exception { System.out.println(LocalTime.now() + ": Before"); } @After public void tearDown() throws Exception { System.out.println(LocalTime.now() + ": After"); } // @Ignore @Test public void testSumOfNull() throws Exception { System.out.println(LocalTime.now() + ": test testSumOfNull about to run"); int sumOfNull = this.calculator.sum(null); Assert.assertTrue(0 == sumOfNull); } @Test public void testSumOfEmptyList() throws Exception { System.out.println(LocalTime.now() + ": test testSumOfEmptyList about to run"); int sumOfNull = this.calculator.sum(new ArrayList<>()); Assert.assertTrue(0 == sumOfNull); } @Test public void testSum() throws Exception { System.out.println(LocalTime.now() + ": test testSum about to run"); int sumOfNull = this.calculator.sum(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9)); Assert.assertTrue(45 == sumOfNull); } }
运行结果:
13:05:07.033: BeforeClass constructor called! 13:05:07.039: Before 13:05:07.039: test testSum about to run 13:05:07.062: After constructor called! 13:05:07.065: Before 13:05:07.065: test testSumOfEmptyList about to run 13:05:07.067: After constructor called! 13:05:07.070: Before 13:05:07.070: test testSumOfNull about to run 13:05:07.071: After 13:05:07.071: AfterClass
4 参数化测试
有时在测试一个逻辑点时,会遇到测试多种类型输入的情形,最容易想到的方法是,
写多个test method, 它们仅仅是输入输出不同。而使用参数化测试就可以简化测试代码,具体来说,
当运行参数化测试时,test runner会创建多个test class的instance, 具体创建个数等于 参数个数 * test method个数。
运行参数化测试需要Parameterized作为Runner。
import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import java.util.Arrays; import java.util.Collections; import java.util.List; @RunWith(Parameterized.class) public class ParameterizedSumCalculatorTest { @Parameterized.Parameters(name = "{index}: sum[{0}]={1}") public static Object[][] data() { return new Object[][]{ {null, 0}, {Collections.EMPTY_LIST, 0}, {Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9), 45} }; } private SumCalculator calculator = new SumCalculator(); private List<Integer> inputList; private Integer expected; public ParameterizedSumCalculatorTest(List<Integer> inputList, Integer expected) { this.inputList = inputList; this.expected = expected; System.out.println("constructor called!"); } @Test public void test0() { System.out.println("test 0"); System.out.println(inputList); Assert.assertEquals(expected, calculator.sum(inputList)); } @Test public void test1() { System.out.println("in test1"); System.out.println(inputList); Assert.assertEquals(expected, calculator.sum(inputList)); } }
在上面的代码中, 用注解@Parameters标记提供测试参数的方法,即上面的static方法data(),data()方法返回二维数组。
注解的参数name是一个模板,其中模板参数{index}表示具体使用的测试参数在二维数组中的index,
{0}和{1}表示测试参数是内层数组中的第几个参数,例如:在上面的代码中,当index=1时,name="1: sum[[]]=0"。
那这个name模板的作用是啥呢,其实就是当测试用例跑不过的时候,用来识别是哪一组/多组参数跑失败了。
Parameterized作为Runner会为每组参数和每个测试方法创建测试Instance,在创建Instance过程中,
需要把参数赋值给对象中的字段“inputList”和“expected”,赋值方式有两种:
(1)添加使用“inputList”和“expected”作为初始化参数的构造函数
如上例所示;
(2)使用无初始化参数的构造函数,把field“inputList”和“expected”的访问限定符改为public,同时
添加注解@Parameter(),括号中填入参数的位置,例如:
@Parameter(0) public List<Integer> inputList; @Parameter(1) public Integer expected;
其中无参构造函数可以省略。
5 打包测试Suite
如果想要一次性跑多个Test Class下的测试用例,可以新建一个Test Class,设置其Runner为Suite.class,
然后,在加入注解@Suite.SuiteClasses({ TestClassA.class, TestClassB.class, ... })就可以了。
import org.junit.runner.RunWith; import org.junit.runners.Suite; @RunWith(Suite.class) @Suite.SuiteClasses({TestClassA.class, TestClassB.class}) public class AllCaseTest { }
6 Rule
在4.12版本中,官方推荐使用TestRule接口,所以,从TestRule着手,TestRule长这样:
public interface TestRule { Statement apply(Statement base, Description description); }
在TestRule的注释中,TestRule被描述为 an alteration in how a test is run and reported.
个人理解TestRule是一种可以影响测试过程和结果记录的组件,具体来说,
(1)它可以是test种用到的资源,比如文件系统,数据库连接等,测试前建立,测试后销毁;
(2)或者是添加对test的额外检查;
(3)或者是用于记录test结果等。
TestRule可以使用两种注解来声明:
(1)@org.junit.Rule : Rule用在public, non-static的Field或者Method上,
(2)@org.junit.ClassRule: ClassRule用在Class上。
例子:在测试执行前创建temporaryFolder,在执行后被删除。
public static class HasTempFolder { @Rule public TemporaryFolder folder= new TemporaryFolder(); @Test public void testUsingTempFolder() throws IOException { File createdFile= folder.newFile("myfile.txt"); File createdFolder= folder.newFolder("subfolder"); // ... } }
@RunWith(Suite.class) @SuiteClasses({A.class, B.class, C.class}) public class UsesExternalResource { public static Server myServer= new Server(); @ClassRule public static ExternalResource resource= new ExternalResource() { @Override protected void before() throws Throwable { myServer.connect(); } @Override protected void after() { myServer.disconnect(); } }; }
TestRule中的ExpectedException用于检测test是否抛出了期望的Exception,抛出则测试通过,反之,则测试不通过。
@Rule public ExpectedException thrownRule = ExpectedException.none(); @Test public void throwsExceptionWithSpecificType() { thrownRule.expect(NullPointerException.class); throw new NullPointerException(); }
执行顺序:TestRules在@Before,@After,@BeforeClass,@AfterClass之前执行。
7 其他
(1)限时测试:在@Test中加入timeout参数就可以:
@Test(timeout=1000) public void testWithTimeout() { ... }
(2)@Ignore注解
忽略这个test medhot
8 Spring测试框架
参考文章写得很好,这里直接给出链接: Junit使用教程(四)。
参考文章:
【1】JUnit in Action 2E by Tahciver, Leme, Massol, and Gregory;
【2】Junit 使用教程(二,三,四) by 鹏霄万里展雄飞。
【3】JUnit 官网