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閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,777評論 18 139
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,707評論 18 399
  • 背景 一年多以前我在知乎上答了有關LeetCode的問題, 分享了一些自己做題目的經驗。 張土汪:刷leetcod...
    土汪閱讀 12,761評論 0 33
  • 單元測試 單測定義 單元測試(Unit Testing)又稱為模塊測試, 是針對程序模塊(軟件設計的最小單位)來進...
    運維開發筆記閱讀 2,007評論 0 2
  • 望月樓臺伊人獨抱月 白玉盤灑下滿壺銀漿 應嫦娥邀你一醉方休 借一縷星光且作酒盞 將舉杯欲泯去千萬愁 奈何斬不斷青絲...
    郁間非流離閱讀 119評論 2 6