Android Test Base--JUnit Framework

JUnit Intro

Android基于JUnit Framework来书写测试代码。JUnit是基于Java语言的流行的、广泛被使用的测试框架,当前最新的版本是JUnit4。相对于之前的版本而言,JUnit4允许开发者使用更清晰、简洁、灵活的方式来构建测试代码。Android也提供相应更新支持JUnit4,并建议开发者基于JUnit4来写测试代码。因此,我们这里主要学习JUnit4的使用。

JUnit的一个测试类就是一个普通的Java Class。通过使用JUnit4提供的注解可以快速便捷的构建Test Class。e.g.

public class CalculatorTest {

    private Calculator calculator;

    @BeforeClass
    public static void setUpBeforeClass(){

    }

    @Before
    public void setupBeforeTest(){
        calculator = new Calculator();
    }

    @Test
    public void testPlus() throws Exception {
        assertEquals(calculator.plus(2, 3), 5);
    }

    @Test
    public void testMinus() throws Exception {
        assertEquals(calculator.minus(5, 3), 2);
    }

    @Test
    public void testMultiply() throws Exception {
        assertEquals(calculator.multiply(2, 3), 6);
    }

    @Test
    public void testDivide(){
        assertEquals(calculator.divide(10, 2), 5);
    }

    @After
    public void cleanupAfterTest(){
        calculator = null;
    }

    @AfterClass
    public static void cleanupAfterClass(){

    }
}

上面的代码展示了一个简单的Test Class。包含了最常用的注解,注解名称代表的含义一目了然,说明如下:

  • @Test:方法注解,要求方法签名为public void。声明该方法为一个Test Case Method。一个Test Class可以包含若干个这样的方法,JUnit 会依次运行这些方法。该注解中又包含两个可配置的值:
    • timeout:设置一个单位为毫秒的时间值,如果该测试方法运行的时间超过该指定值,则该测试方法失败;一般用来捕获或者终止循环;e.g.
@Test(timeout=100) 
public void infinity() {
       while(true);
 }
- *exception*:指定对应的测试方法会抛出某个异常;如果该测试方法没有抛出指定的异常,则测试不通过;e.g.
@Test(expected = ArithmeticException.class)
public void testDivideExpectException(){
      calculator.divide(10, 0);
 }
  • @Before :方法注解,要求方法签名为public void。每一个Test Class中允许包含多个这种方法,对应方法会在执行该Test Class中的每个test method之前调用。
  • @After :同@Before对应,只是调用时机不同,该方法会在该Test Class中每个test method执行完成之后调用。
  • @BeforeClass :方法注解,要求方法签名为public static void。该方法会在执行该Test Class时被调用且只会被调用一次;一般在该方法中初始化一些全局的、开销比较大的操作,比如初始化数据库连接等。
  • @AfterClass :同@BeforeClass注解对应,只是调用时机不同,该方法会在Test Class执行完所有的Test method后调用且只会调用一次;在该方法中可以做一些清理的工作。

Base Concept

  • Runner类:JUnit将如何运行一个Test Class抽象为一个Runner类 。JUnit4提供了一个基础的抽象Runner子类ParentRunner<T>
public abstract class ParentRunner<T> extends Runner implements Filterable, Sortable {
    ......
    /** * Returns a list of objects that define the children of this Runner. */
    protected abstract List<T> getChildren();
    protected abstract void runChild(T child, RunNotifier notifier);
     .....
}

该类是一个泛型类,可以将ParentRunner看成是一棵Test Tree的父亲节点,对应的类型参数T 就是代表其下的子节点的类型。针对一个具体的Test Class,ParentRunner层负责处理@BeforeClass@AfterClass@ClassRule注解的方法,遍历并执行所有的child。JUnit允许自定义Runner,通过@RunWith注解可以指定一个Test Class使用某个Runner。

  • Statement:抽象类,代表了一个Test Class执行过程中的一个或多个动作,通过evaluate()方法来执行这些动作。类似于Java中的Runnable接口,evaluate()就相当于run()方法。
public abstract class Statement {    
    /** Run the action, throwing a Throwable if anything goes wrong. */
    public abstract void evaluate() throws Throwable;
}

How JUnit Run?

  1. Build Runner
     我们可以很方便的通过IDE来运行我们的Test Class,也可以自己通过命令行工具运行。因为本质上我们只是运行了一个普通的Java程序而已,IDE只不过是帮我们写好了命令、封装好参数而已。

java -cp .;C:\Users\Administrator.gradle\caches\modules-2\files-2.1\junit\junit\4.12\2973d150c0dc1fefe998f834810d68f278ea58ec\junit-4.12.jar;D:\AndroidCode\StudioCode\AndroidTestPractice\app\build\intermediates\classes\test\debug;D:\AndroidCode\StudioCode\AndroidTestPractice\app\build\intermediates\classes\debug;D:\AndroidCode\StudioCode\AndroidTestPractice\build\generated\mockable-android-23.jar;C:\Users\Administrator.gradle\caches\modules-2\files-2.1\org.hamcrest\hamcrest-core\1.3\42a25dc3219429f0e5d060061f71acb49bf010a0\hamcrest-core-1.3.jar; org.junit.runner.JUnitCore com.lcd.androidtestpractice.ExampleUnitTest com.lcd.androidtestpractice.CalculatorTest

上面的命令指定运行ExampleUnitTest和CalculatorTest两个Test Class。-cp后面指定class path,多个path之间使用;分号隔开。我们需要指定所有需要的classpath(包括JUnit的jar路径、JUnit所依赖的其他jar包的路径、Test Classes路径和其依赖的其他所有类路径),需要指定运行JUnit的入口类org.junit.runner.JUnitCore以及指定我们需要运行的Test Classes。那么长的执行语句,想想还是用IDE吧
 不管是IDE还是CMD,最终的统一入口都是JUnitCore。在其main方法中会解析参数,这些参数其实就是我们需要运行的所有Test Class的全域限定名称。JUnit将这些参数封装成为一个Request对象执行,其实就是从request中取出Runner调用run方法执行,这些步骤比较简单,在此略过分析。
```java
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);
}

  public Result run(Request request) {
    return run(request.getRunner());
  }
  ...
}
```

我们先来看看Runner的实现到底是个什么对象,如何构造的?因为具体的代码比较简单,这里我就不一一分析了,有兴趣的同学可以自己阅读源码。 我直接给出答案:JUnit通过RunnerBuilder类来针对具体的Test Class为其构造具体的Runner实现。且在JUnit4中提供AllDefaultPossibilitiesBuilder类为默认使用的builder。

public class AllDefaultPossibilitiesBuilder extends RunnerBuilder {
    private final boolean canUseSuiteMethod;

    public AllDefaultPossibilitiesBuilder(boolean canUseSuiteMethod) {
        this.canUseSuiteMethod = canUseSuiteMethod;
    }

    @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;
    }

    protected JUnit4Builder junit4Builder() {
        return new JUnit4Builder();
    }

    protected JUnit3Builder junit3Builder() {
        return new JUnit3Builder();
    }

    protected AnnotatedBuilder annotatedBuilder() {
        return new AnnotatedBuilder(this);
    }

    protected IgnoredBuilder ignoredBuilder() {
        return new IgnoredBuilder();
    }

    protected RunnerBuilder suiteMethodBuilder() {
        if (canUseSuiteMethod) {
            return new SuiteMethodBuilder();
        }
        return new NullBuilder();
    }
}

代码很清晰,通过Builder的runnerForClass(Class<?> testClass)方法为一个具体的Test Class构建对应的Runner。该方法会依次遍历AllDefaultPossibilitiesBuilder中内置的Builders,调用每个builder的safeRunnerForClass方法请求为该Test Class生成Runner。如果返回为null,则说明这个builder无法为此Test Class构建Runner,继续遍历其他Builder。否则返回。

  • IgnoredBuilder:该Builder检查Test Class是否使用@Ignore注解修饰,如果是,返回IgnoredClassRunner,否则返回null。IgnoredClassRunner在执行时,其实什么都没干,等于就是忽略执行这个Test Class。
    public class IgnoredBuilder extends RunnerBuilder {
    @Override
    public Runner runnerForClass(Class<?> testClass) {
        if (testClass.getAnnotation(Ignore.class) != null) {
            return new IgnoredClassRunner(testClass);
        }
        return null;
    }
    

}
```

  • AnnotatedBuilder:该Builder检查Test Class是否使用@RunWith注解修饰,如果有,会通过反射构建对应的Runner对象返回,否则返回null。这里不再给出具体的代码。
  • SuiteMethodBuilderJUnit3Builder:这两个Builder是兼容老版本的,用来构建基于JUnit3的Runner,不再讨论。
  • JUnit4Builder:构建基于JUnit4的runner,该builder直接返回一个BlockJUnit4ClassRunner的对象。所以如果前面的Builder都没有能够为Test Class构建Runner,则这个就是其默认的Runner。

到现在为止,Runner对象已经构建并且返回。直接调用它的run()方法就相当于执行这个Test Class。

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;
    }
  1. ParentRunner.run() Flow

  2. 上面说到了Runner的run方法。在JUnit4中的Runner一般为ParentRunner的子类,所以相应的这里从ParentRunner的run(final RunNotifier notifier)方法开始分析。首先通过classBlock(notifier)方法返回一个statement对象,包含了一系列要执行的动作,直接调用evaluate()方法执行。

@Override
    public void run(final RunNotifier notifier) {
        ...
        Statement statement = classBlock(notifier);
        statement.evaluate();
        ...
    }
  1. 那这个statement对象是怎么构造的呢?具体里面包含哪些执行动作?
protected Statement classBlock(final RunNotifier notifier) {
        Statement statement = childrenInvoker(notifier);
        if (!areAllChildrenIgnored()) {
            statement = withBeforeClasses(statement);
            statement = withAfterClasses(statement);
            statement = withClassRules(statement);
        }
        return statement;
    }
  1. 首先是通过childrenInvoker方法封装执行所有child的动作到statement中,该动作等于是调用runChildren方法。
protected Statement childrenInvoker(final RunNotifier notifier) {
        return new Statement() {
            @Override
            public void evaluate() {
                runChildren(notifier);
            }
        };
    }
  1. 然后是通过withBeforeClasses()方法来封装@BeforeClass注解修饰的方法,如果Test Class中存在使用@BeforeClass注解修饰的方法,则new一个RunBefores对象返回,否则直接返回原来的statement对象。
protected Statement withBeforeClasses(Statement statement) {
        List<FrameworkMethod> befores = testClass
                .getAnnotatedMethods(BeforeClass.class);
        return befores.isEmpty() ? statement :
                new RunBefores(statement, befores, null);
    }
public class RunBefores extends Statement {
    private final Statement next;
    private final Object target;
    private final List<FrameworkMethod> befores;

    public RunBefores(Statement next, List<FrameworkMethod> befores, Object target) {
        this.next = next;
        this.befores = befores;
        this.target = target;
    }

    @Override
    public void evaluate() throws Throwable {
        for (FrameworkMethod before : befores) {
            before.invokeExplosively(target);
        }
        next.evaluate();
    }
}

RunBefores继承于Statement,如果返回的是RunBefores对象,当执行其evaluate()方法时,会先执行所有的befores指定的动作,即执行所有的@BeforeClass修饰的方法,之后执行next指代的动作,这里next指代的其实就是上一步的runChildren动作。

  1. 同理,之后是通过withAfterClasses()方法加入@AfterClass对应的动作。如果Test Class中存在使用@AfterClass注解修饰的方法,则new一个RunAfters对象返回,否则直接返回原来的statement对象。
protected Statement withAfterClasses(Statement statement) {
        List<FrameworkMethod> afters = testClass
                .getAnnotatedMethods(AfterClass.class);
        return afters.isEmpty() ? statement :
                new RunAfters(statement, afters, null);
    }
public class RunAfters extends Statement {
    private final Statement next;
    private final Object target;
    private final List<FrameworkMethod> afters;

    public RunAfters(Statement next, List<FrameworkMethod> afters, Object target) {
        this.next = next;
        this.afters = afters;
        this.target = target;
    }

    @Override
    public void evaluate() throws Throwable {
       ...
            next.evaluate();
       ...
            for (FrameworkMethod each : afters) {
                    each.invokeExplosively(target);
            }
        ...
    }
}

RunAfters同样继承于Statement,如果返回的是RunAfters对象,当执行其evaluate()方法时,会先执行next指代的动作,即先执行上一步的动作。然后才会执行所有的afters指定的动作,即执行所有的@AfterClass修饰的方法。

  1. 下一步,通过withClassRules方法添加ClassRule中的动作。
private Statement withClassRules(Statement statement) {
        List<TestRule> classRules = classRules();
        return classRules.isEmpty() ? statement :
                new RunRules(statement, classRules, getDescription());
    }

这里有必要先来了解一下什么是TestRule。先看它的类定义:

public interface TestRule {
    Statement apply(Statement base, Description description);
}

TestRule允许我们在Test Class运行过程中插入自定义的一些操作。具体的可以在base statement执行的前后加入一些其他操作逻辑。下面的代码自定义一个Rule,该Rule先执行doSomethingBefore()方法,然后执行statement,最后执行doSomethingAfter()方法。e.g.

public class ClassRuleTest {

    @ClassRule
    public static TestRule myRule(){
        return new TestRule() {

            private void doSomethingBefore(){}

            private void doSomethingAfter(){}

            @Override
            public Statement apply(final Statement base, Description description) {
                return new Statement() {
                    @Override
                    public void evaluate() throws Throwable {    
                        doSomethingBefore(); //do something before base actions
                        base.evaluate();
                        doSomethingAfter(); //do something after base actions
                    }
                };
            }
        };
    }
}

TestRule又分为类级别的Rule和实例级别的Rule。
- @ClassRule:用来声明一个类级别的Rule,可修饰方法和字段;当修饰方法时,要求方法签名为public static且方法的返回类型为TestRule或者其子类型;同样的,修饰变量时,要求变量为public static且类型为TestRule或者其子类型;JUnit在搜索ClassRule时,会先查找符合条件的方法,并调用,将返回值添加到TestRule列表中;然后搜索符合条件的Field字段,同样加入列表中。
- @Rule:用来声明一个实例级别的Rule,可修饰方法和字段;方法要求为public且返回类型为TestRule或者其子类型;修饰变量时要求为public的实例变量且类型为TestRule或者其子类型;JUnit同样会在适当的时机搜索Test Class中的所有实例Rule并运用。

 现在回到上面的流程,如果Test Class中不存在类级别的Rule,则直接返回上一步的statement对象;否则构建一个`RunRules`对象返回。`RunRules`同样继承于`Statement`,在其构造函数中会遍历所有的ClassRule并调用apply方法返回一个新的statement,这样就给每个Rule提供了在base statement基础上插入自定义操作的机会。上面的分析,其实主要涉及的是Test Class类层面的操作。现在我们先来梳理一下最终返回的class statement可能包含的操作和其执行流程流程:
class statement

这里我们假设ClassRule、BeforeClass和AfterClass都存在,如果某一项不存在,则只需忽略掉流程图中对应的部分即可。因为TestRule的特殊性,某个TestRule可能在Statement前后都添加了自定义操作,所以流程图中ClassRule将对应两个部分。当然,某个TestRule可能只在base statement基础操作前添加自定义操作,那么其对应的后置操作部分相当于什么都没干;反之亦然!这里Statement对象的层层嵌套,其实是使用了设计模式中的装饰器模式,感兴趣的同学可以私下了解一下。

  1. runChildren
    下面我们来分析runChildren的流程。runChildren会首先获取Children列表,然后遍历并在每个Child上调用runChild方法。runChild方法在ParentRunner中是一个抽象方法,由具体的subclass类来实现。
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();
        }
    }

JUnit4提供了ParentRunner的两个直接实现BlockJUnit4ClassRunnerSuite

  • BlockJUnit4ClassRunner:JUnit4默认Ruuner,运行Test Class下的所有Test method。该Runner包含的每个Child其实是一个FrameworkMethod对象,代表一个@Test方法。
public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> {

  @Override
  protected List<FrameworkMethod> getChildren() {
      return computeTestMethods();
  }

  /**
   * Returns the methods that run tests. Default implementation returns all
   * methods annotated with {@Test} on this class and superclasses that
   * are not overridden.
   */
  protected List<FrameworkMethod> computeTestMethods() {
      return getTestClass().getAnnotatedMethods(Test.class);
  }
  ...
}

可以看到,getChildren()方法返回的Children列表其实是FrameworkMethod对象列表,即Test Class中所有使用@Test注解修饰的方法列表。在ParentRunner中调用runChildren时,其实是在每个@Test方法上调用runChild方法。在该方法中,首先检查是否忽略,其实就是检查是否有@Ignore注解;如果忽略,则该Test Method得不到执行;否则,通过methodBlock(method)方法返回一个Statement对象,并调用runLeaf方法运行这个statement对象。

protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
      Description description = describeChild(method);
      if (isIgnored(method)) {
          notifier.fireTestIgnored(description);
      } else {
          runLeaf(methodBlock(method), description, notifier);
      }
  }
protected final void runLeaf(Statement statement, Description description,
          RunNotifier notifier) {
      ...
         statement.evaluate();
     ...
  }

那这个statement对象里面又封装了哪些操作呢?来看methodBlock方法怎么构造这个对象:

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;
  }

首先,通过createTest()方法直接构造了一个Test Class的实例test。createTest()方法使用的是反射的方式构建实例对象,使用的是Test Class默认构造函数,如果Test Class类中声明有其他的构造函数,请保证默认的构造函数存在,否则会因为无法创建实例而抛出异常。无法然后才开始处理Statement。这里我们看到了类似ParentRunner中的处理逻辑,一层层的statement的嵌套。首先methodInvoker返回的基础statement封装了在创建的实例test上运行@Test方法的操作,接着是封装@Test中声明的expected异常和timeout的处理逻辑,再然后就是withBefores封装所有的@Before方法操作、withAfters封装所有@After方法操作和withRules封装所有的@Rule操作。这里跟前面已经分析过的@BeforeClass@AfterClass@ClassRule操作的封装原理是相通的,就不一一贴出代码说明了。

  • Suite:该Runner允许将多个Test Class通过@SuiteClasses注解声明为一个测试套件来运行;当执行该Test Class时,会依次执行注解中包含的所有Test Class。一般我们可以通过如下的方式使用
@RunWith(Suite.class)
@SuiteClass({TestClass1.class, TestClass2.class...})
public class MySuite {}

通过@RunWith注解,在为该MySuite这个Test Class构建具体的Runner时,返回的就是Suite对象。

public class Suite extends ParentRunner<Runner> {
  ...

  private final List<Runner> runners;

  @Override
  protected List<Runner> getChildren() {
      return runners;
  }

  @Override
  protected void runChild(Runner runner, final RunNotifier notifier) {
      runner.run(notifier);
  }
  ...
}

Suite中的每一个Child都是一个Runner,因为Suite中包含了一系列的Test Classes,Suite对象在构建的时候,就会为这些Test Class都构建相应Runner(Runner构建的流程参照上面的说明);这些Runners作为Suite的Children保存到runners列表中。当在ParentRunner中调用runChildren时,其实就是在这些Runner对象上依次调用runChild方法。而runChild方法实现很简单,直接交给对应的Runner来处理。所以Suite并不关心如何去执行它包含的每个Test Class,真正的执行还是由Test Class自己决定的。到这里,我们runChildren的执行逻辑已经分析完了。根据上面的分析,下面给出相应的流程图来说明最终每个child statement具体的执行过程:


child statement

child statement的执行流程对照class statement的流程。这里也是假设Rule、Before和After都存在,如果不存在,请忽略图中对应的部分。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,490评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,493评论 18 399
  • 背景 一年多以前我在知乎上答了有关LeetCode的问题, 分享了一些自己做题目的经验。 张土汪:刷leetcod...
    土汪阅读 12,712评论 0 33
  • 单元测试 单测定义 单元测试(Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进...
    运维开发笔记阅读 1,975评论 0 2
  • 望月楼台伊人独抱月 白玉盘洒下满壶银浆 应嫦娥邀你一醉方休 借一缕星光且作酒盏 将举杯欲泯去千万愁 奈何斩不断青丝...
    郁间非流离阅读 118评论 2 6