摘要:命名模式為了做到自動(dòng)發(fā)現(xiàn)機(jī)制,在運(yùn)行時(shí)完成用例的組織,規(guī)定所有的測(cè)試用例必須遵循的函數(shù)原型。在后文介紹,可以將理解為及其的運(yùn)行時(shí)行為其中,對(duì)于于子句,對(duì)于于子句。將的執(zhí)行序列行為固化。
前世今生There are two ways of constructing a software design. One way is to make it so simple that there are obviously no deficiencies. And the other way is to make it so complicated that there are no obvious deficiencies. -- C.A.R. Hoare
本文是《Programming DSL》系列文章的第2篇,如果該主題感興趣,可以查閱如下文章:
Programming DSL: Implements JHamcrest
正交設(shè)計(jì)
本文通過「JSpec」的設(shè)計(jì)和實(shí)現(xiàn)的過程,加深認(rèn)識(shí)「內(nèi)部DSL」設(shè)計(jì)的基本思路。JSpec是使用Java8實(shí)現(xiàn)的一個(gè)簡(jiǎn)單的「BDD」測(cè)試框架。
動(dòng)機(jī)在Java社區(qū)中,JUnit是一個(gè)廣泛被使用的測(cè)試框架。不幸的是,JUnit的測(cè)試用例必須遵循嚴(yán)格的「標(biāo)識(shí)符」命名規(guī)則,給程序員帶來了很大的不便。
命名模式Junit為了做到「自動(dòng)發(fā)現(xiàn)」機(jī)制,在運(yùn)行時(shí)完成用例的組織,規(guī)定所有的測(cè)試用例必須遵循public void testXXX()的函數(shù)原型。
public void testTrue() { Assert.assertTrue(true); }注解
自Java 1.5支持「注解」之后,社區(qū)逐步意識(shí)到了「注解優(yōu)于命名模式」的最佳實(shí)踐,JUnit使用@Test注解,增強(qiáng)了用例的表現(xiàn)力。
@Test public void alwaysTrue() { Assert.assertTrue(true); }Given-When-Then
經(jīng)過實(shí)踐證明,基于場(chǎng)景驗(yàn)收的Given-When-Then命名風(fēng)格具有強(qiáng)大的表現(xiàn)力。但JUnit遵循嚴(yán)格的標(biāo)示符命名規(guī)則,程序員需要承受巨大的痛苦。
這種混雜「駝峰」和「下劃線」的命名風(fēng)格,雖然在社區(qū)中得到了廣泛的應(yīng)用,但在重命名時(shí),變得非常不方便。
public class GivenAStack { @Test public void should_be_empty_when_created() { } @Test public void should_pop_the_last_element_pushed_onto_the_stack() { } }新貴
以RSpec, Cucumber, Jasmine等為代表的[BDD」(Behavior-Driven Development)測(cè)試框架以強(qiáng)大的表現(xiàn)力,迅速得到了社區(qū)的廣泛應(yīng)用。其中,RSpec, Jasmine就是我較為喜愛的測(cè)試框架。例如,Jasmine的JavaScript測(cè)試用例是這樣的。
describe("A suite", function() { it("contains spec with an expectation", function() { expect(true).toBe(true); }); });JSpec
我們將嘗試設(shè)計(jì)和實(shí)現(xiàn)一個(gè)Java版的BDD測(cè)試框架:JSpec。它的風(fēng)格與Jasmine基本類似,并與Junit4配合得完美無瑕。
@RunWith(JSpec.class) public class JSpecs {{ describe("A spec", () -> { List初始化塊items = new ArrayList<>(); before(() -> { items.add("foo"); items.add("bar"); }); after(() -> { items.clear(); }); it("runs the before() blocks", () -> { assertThat(items, contains("foo", "bar")); }); describe("when nested", () -> { before(() -> { items.add("baz"); }); it("runs before and after from inner and outer scopes", () -> { assertThat(items, contains("foo", "bar", "baz")); }); }); }); }}
public class JSpecs {{ ...... }}
嵌套兩層{},這是Java的一種特殊的初始化方法,常稱為初始化塊。其行為與如下代碼類同,但它更加簡(jiǎn)潔、漂亮。
public class JSpecs { public JSpecs() { ...... } }代碼塊
describe, it, before, after都存在一個(gè)() -> {...}代碼塊,以便實(shí)現(xiàn)行為的定制化,為此先抽象一個(gè)Block的概念。
@FunctionalInterface public interface Block { void apply() throws Throwable; }雛形
定義如下幾個(gè)函數(shù),明確JSpec DSL的基本雛形。
public class JSpec { public static void describe(String desc, Block block) { ...... } public static void it(String behavior, Block block) { ...... } public static void before(Block block) { ...... } public static void after(Block block) { ...... }上下文
describe可以嵌套describe, it, before, after的代碼塊,并且外層的describe給內(nèi)嵌的代碼塊建立了「上下文」環(huán)境。
例如,items在最外層的describe中定義,它對(duì)describe整個(gè)內(nèi)部都可見。
隱式樹describe可以嵌套describe,并且describe為內(nèi)部的結(jié)構(gòu)建立「上下文」,因此describe之間建立了一棵「隱式樹」。
領(lǐng)域模型為此,抽象出了Context的概念,用于描述describe的運(yùn)行時(shí)。也就是是,Context描述了describe內(nèi)部可見的幾個(gè)重要實(shí)體:
List
List
Description desc:包含了父子之間的層次關(guān)系等上下文描述信息
Deque
Executor在后文介紹,可以將Executor理解為Context及其Spec的運(yùn)行時(shí)行為;其中,Context對(duì)于于desribe子句,Spec對(duì)于于it子句。
因?yàn)?b>describe之間存在「隱式樹」的關(guān)系,Context及Spec之間也就形成了「隱式樹」的關(guān)系。
參考實(shí)現(xiàn)public class Context { private List實(shí)現(xiàn)addChildbefores = new ArrayList<>(); private List afters = new ArrayList<>(); private Deque executors = new ArrayDeque<>(); private Description desc; public Context(Description desc) { this.desc = desc; } public void addChild(Context child) { desc.addChild(child.desc); executors.add(child); child.addBefore(collect(befores)); child.addAfter(collect(afters)); } public void addBefore(Block block) { befores.add(block); } public void addAfter(Block block) { afters.add(block); } public void addSpec(String behavior, Block block) { Description spec = createTestDescription(desc.getClassName(), behavior); desc.addChild(spec); addExecutor(spec, block); } private void addExecutor(Description desc, Block block) { Spec spec = new Spec(desc, blocksInContext(block)); executors.add(spec); } private Block blocksInContext(Block block) { return collect(collect(befores), block, collect(afters)); } }
describe嵌套describe時(shí),通過addChild完成了兩件重要工作:
「子Context」向「父Context」的注冊(cè);也就是說,Context之間形成了「樹」形結(jié)構(gòu);
控制父Context中的before/after的代碼塊集合對(duì)子Context的可見性;
public void addChild(Context child) { desc.addChild(child.desc); executors.add(child); child.addBefore(collect(befores)); child.addAfter(collect(afters)); }
其中,collect定義于Block接口中,完成before/after代碼塊「集合」的迭代處理。這類似于OO世界中的「組合模式」,它們代表了一種隱式的「樹狀結(jié)構(gòu)」。
public interface Block { void apply() throws Throwable; static Block collect(Iterable extends Block> blocks) { return () -> { for (Block b : blocks) { b.apply(); } }; } }實(shí)現(xiàn)addExecutor
其中,Executor存在兩種情況:
Spec: 使用it定義的用例的代碼塊
Context: 使用describe定義上下文。
為此,addExecutor被addSpec, addChild所調(diào)用。addExecutor調(diào)用時(shí),將Spec注冊(cè)到Executor集合中,并定義了Spec的「執(zhí)行規(guī)則」。
private void addExecutor(Description desc, Block block) { Spec spec = new Spec(desc, blocksInContext(block)); executors.add(spec); } private Block blocksInContext(Block block) { return collect(collect(befores), block, collect(afters)); }
blocksInContext將it的「執(zhí)行序列」行為固化。
首先執(zhí)行before代碼塊集合;
然后執(zhí)行it代碼塊;
最后執(zhí)行after代碼塊集合;
抽象Executor之前談過,Executor存在兩種情況:
Spec: 使用it定義的用例的代碼塊
Context: 使用describe定義上下文。
也就是說,Executor構(gòu)成了一棵「樹狀」的數(shù)據(jù)結(jié)構(gòu);it扮演了「葉子節(jié)點(diǎn)」的角色;Context扮演了「非葉子節(jié)點(diǎn)」的角色。為此,Executor的設(shè)計(jì)采用了「組合模式」。
import org.junit.runner.notification.RunNotifier; @FunctionalInterface public interface Executor { void exec(RunNotifier notifier); }葉子節(jié)點(diǎn):Spec
Spec完成對(duì)it行為的封裝,當(dāng)exec時(shí)完成it代碼塊() -> {...}的調(diào)用。
public class Spec implements Executor { public Spec(Description desc, Block block) { this.desc = desc; this.block = block; } @Override public void exec(RunNotifier notifier) { notifier.fireTestStarted(desc); runSpec(notifier); notifier.fireTestFinished(desc); } private void runSpec(RunNotifier notifier) { try { block.apply(); } catch (Throwable t) { notifier.fireTestFailure(new Failure(desc, t)); } } private Description desc; private Block block; }非葉子節(jié)點(diǎn):Context
public class Context implements Executor { ...... private Description desc; @Override public void exec(RunNotifier notifier) { for (Executor e : executors) { e.exec(notifier); } } }實(shí)現(xiàn)DSL
有了Context的領(lǐng)域模型的基礎(chǔ),DSL的實(shí)現(xiàn)變得簡(jiǎn)單了。
public class JSpec { private static Deque上下文切換ctxts = new ArrayDeque (); public static void describe(String desc, Block block) { Context ctxt = new Context(createSuiteDescription(desc)); enterCtxt(ctxt, block); } public static void it(String behavior, Block block) { currentCtxt().addSpec(behavior, block); } public static void before(Block block) { currentCtxt().addBefore(block); } public static void after(Block block) { currentCtxt().addAfter(block); } private static void enterCtxt(Context ctxt, Block block) { currentCtxt().addChild(ctxt); applyBlock(ctxt, block); } private static void applyBlock(Context ctxt, Block block) { ctxts.push(ctxt); doApplyBlock(block); ctxts.pop(); } private static void doApplyBlock(Block block) { try { block.apply(); } catch (Throwable e) { it("happen to an error", failing(e)); } } private static Context currentCtxt() { return ctxts.peek(); } }
但為了控制Context之間的「樹型關(guān)系」(即describe的嵌套關(guān)系),為此建立了一個(gè)Stack的機(jī)制,保證運(yùn)行時(shí)在某一個(gè)時(shí)刻Context的唯一性。
只有describe的調(diào)用會(huì)開啟「上下文的建立」,并完成上下文「父子關(guān)系」的鏈接。其余操作,例如it, before, after都是在當(dāng)前上下文進(jìn)行「元信息」的注冊(cè)。
虛擬的根結(jié)點(diǎn)使用靜態(tài)初始化塊,完成「虛擬根結(jié)點(diǎn)」的注冊(cè);也就是說,在運(yùn)行時(shí)初始化時(shí),棧中已存在唯一的 Context("JSpec: All Specs")虛擬根節(jié)點(diǎn)。
public class JSpec { private static Deque運(yùn)行器ctxts = new ArrayDeque (); static { ctxts.push(new Context(createSuiteDescription("JSpec: All Specs"))); } ...... }
為了配合JUnit框架將JSpec運(yùn)行起來,需要定制一個(gè)JUnit的Runner。
public class JSpec extends Runner { private Description desc; private Context root; public JSpec(Class> suite) { desc = createSuiteDescription(suite); root = new Context(desc); enterCtxt(root, reflect(suite)); } @Override public Description getDescription() { return desc; } @Override public void run(RunNotifier notifier) { root.exec(notifier); } ...... }
在編寫用例時(shí),使用@RunWith(JSpec.class)注解,告訴JUnit定制化了運(yùn)行器的行為。
@RunWith(JSpec.class) public class JSpecs {{ ...... }}
在之前已討論過,JSpec的run無非就是將「以樹形組織的」Executor集合調(diào)度起來。
實(shí)現(xiàn)reflectJUnit在運(yùn)行時(shí),首先看到了@RunWith(JSpec.class)注解,然后反射調(diào)用JSpec的構(gòu)造函數(shù)。
public JSpec(Class> suite) { desc = createSuiteDescription(suite); root = new Context(desc); enterCtxt(root, reflect(suite)); }
通過Block.reflect的工廠方法,將開始執(zhí)行測(cè)試用例集的「初始化塊」。
public interface Block { void apply() throws Throwable; static Block reflect(Class> c) { return () -> { Constructor> cons = c.getDeclaredConstructor(); cons.setAccessible(true); cons.newInstance(); }; } }
此刻,被@RunWith(JSpec.class)注解標(biāo)注的「初始化塊」被執(zhí)行。
@RunWith(JSpec.class) public class JSpecs {{ ...... }}
在「初始化塊」中順序完成對(duì)describe, it, before, after等子句的調(diào)用,其中:
describe開辟新的Context;
describe可以遞歸地調(diào)用內(nèi)部嵌套的describe;
describe調(diào)用it, before, after時(shí),將信息注冊(cè)到了Context中;
最終Runner.run將Executor集合按照「樹」的組織方式調(diào)度起來;
GitHubJSpec已上傳至GitHub:https://github.com/horance-liu/jspec,代碼細(xì)節(jié)請(qǐng)參考源代碼。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/65594.html
摘要:袁英杰回顧設(shè)計(jì)上次在軟件匠藝小組上分享了正交設(shè)計(jì)的基本理論,原則和應(yīng)用,在活動(dòng)線下收到了很多朋友的反饋。強(qiáng)迫用戶雖然的設(shè)計(jì)高度可復(fù)用性,可由用戶根據(jù)實(shí)際情況,自由拼裝組合各種算子。鳴謝正交設(shè)計(jì)的理論原則及其方法論出自前軟件大師袁英杰先生。 軟件設(shè)計(jì)是一個(gè)「守破離」的過程。 --袁英杰 回顧設(shè)計(jì) 上次在「軟件匠藝小組」上分享了「正交設(shè)計(jì)」的基本理論,原則和應(yīng)用,在活動(dòng)線下收到了很多朋友的...
摘要:在這個(gè)例子中,我們將整合但您也可以使用其他連接池,如,,等。作為構(gòu)建和執(zhí)行。 jOOQ和Spring很容易整合。 在這個(gè)例子中,我們將整合: Alibaba Druid(但您也可以使用其他連接池,如BoneCP,C3P0,DBCP等)。 Spring TX作為事物管理library。 jOOQ作為SQL構(gòu)建和執(zhí)行l(wèi)ibrary。 一、準(zhǔn)備數(shù)據(jù)庫(kù) DROP TABLE IF EXIS...
摘要:轉(zhuǎn)換成為模板函數(shù)聯(lián)系上一篇文章,其實(shí)模板函數(shù)的構(gòu)造都大同小異,基本是都是通過拼接函數(shù)字符串,然后通過對(duì)象轉(zhuǎn)換成一個(gè)函數(shù),變成一個(gè)函數(shù)之后,只要傳入對(duì)應(yīng)的數(shù)據(jù),函數(shù)就會(huì)返回一個(gè)模板數(shù)據(jù)渲染好的字符串。 教程目錄1.手把手教你從零寫一個(gè)簡(jiǎn)單的 VUE2.手把手教你從零寫一個(gè)簡(jiǎn)單的 VUE--模板篇 Hello,我又回來了,上一次的文章教會(huì)了大家如何書寫一個(gè)簡(jiǎn)單 VUE,里面實(shí)現(xiàn)了VUE 的...
閱讀 3206·2021-09-06 15:02
閱讀 2255·2019-08-30 15:48
閱讀 3450·2019-08-29 11:08
閱讀 3294·2019-08-26 13:55
閱讀 2456·2019-08-26 13:35
閱讀 3172·2019-08-26 12:11
閱讀 2610·2019-08-26 11:48
閱讀 894·2019-08-26 11:42