前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >单元测试以及JUnit框架解析

单元测试以及JUnit框架解析

作者头像
幺鹿
发布2018-08-21 16:04:37
2.3K0
发布2018-08-21 16:04:37
举报
文章被收录于专栏:Java呓语Java呓语

前言

我们都有个习惯,常常不乐意去写个简单的单元测试程序来验证自己的代码。对自己的程序一直非常有自信,或存在侥幸心理每次运行通过后就直接扔给测试组测试了。然而每次测试组的BUG提交过来后就会发现自己的程序还存在许多没有想到的漏洞。但是每次修改好BUG以后还是怀着侥幸心理,认为这次不会有bug了。然后又一次自信地提交,结果又败了。因为这样反复几次后。开发者花在找BUG和修复BUG的这些时间加起来已经比他开发这个模块花的时间还要多了。虽然项目经理已经预留了修改BUG和单元测试的时间。但是开发者却习惯性地在写好代码后就认为任务完成了。 然后等问题出来了bug改了很多次还是修复不了的时候才和项目经理说“我碰到预想不到的问题,可能要延期发布我的代码“。如果这个项目不可延期,痛苦的加班就无法避免了。

BUG是不可避免的,只是每次在修复一个BUG之前基本上无法知道这个BUG是哪段代码引起。每次定位BUG可能会耗去你一个小时还是一天,这还要取决于你的水平了。但是如果你的每段核心程序都有单元测试代码。你将不需要靠你的经验去判断或猜测BUG是由哪段程序引起。你只要运行你的单元测试方法。通过简单判断测试方法的结果就可以轻松定位BUG了。所以从表面上看,为每个单元程序都编写测试代码似乎是增加了工作量,但是其实这些代码不仅为你织起了一张保护网,而且还可以帮助你快速定位错误从而使你大大减少修复BUG的时间。而且这还有利你的身体健康,你将不会因为找不出BUG而痛苦不已,也将不用废寝忘食地加班了。而且项目的进度也将尽在掌握。

其实单元测试不仅能保证项目进度还能优化你的设计。有些开发者会说,写单元测试代码太费劲了,比写业务代码还麻烦。可是如果强迫开发者必须写单元测试代码的时候。聪明且又想‘偷懒’的开发人员为了将来可以更方便地编写测试代码。唯一的办法就是通过优化设计,尽可能得将业务代码设计成更容易测试的代码。慢慢地开发者就会发现。自己设计的程序耦合度也越来越低。每个单元程序的输入输出,业务内容和异常情况都会尽可能变得简单。最后发现自己的编程习惯和设计能力也越来越老练了。

其实容易测试的代码基本上可以和设计良好的代码划等号。因为一个单元测试用例其实就是一个单元的最早用户。容易使用显然意味着良好的设计。

什么是单元测试

单元测试的目的 测试当前所写的代码是否是正确的, 例如输入一组数据, 会输出期望的数据; 输入错误数据, 会产生错误异常等。

在单元测试中, 我们需要保证被测系统是独立的,即当被测系统通过测试时,那么它在任何环境下都是能够正常工作的。编写单元测试时, 仅仅需要关注单个类就可以了,而不需要关注例如数据库服务、Web 服务等组件。

JUnit模块和说明

模块

说明

Assertions

断言,单元测试中不可或缺的组成部分

Test Runners

应该如何执行测试

Aggregating tests in Suites

如何将多个相关测试组合到一个测试套件中

Test Execution Order

指定运行单元测试的顺序

Exception Testing

如何在单元测试中指定预期的异常

Matchers and assertThat

如何使用Hamcrest匹配器和更具描述性的断言

Ignoring Tests

如何禁用测试方法或类

Timeout for Tests

如何指定测试的最长执行时间

Parameterized Tests

编写可以使用不同参数值多次执行的测试

Assumptions with Assume

类似于断言,但没有使测试失败

Rules

停止扩展抽象测试类并开始编写测试规则

Theories

使用随机生成的数据编写更像科学实验的测试

Test Fixtures

在每个方法和每个类的基础上指定设置和清理方法

Categories

将测试分组在一起以便于测试过滤

Multithreaded code and Concurrency

并发代码测试的基本思路

JUnit4 注解

  • @BeforeClass 表示该方法只执行一次,并且在所有方法之前执行。一般可以使用该方法进行数据库连接操作,注意该注解运用在静态方法。
  • @AfterClass 表示该方法只执行一次,并且在所有方法之后执行。一般可以使用该方法进行数据库连接关闭操作,注意该注解运用在静态方法。
  • @Before 表示该方法在每一个测试方法之前运行,可以使用该方法进行初始化之类的操作
  • @After 表示该方法在每一个测试方法之后运行,可以使用该方法进行释放资源,回收内存之类的操作

以上4个注解只能修饰方法,对应模块是Test Fixtures。用于执行测试用例之前,对资源的初始化以及资源清理等工作。这么做的目的是为了避免多个测试用例相互影响。

  • @Rule
  • @ClassRule

以上2个注解可以修饰域和方法,对应模块是Rules。加Class的目的用于修饰static域或方法。

  • @Ignore

当需要临时禁用一个/组测试用例时,可以在已经标注@Test的方法中继续标注@Ignore,则该测试用例会在执行时被忽略。

  • @FixMethodOrder

此类允许用户选择测试类内方法的执行顺序。

  • @Test

@Test 修饰public(Junit5 以后能支持包访问权限)的方法,但凡测试用例抛出不可预期的异常即认定为测试用例执行失败。

使用教程

Assume

假设是在断言之前增加前提条件,只有当条件成立时断言才会执行。 否则会抛出假设不通过的异常(但不会判定为测试用例失败,而是认为是忽略)。

代码语言:javascript
复制
import static org.junit.Assume.*
    @Test public void filenameIncludesUsername() {
        assumeThat(File.separatorChar, is('/'));
        assertThat(new User("optimus").configFileName(), is("configfiles/optimus.cfg"));
    }

    @Test public void correctBehaviorWhenFilenameIsNull() {
       assumeTrue(bugFixed("13356"));  // bugFixed is not included in JUnit
       assertThat(parse(null), is(new NullDocument()));
    }

Assert

JUnit为所有原始类型、对象和数组(原语或对象)提供了重载断言方法。参数顺序是期望值,其次是实际值。可选地,第一个参数可以是在失败时输出的字符串消息。

代码语言:javascript
复制
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.both;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.everyItem;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;

import java.util.Arrays;

import org.hamcrest.core.CombinableMatcher;
import org.junit.Test;

public class AssertTests {
  @Test
  public void testAssertArrayEquals() {
    byte[] expected = "trial".getBytes();
    byte[] actual = "trial".getBytes();
    assertArrayEquals("failure - byte arrays not same", expected, actual);
  }

  @Test
  public void testAssertEquals() {
    assertEquals("failure - strings are not equal", "text", "text");
  }

  @Test
  public void testAssertFalse() {
    assertFalse("failure - should be false", false);
  }

  @Test
  public void testAssertNotNull() {
    assertNotNull("should not be null", new Object());
  }

  @Test
  public void testAssertNotSame() {
    assertNotSame("should not be same Object", new Object(), new Object());
  }

  @Test
  public void testAssertNull() {
    assertNull("should be null", null);
  }

  @Test
  public void testAssertSame() {
    Integer aNumber = Integer.valueOf(768);
    assertSame("should be same", aNumber, aNumber);
  }

  // JUnit Matchers assertThat
  @Test
  public void testAssertThatBothContainsString() {
    assertThat("albumen", both(containsString("a")).and(containsString("b")));
  }

  @Test
  public void testAssertThatHasItems() {
    assertThat(Arrays.asList("one", "two", "three"), hasItems("one", "three"));
  }

  @Test
  public void testAssertThatEveryItemContainsString() {
    assertThat(Arrays.asList(new String[] { "fun", "ban", "net" }), everyItem(containsString("n")));
  }

  // Core Hamcrest Matchers with assertThat
  @Test
  public void testAssertThatHamcrestCoreMatchers() {
    assertThat("good", allOf(equalTo("good"), startsWith("good")));
    assertThat("good", not(allOf(equalTo("bad"), equalTo("good"))));
    assertThat("good", anyOf(equalTo("bad"), equalTo("good")));
    assertThat(7, not(CombinableMatcher.<Integer> either(equalTo(3)).or(equalTo(4))));
    assertThat(new Object(), not(sameInstance(new Object())));
  }

  @Test
  public void testAssertTrue() {
    assertTrue("failure - should be true", true);
  }
}

当测试用例需要验证异常抛出时

方法一,这个方法的缺陷是无法验证是在哪一个环节抛出的异常,所以个人不推荐使用。
代码语言:javascript
复制
@Test(expected = IndexOutOfBoundsException.class) 
public void empty() { 
     new ArrayList<Object>().get(0); 
}
方法二,try/catch方式,这种方式的缺点是代码量多
代码语言:javascript
复制
@Test
public void testExceptionMessage() {
    try {
        new ArrayList<Object>().get(0);
        fail("Expected an IndexOutOfBoundsException to be thrown");
    } catch (IndexOutOfBoundsException anIndexOutOfBoundsException) {
        assertThat(anIndexOutOfBoundsException.getMessage(), is("Index: 0, Size: 0"));
    }
}
方法三,使用内置的@ExpectedException,个人比较推荐
代码语言:javascript
复制
@Rule
public ExpectedException thrown = ExpectedException.none();

@Test
public void shouldTestExceptionMessage() throws IndexOutOfBoundsException {
    List<Object> list = new ArrayList<Object>();
 
    thrown.expect(IndexOutOfBoundsException.class);
    thrown.expectMessage("Index: 0, Size: 0");
    list.get(0); // execution will never get past this line
}

Matchers and assertThat

表达形式如下:

代码语言:javascript
复制
assertThat([value], [matcher statement]);

assertThat(x, is(3));
assertThat(x, is(not(4)));
assertThat(responseString, either(containsString("color")).or(containsString("colour")));
assertThat(myList, hasItem("3"));

它的好处是非常灵活,并且外部有扩展实现(org.hamcrest)可以无缝使用。 虽然对开发人员来说,这套Matchers的设计显得有些画蛇添足。但对测试人员来讲,这套设计可以减少很多麻烦。 按需取用即可。

需要参数的测试用例

我们都知道@Test修饰方法是不能加参数的,否则在执行时会抛出异常。但是的确存在需要参数的情况,可以使用以下方式进行实现。

代码语言:javascript
复制
import static org.junit.Assert.assertEquals;

import java.util.Arrays;
import java.util.Collection;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

@RunWith(Parameterized.class)
public class FibonacciTest {
    @Parameters
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][] {     
                 { 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 }  
           });
    }

    private int fInput;

    private int fExpected;

    public FibonacciTest(int input, int expected) {
        this.fInput = input;
        this.fExpected = expected;
    }

    @Test
    public void test() {
        assertEquals(fExpected, Fibonacci.compute(fInput));
    }
}

示例代码实现了使用7组参数输入,来验证斐波那契数列的合法性。

延伸:Mock or Stub

在做单元测试的时候,我们会发现我们要测试的方法会引用很多外部依赖的对象,比如:(发送邮件,网络通讯,记录Log, 文件系统 之类的)。 而我们没法控制这些外部依赖的对象。 为了解决这个问题,我们需要用到Stub和Mock来模拟这些外部依赖的对象,从而控制它们。

JUnit是单元测试框架,可以轻松的完成关联依赖关系少或者比较简单的类的单元测试,但是对于关联到其它比较复杂的类或对运行环境有要求的类的单元测试,模拟环境或者配置环境会非常耗时,实施单元测试比较困难。而这些“mock框架”(Mockito 、jmock 、 powermock、EasyMock),可以通过mock框架模拟一个对象的行为,从而隔离开我们不关心的其他对象,使得测试变得简单。(例如service调用dao,即service依赖dao,我们可以通过mock dao来模拟真实的dao调用,从而能达到测试service的目的。)

模拟对象(Mock Object)可以取代真实对象的位置,用于测试一些与真实对象进行交互或依赖于真实对象的功能,模拟对象的背后目的就是创建一个轻量级的、可控制的对象来代替测试中需要的真实对象,模拟真实对象的行为和功能。

Mockito简单运用说明

  • when(mock.someMethod()).thenReturn(value)设定mock对象某个方法调用时的返回值。可以连续设定返回值,即when(mock.someMethod()).thenReturn(value1).thenReturn(value2),第一次调用时返回value1,第二次返回value2。也可以表示为如下:when(mock.someMethod()).thenReturn(value1,value2)
  • ② 调用以上方法时抛出异常: when(mock.someMethod()).thenThrow(new RuntimeException());
  • ③ 另一种stubbing语法: doReturn(value).when(mock.someMethod()) doThrow(new RuntimeException()).when(mock.someMethod())
  • ④ 对void方法进行方法预期设定只能用如下语法:doNothing().when(mock.someMethod()) doThrow(new RuntimeException()).when(mock.someMethod()) doNothing().doThrow(new RuntimeException()).when(mock.someMethod())
  • ⑤ 方法的参数可以使用参数模拟器,可以将anyInt()传入任何参数为int的方法,即anyInt匹配任何int类型的参数,anyString()匹配任何字符串,anySet()匹配任何Set。
  • ⑥ Mock对象只能调用stubbed方法,调用不了它真实的方法,但是Mockito可以用spy来监控一个真实对象,这样既可以stubbing这个对象的方法让它返回我们的期望值,又可以使得对其他方法调用时将会调用它的真实方法。
  • ⑦ Mockito会自动记录自己的交互行为,可以用verify(…).methodXxx(…)语法来验证方法Xxx是否按照预期进行了调用。
    • (1) 验证调用次数:verify(mock,times(n)).someMethod(argument),n为被调用的次数,如果超过或少于n都算失败。除了times(n),还有never(),atLease(n),atMost(n)。
    • (2) 验证超时:verify(mock, timeout(100)).someMethod();
    • (3) 同时验证:verify(mock, timeout(100).times(1)).someMethod();

项目框架过程梳理

1. 0层 整体架构

JUnitCoremain()方法入口类,所有单元测试用例由这里开始执行。

代码语言:javascript
复制
public class JUnitCore {
    private final RunNotifier notifier = new RunNotifier();
    public static void main(String... args) {
        Result result = new JUnitCore().runMain(new RealSystem(), args);
        System.exit(result.wasSuccessful() ? 0 : 1);
    }
    // ignore 
}

args 是测试类的类名,通过执行runMain()方法得到单元测试结果result

代码语言:javascript
复制
public class Result implements Serializable {
    private static final ObjectStreamField[] serialPersistentFields =
            ObjectStreamClass.lookup(SerializedForm.class).getFields();
    private final AtomicInteger count;
    private final AtomicInteger ignoreCount;
    private final CopyOnWriteArrayList<Failure> failures;
    private final AtomicLong runTime;
    private final AtomicLong startTime;

    // ignore
}

继续看一眼Failure这个类的构成:

代码语言:javascript
复制
public class Failure implements Serializable {
    private final Description fDescription;
    private final Throwable fThrownException;
    // ignore
}

阅读源码我的做法是:先从顶层开始闭环,再逐渐向下分析,切勿在第一层架构上就深入到第二层第三层等,先闭合每一层再逐步深入。

在0层阶段,我们得到如下结论:传入测试类的类名数组,经过内部处理后,返回测试用例执行结果。这些结果包含:执行次数、忽略次数、失败信息描述及异常、执行开始时间、执行运行时间。

2. 1层 整体架构

代码语言:javascript
复制
public class JUnitCore {
    Result runMain(JUnitSystem system, String... args) {

        // step 2.1 
        JUnitCommandLineParseResult jUnitCommandLineParseResult = JUnitCommandLineParseResult.parse(args);

        // step 2.2
        RunListener listener = new TextListener(system);
        addListener(listener);

        // step 2.3
        return run(jUnitCommandLineParseResult.createRequest(defaultComputer()));
    }
}

先看JUnitCommandLineParseResult的数据结构,在跟踪一眼

代码语言:javascript
复制
class JUnitCommandLineParseResult {
    private final List<String> filterSpecs = new ArrayList<String>();
    private final List<Class<?>> classes = new ArrayList<Class<?>>();
    private final List<Throwable> parserErrors = new ArrayList<Throwable>();

    void parseParameters(String[] args) {
        for (String arg : args) {
            try {
                classes.add(Classes.getClass(arg));
            } catch (ClassNotFoundException e) {
                parserErrors.add(new IllegalArgumentException("Could not find class [" + arg + "]", e));
            }
      
    // ignore     
}
  • classes由字符串构建成Class<?>对象,目的必然是反射。
  • parserErrors是上一步构建Class<?>对象失败时,存储异常信息的容器。
  • filterSpecs尚未调用到,先忽略。

至此对所有传入的args校验和初始化算式完成了。接着初始化了TextListener对象并添加到RunNotifier中,目的是执行测试用例时候控制台的输出日志。 前期的准备工作已经做好了,剩下的就是准备真正命令对象,在JUnit中它的定义是org.junit.runner.Request。最后在调用一下JUnitCore.run()方法就完成调用了。

在1层阶段,我们看到对args的预处理。JUnit设计人员使用org.junit.runner.Request来作为命令对象(命令模式),JUnitCore作为门面类揽下:创建Request,调度Request,以及生命周期回调管理等一系列脏活。

综上我们可以推断出阅读的重点在:

  • Request的构成?支持哪些Request?
  • 如何调用Request?调用后Result是否有再加工?
  • NotifyListener生命周期?

2层 Request的构成?支持哪些Request?

代码语言:javascript
复制
class JUnitCommandLineParseResult {
    public Request createRequest(Computer computer) {
        if (parserErrors.isEmpty()) {
            Request request = Request.classes(
                    computer, classes.toArray(new Class<?>[classes.size()]));
            return applyFilterSpecs(request);
        } else {
            return errorReport(new InitializationError(parserErrors));
        }
    }
    // ignore
}

异常分支暂不深入看,且看正常情况下的两步:

  1. Request.classes()构建了Request对象
  2. 调用applyFilterSpecs()似乎是过滤了某些Specs(特征?)
代码语言:javascript
复制
public abstract class Request {

    public static Request classes(Computer computer, Class<?>... classes) {
        try {
            AllDefaultPossibilitiesBuilder builder = new AllDefaultPossibilitiesBuilder(true);
            Runner suite = computer.getSuite(builder, classes);
            return runner(suite);
        } catch (InitializationError e) {
            throw new RuntimeException(
                    "Bug in saff's brain: Suite constructor, called as above, should always complete");
        }
    }

    public static Request runner(final Runner runner) {
        return new Request() {
            @Override
            public Runner getRunner() {
                return runner;
            }
        };
    }
    
}

千回百转computer.getSuite()最终还是回到了AllDefaultPossibilitiesBuilder.runnerForClass()来构建Runner对象。

代码语言:javascript
复制
public class AllDefaultPossibilitiesBuilder extends RunnerBuilder {
 
    @Override
    public Runner runnerForClass(Class<?> testClass) throws Throwable {
        List<RunnerBuilder> builders = Arrays.asList(
                ignoredBuilder(),
                annotatedBuilder(),
                suiteMethodBuilder(),
                junit3Builder(),
                junit4Builder());

        for (RunnerBuilder each : builders) {
            Runner runner = each.safeRunnerForClass(testClass);
            if (runner != null) {
                return runner;
            }
        }
        return null;
    }
    // ignore
}

each.safeRunnerForClass(testClass);方法会依据你当前所配置的@RunWith注解来选择实现方法。目前我们使用@RunWith(Junit4.class)

代码语言:javascript
复制
public class JUnit4Builder extends RunnerBuilder {
    @Override
    public Runner runnerForClass(Class<?> testClass) throws Throwable {
        return new BlockJUnit4ClassRunner(testClass);
    }
}

划重点:到此我们得知默认情况下,单元测试最终创建的Runner都是BlockJUnit4ClassRunner类型,而Request又仅是对Runner的封装,所以只需要精读BlockJUnit4ClassRunner方法即可。

Request对象已经准备妥当,接着程序执行到applyFilterSpecs()方法。

代码语言:javascript
复制
class JUnitCommandLineParseResult {
    private Request applyFilterSpecs(Request request) {
        try {
            for (String filterSpec : filterSpecs) {
                Filter filter = FilterFactories.createFilterFromFilterSpec(
                        request, filterSpec);
                request = request.filterWith(filter);
            }
            return request;
        } catch (FilterNotCreatedException e) {
            return errorReport(e);
        }
    }
    // ignore
}

啥都不看,凭感觉猜就知道是过滤某些请求(对应注解@Ignore)。但咱们还是务实一点,看看代码。

代码语言:javascript
复制
public abstract class Request {
    public Request filterWith(Filter filter) {
        return new FilterRequest(this, filter);
    }
    // ignore
}

这结构熟不熟悉?典型的装饰器模式——将Filter的职责装饰到原来的Request对象上。

代码语言:javascript
复制
public final class FilterRequest extends Request {
    // ignore

    @Override
    public Runner getRunner() {
        try {
            Runner runner = request.getRunner();
            fFilter.apply(runner);
            return runner;
        } catch (NoTestsRemainException e) {
            return new ErrorReportingRunner(Filter.class, new Exception(String
                    .format("No tests found matching %s from %s", fFilter
                            .describe(), request.toString())));
        }
    }
}
public abstract class Filter {
    public void apply(Object child) throws NoTestsRemainException {
        if (!(child instanceof Filterable)) {
            return;
        }
        Filterable filterable = (Filterable) child;
        filterable.filter(this);
    }
    // ignore
}

至此Request对象的构成已经完全透明,在JUnit中有如下几种:

  • SortingRequest
  • FilterRequest
  • ClassRequest

基于以上的分析,我们知道要实现:对测试用例进行特定排序,并且过滤掉部分用例的需求是非常容易实现的 —— 装饰器。

2层 如何调用Request?调用后Result是否有再加工?

代码语言:javascript
复制
public class JUnitCore {
    public Result run(Runner runner) {
        Result result = new Result();
        RunListener listener = result.createListener();
        notifier.addFirstListener(listener);
        try {
            notifier.fireTestRunStarted(runner.getDescription());
            runner.run(notifier);
            notifier.fireTestRunFinished(result);
        } finally {
            removeListener(listener);
        }
        return result;
    }
    // ignore
}

执行runner.run(notifier);的前后环绕notifier通知,执行完removeListener避免内存泄露。生命周期回调这块太直接,直接略过。跟一下runner.run(notifier)看看。

基于上一个段落的分析,我们知道Runner的实例类型是BlockJUnit4ClassRunner,所以直接看它的run()方法。

BlockJUnit4ClassRunner继承自ParentRunner:

代码语言:javascript
复制
public abstract class ParentRunner<T> extends Runner implements Filterable,
        Sortable {
    
    protected Statement childrenInvoker(final RunNotifier notifier) {
        return new Statement() {
            @Override
            public void evaluate() {
                runChildren(notifier);
            }
        };
    }
    
    protected Statement classBlock(final RunNotifier notifier) {
        Statement statement = childrenInvoker(notifier);
        if (!areAllChildrenIgnored()) {
            statement = withBeforeClasses(statement);
            statement = withAfterClasses(statement);
            statement = withClassRules(statement);
        }
        return statement;
    }
        
    @Override
    public void run(final RunNotifier notifier) {
        EachTestNotifier testNotifier = new EachTestNotifier(notifier,
                getDescription());
        try {
            Statement statement = classBlock(notifier);
            statement.evaluate();
        } catch (AssumptionViolatedException e) {
            testNotifier.addFailedAssumption(e);
        } catch (StoppedByUserException e) {
            throw e;
        } catch (Throwable e) {
            testNotifier.addFailure(e);
        }
    }
    // ignore
}

这个方法的返回类型是void,并且例外了两种异常:

  • AssumptionViolatedException表明假设不成立,无任何异常抛出。
  • StoppedByUserException用户主动停止单元测试,单独抛出异常。
  • 其余情况下都由testNotifier接口异常。

最最最最重要的部分就是与Statement相关联的部分,这部分是单元测试的核心功能。classBlock方法做的事情:将测试类中的测试用例映射成Statement对象,并按照@Before>@Test>@After的顺序构建职责链。

构建完成后调用statement.evaluate(),这是最后的挣扎调用了。所有的evaluate()都会进到方法:

代码语言:javascript
复制
// ParentRunner.class

    private volatile RunnerScheduler scheduler = new RunnerScheduler() {
        public void schedule(Runnable childStatement) {
            childStatement.run();
        }

        public void finished() {
            // do nothing
        }
    };
    
    private void runChildren(final RunNotifier notifier) {
        final RunnerScheduler currentScheduler = scheduler;
        try {
            for (final T each : getFilteredChildren()) {
                currentScheduler.schedule(new Runnable() {
                    public void run() {
                        ParentRunner.this.runChild(each, notifier);
                    }
                });
            }
        } finally {
            currentScheduler.finished();
        }
    }
代码语言:javascript
复制
public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> {

    @Override
    protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
        Description description = describeChild(method);
        if (isIgnored(method)) {
            notifier.fireTestIgnored(description);
        } else {
            runLeaf(methodBlock(method), description, notifier);
        }
    }
    
    /**
     * Runs a {@link Statement} that represents a leaf (aka atomic) test.
     */
    protected final void runLeaf(Statement statement, Description description,
            RunNotifier notifier) {
        EachTestNotifier eachNotifier = new EachTestNotifier(notifier, description);
        eachNotifier.fireTestStarted();
        try {
            statement.evaluate();
        } catch (AssumptionViolatedException e) {
            eachNotifier.addFailedAssumption(e);
        } catch (Throwable e) {
            eachNotifier.addFailure(e);
        } finally {
            eachNotifier.fireTestFinished();
        }
    }
    
    protected Statement methodBlock(FrameworkMethod method) {
        Object test;
        try {
            test = new ReflectiveCallable() {
                @Override
                protected Object runReflectiveCall() throws Throwable {
                    return createTest();
                }
            }.run();
        } catch (Throwable e) {
            return new Fail(e);
        }

        Statement statement = methodInvoker(method, test);
        statement = possiblyExpectingExceptions(method, test, statement);
        statement = withPotentialTimeout(method, test, statement);
        statement = withBefores(method, test, statement);
        statement = withAfters(method, test, statement);
        statement = withRules(method, test, statement);
        return statement;
    }
    
}

执行evaluate()调用是整个JUnit中最简单的事情了,复杂性体现在构建Statement的职责链上,比如:前面对基本@Test的用例的构建,到现在在methodBlock()中追加@Timeout @ExpectingException相应的处理。

结束语

单元测试不是来恶心开发者的,它是帮助开发者尽早发现问题的利器。因为问题越往后发现,它的修复成本就会越高。

GitHub上绝大多数优秀的项目单元测试的覆盖率都是90%以上,在这些项目(前端、后端、客户端)里面,我们可以从中学到丰富的测试技巧。所以,不能说不知道怎么写单元测试噢~

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.07.29 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 什么是单元测试
  • JUnit模块和说明
  • JUnit4 注解
  • 使用教程
    • Assume
      • Assert
        • 当测试用例需要验证异常抛出时
          • 方法一,这个方法的缺陷是无法验证是在哪一个环节抛出的异常,所以个人不推荐使用。
          • 方法二,try/catch方式,这种方式的缺点是代码量多
          • 方法三,使用内置的@ExpectedException,个人比较推荐
        • Matchers and assertThat
          • 需要参数的测试用例
          • 延伸:Mock or Stub
          • 项目框架过程梳理
            • 1. 0层 整体架构
            • 2. 1层 整体架构
            • 2层 Request的构成?支持哪些Request?
            • 2层 如何调用Request?调用后Result是否有再加工?
            • 结束语
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档