新聞中心
當(dāng)應(yīng)用六邊形架構(gòu)(端口和適配器)訪問數(shù)據(jù)庫(kù)等基礎(chǔ)設(shè)施元素時(shí),可以通過適配器的方式實(shí)現(xiàn)。適配器只是域定義的接口(端口)的實(shí)現(xiàn)。本文將提供同一存儲(chǔ)庫(kù)端口的兩個(gè)實(shí)現(xiàn),一個(gè)在內(nèi)存中,另一個(gè)基于JPA。其重點(diǎn)是如何使用相同的測(cè)試集測(cè)試這兩個(gè)實(shí)現(xiàn)。?

創(chuàng)新互聯(lián)建站主營(yíng)張家界網(wǎng)站建設(shè)的網(wǎng)絡(luò)公司,主營(yíng)網(wǎng)站建設(shè)方案,成都App定制開發(fā),張家界h5重慶小程序開發(fā)搭建,張家界網(wǎng)站營(yíng)銷推廣歡迎張家界等地區(qū)企業(yè)咨詢
場(chǎng)景
許多在企業(yè)場(chǎng)景中開發(fā)的軟件解決方案都有一些狀態(tài),需要保存在持久存儲(chǔ)設(shè)備中以供以后訪問。根據(jù)特定的功能性需求和非功能性需求,選擇正確的持久性解決方案可能很難,而且很可能需要一份架構(gòu)決策記錄(ADR),其中詳細(xì)說明了選擇的基本原理,包括替代方案和權(quán)衡。為了持久保持應(yīng)用程序狀態(tài),用戶需要參考CAP定理來做出最適當(dāng)?shù)臎Q策。 ?
這個(gè)決策過程不應(yīng)該延遲應(yīng)用程序域模型的設(shè)計(jì)和開發(fā)。工程團(tuán)隊(duì)?wèi)?yīng)該專注于交付(業(yè)務(wù))價(jià)值,而不是維護(hù)一堆DDL腳本和開發(fā)一個(gè)高度變化的數(shù)據(jù)庫(kù)模式,在幾周(或幾個(gè)月)之后,他們會(huì)意識(shí)到使用文檔數(shù)據(jù)庫(kù)而不是關(guān)系數(shù)據(jù)庫(kù)可能會(huì)更好。 ?
同樣,關(guān)注交付域值也會(huì)阻止工程團(tuán)隊(duì)基于過早采取的技術(shù)或基礎(chǔ)設(shè)施相關(guān)決策(例如在本例中是數(shù)據(jù)庫(kù)技術(shù))的約束而做出與域相關(guān)的決策。正如行業(yè)專家所說,其架構(gòu)應(yīng)該允許延遲框架決策(以及基礎(chǔ)設(shè)施決策)。 ?
推遲與基礎(chǔ)設(shè)施相關(guān)的決策
回到數(shù)據(jù)庫(kù)技術(shù)的例子,一種推遲基礎(chǔ)設(shè)施決策的方法是決定使用哪種數(shù)據(jù)庫(kù)技術(shù)應(yīng)使用,它將從存儲(chǔ)庫(kù)的簡(jiǎn)單內(nèi)存實(shí)現(xiàn)開始,其中域?qū)嶓w可以存儲(chǔ)在內(nèi)存中的列表中。這種方法加速了特性和領(lǐng)域用例的發(fā)現(xiàn)、設(shè)計(jì)和實(shí)現(xiàn),使利益相關(guān)者能夠快速反饋重要事項(xiàng):域值。?
現(xiàn)在,有人可能會(huì)想,“但是,我并沒有交付一個(gè)端到端工作的特性”,或者“我如何使用存儲(chǔ)庫(kù)的內(nèi)存適配器驗(yàn)證這一特性?”在這里,像六邊形架構(gòu)(也稱為端口和適配器)這樣的架構(gòu)模式和像域驅(qū)動(dòng)設(shè)計(jì)(DDD)這樣的方法(對(duì)于擁有干凈的架構(gòu)和最終干凈的代碼來說不是強(qiáng)制性的)開始發(fā)揮作用。 ?
六邊形架構(gòu)
許多應(yīng)用程序是按照經(jīng)典的三層架構(gòu)設(shè)計(jì)的:?
(1)演示/控制器 ?
(2)服務(wù)(業(yè)務(wù)邏輯) ?
(3)持久層 ?
這種架構(gòu)傾向于將域定義(例如,域?qū)嶓w和值對(duì)象)與表(例如,ORM實(shí)體)混合在一起,通常表示為簡(jiǎn)單的數(shù)據(jù)傳輸對(duì)象。如下圖所示:?
與其相反,在六邊形架構(gòu)中,實(shí)際的持久性相關(guān)類都是基于域模型定義的。?
通過使用存儲(chǔ)庫(kù)的端口 (它被定義為域模型的一部分),可以定義與底層技術(shù)無關(guān)的集成測(cè)試,它驗(yàn)證了對(duì)存儲(chǔ)庫(kù)的域期望。以下了解在用于管理學(xué)生的簡(jiǎn)單域模型中的代碼是什么樣子的。 ?
展示代碼
作為域的一部分,這個(gè)存儲(chǔ)庫(kù)端口看起來如何呢?它本質(zhì)上定義了域?qū)Υ鎯?chǔ)庫(kù)的期望,并根據(jù)域泛在語(yǔ)言定義了所有方法: ?
Java
public interface StudentRepository {
Student save(Student student);
OptionalretrieveStudentWithEmail(ContactInfo contactInfo);
PublishersaveReactive(Student student);
}
基于存儲(chǔ)庫(kù)端口規(guī)范,可以創(chuàng)建集成測(cè)試定義。該定義僅依賴于端口,并且不知道為持久化域狀態(tài)而做出的任何底層技術(shù)決策。這個(gè)測(cè)試類將有一個(gè)屬性作為驗(yàn)證期望的存儲(chǔ)庫(kù)接口(端口)的實(shí)例。以下顯示了這些測(cè)試的樣子:?
Java
public class StudentRepositoryTest {
StudentRepository studentRepository;
@Test
public void shouldCreateStudent() {
Student expected = randomNewStudent();
Student actual = studentRepository.save(expected);
assertAll("Create Student",
() -> assertEquals(0L, actual.getVersion()),
() -> assertEquals(expected.getStudentName(), actual.getStudentName()),
() -> assertNotNull(actual.getStudentId())
);
}
@Test
public void shouldUpdateExistingStudent() {
Student expected = randomExistingStudent();
Student actual = studentRepository.save(expected);
assertAll("Update Student",
() -> assertEquals(expected.getVersion()+1, actual.getVersion()),
() -> assertEquals(expected.getStudentName(), actual.getStudentName()),
() -> assertEquals(expected.getStudentId(), actual.getStudentId())
);
}
}
一旦存儲(chǔ)庫(kù)測(cè)試定義完成,就可以為內(nèi)存存儲(chǔ)庫(kù)創(chuàng)建一個(gè)測(cè)試運(yùn)行時(shí)(集成測(cè)試): ?
Java
public class StudentRepositoryInMemoryIT extends StudentRepositoryTest {
@BeforeEach
public void setup() {
super.studentRepository = new StudentRepositoryInMemory();
}
}
或者使用Postgres對(duì)JPA進(jìn)行更詳細(xì)的集成測(cè)試: ?
Java
@Testcontainers
@ContextConfiguration(classes = {PersistenceConfig.class})
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class StudentRepositoryJpaIT extends StudentRepositoryTest{
@Autowired
public StudentRepository studentRepository;
@Container
public static PostgreSQLContainer container = new PostgreSQLContainer("postgres:latest")
.withDatabaseName("students_db")
.withUsername("sa")
.withPassword("sa");
@DynamicPropertySource
public static void overrideProperties(DynamicPropertyRegistry registry){
registry.add("spring.datasource.url", container::getJdbcUrl);
registry.add("spring.datasource.username", container::getUsername);
registry.add("spring.datasource.password", container::getPassword);
registry.add("spring.datasource.driver-class-name", container::getDriverClassName);
}
@BeforeEach
public void setup() {
super.studentRepository = studentRepository;
}
}
兩個(gè)測(cè)試運(yùn)行時(shí)都擴(kuò)展了相同的測(cè)試定義,因此可以確定,當(dāng)從內(nèi)存適配器切換到最終的全功能JPA持久性時(shí),不會(huì)有任何測(cè)試受到影響,因?yàn)樗恍枰渲孟鄳?yīng)的測(cè)試運(yùn)行時(shí)。
這種方法將允許用戶在不依賴于框架的情況下定義存儲(chǔ)庫(kù)端口的測(cè)試,并在域定義更好、更穩(wěn)定,以及團(tuán)隊(duì)決定使用更好地滿足解決方案質(zhì)量屬性的數(shù)據(jù)庫(kù)技術(shù)時(shí)重用這些測(cè)試。?
項(xiàng)目的整體結(jié)構(gòu)如下圖所示:?
項(xiàng)目的結(jié)構(gòu)介紹:?
- student-domain:域定義模塊,包括實(shí)體、值對(duì)象、域事件、端口等。這個(gè)模塊不依賴于框架,盡可能使用Java。 ?
- student-application:目前,這個(gè)模塊沒有代碼,因?yàn)樗隽吮疚牡姆秶W裱呅渭軜?gòu),該模塊編排對(duì)域模型的調(diào)用,成為域用例的入口點(diǎn)。 ?
- student-repository-test:這個(gè)模塊包含存儲(chǔ)庫(kù)測(cè)試定義,不依賴于框架,只驗(yàn)證所提供的存儲(chǔ)庫(kù)端口的期望。 ?
- student-repository-inmemory:域定義的存儲(chǔ)庫(kù)端口的內(nèi)存實(shí)現(xiàn)。它還包含集成測(cè)試,該測(cè)試為學(xué)生存儲(chǔ)庫(kù)測(cè)試的測(cè)試定義提供了端口的內(nèi)存適配器。 ?
- student-repository-JPA:域定義的存儲(chǔ)庫(kù)端口的JPA實(shí)現(xiàn)。它還包含集成測(cè)試,該測(cè)試為學(xué)生存儲(chǔ)庫(kù)測(cè)試的測(cè)試定義提供了端口的內(nèi)存適配器。這個(gè)集成測(cè)試設(shè)置有點(diǎn)復(fù)雜,因?yàn)樗鼘⒁粋€(gè)基本的Spring場(chǎng)景和一個(gè)Postgres容器一起啟動(dòng)。
- student-shared-kernel:這個(gè)模塊不在本文討論范圍之內(nèi);它為設(shè)計(jì)項(xiàng)目的其余部分提供了一些實(shí)用程序類和接口。
結(jié)論
在項(xiàng)目中使用這種架構(gòu)風(fēng)格可以促進(jìn)域模型和基礎(chǔ)設(shè)施元素之間的良好分離,確保后者不會(huì)影響前者,同時(shí)促進(jìn)良好的代碼質(zhì)量(干凈的代碼)和高可維護(hù)性。 ?
原文標(biāo)題:??Testing Repository Adapters With Hexagonal Architecture??,作者:David Cano
分享題目:如何使用六邊形架構(gòu)測(cè)試存儲(chǔ)庫(kù)適配器
URL網(wǎng)址:http://www.dlmjj.cn/article/cddppes.html


咨詢
建站咨詢
