在我们对数据库 DAO 类进行单元测试时,通常不应该依赖于一个外部数据库,所以会选用特定比较接近于真实数据库类型的内存或嵌入式数据库,如 HSQLDB(HyperSQL), H2, Derby 等。但总难免会用到特定数据库的特性,这时候就无法用前述各种数据库进行测试了。非要单元测试中覆盖到所用的数据库特性的话可以选择用 docker,如 Testcontainers , 经过模块扩展,它可以由 docker 来启动许多种类型的数据库,MySQL, Postgres, Oracle-XE, MS SQL Server, Couchbase 等等,详情见 Database containers 。刚了解到的是它的模块化的无限可能,像支持 Kafka Containers 和 Localstack Module 等。
这里就不走 Testcontainers 那条路 -- 要求构建服务器上也要有 docker。早先希望能找到一种嵌入式或内存 PostgreSQL 数据库,后来发现 PostgreSQL 未能提供 In-Process 和 In-Memory 的启动方式,好在 PostgreSQL 是开源,有人可以把它改造为小型的可由测试代码启停的本地数据库。有两个具有代表性的组件,分别是 OpenTable Embedded PostgreSQL Component 和 Embedded PostgreSQL Server ,它们都号称是 Embedded,所谓嵌入式,其实是进测试进程外的数据库。
下面简单体验下两个组件的用法
在 Maven 项目中引入该组件
<dependency> <groupId>com.opentable.components</groupId> <artifactId>otj-pg-embedded</artifactId> <version>0.13.1</version> <!-- 当前版本是 0.13.1 --> <scope>test</scope> </dependency>
@Rule public SingleInstancePostgresRule pg = EmbeddedPostgresRules.singleInstance();
在测试中就可以用下面的方式获得相应的数据源,不用关心启动 PostgreSQL 所用的端口号,它会随机选用端口号
然后进行执行自己的数据库初始化代码,创建新表,插入测试数据等等。
上面 @Rule 在初始和终止时有类似下面的信息日志输出(节选了重要部分)
2019-06-04 02:51:49,873 INFO class=EmbeddedPostgres thread=main event_description="Detected a Darwin x86_64 system"
2019-06-04 02:51:50,043 INFO class=EmbeddedPostgres thread=main event_description="Postgres binaries at /var/folders/xz/vqv039517flcxtqzrq_jjy1xqzfzc0/T/embedded-pg/PG-3d7ce5d05cd575a649dd635576931b19 "
......................
2019-06-04 02:51:50,113 INFO class=init-aa136468-71bb-40a5-9d6b-0284db0eaa86:initdb thread=log:pid(64860) event_description="fixing permissions on existing directory /var/folders/xz/vqv039517flcxtqzrq_jjy1xqzfzc0/T/epg1343892286024759082 ... ok"
......................
2019-06-04 02:51:51,859 INFO class=init-aa136468-71bb-40a5-9d6b-0284db0eaa86:initdb thread=log:pid(64860) event_description=" /var/folders/xz/vqv039517flcxtqzrq_jjy1xqzfzc0/T/embedded-pg/PG-3d7ce5d05cd575a649dd635576931b19/bin/pg_ctl -D /var/folders/xz/vqv039517flcxtqzrq_jjy1xqzfzc0/T/epg1343892286024759082 -l logfile start "
2019-06-04 02:51:51,857 INFO class=EmbeddedPostgres thread=main event_description="aa136468-71bb-40a5-9d6b-0284db0eaa86 initdb completed in 00:00:01.810"
2019-06-04 02:51:51,875 INFO class=EmbeddedPostgres thread=main event_description="aa136468-71bb-40a5-9d6b-0284db0eaa86 postmaster started as java.lang.UNIXProcess@481a996b on port 60180. Waiting up to PT10S for server startup to finish."
2019-06-04 02:51:51,935 INFO class=pg-aa136468-71bb-40a5-9d6b-0284db0eaa86 thread=log:pid(64871) event_description="waiting for server to start....2019-06-03 21:51:51.935 CDT [64873] LOG: listening on IPv6 address "::1", port 60180"
2019-06-04 02:51:51,936 INFO class=pg-aa136468-71bb-40a5-9d6b-0284db0eaa86 thread=log:pid(64871) event_description="2019-06-03 21:51:51.935 CDT [64873] LOG: listening on IPv4 address "127.0.0.1", port 60180"
2019-06-04 02:51:51,936 INFO class=pg-aa136468-71bb-40a5-9d6b-0284db0eaa86 thread=log:pid(64871) event_description="2019-06-03 21:51:51.936 CDT [64873] LOG: listening on Unix socket "/tmp/.s.PGSQL.60180""
......................
2019-06-04 02:51:52,010 INFO class=pg-aa136468-71bb-40a5-9d6b-0284db0eaa86 thread=log:pid(64871) event_description="server started"
2019-06-04 02:51:52,104 INFO class=EmbeddedPostgres thread=main event_description="aa136468-71bb-40a5-9d6b-0284db0eaa86 postmaster startup finished in 00:00:00.234"
2019-06-04 02:51:52,239 INFO class=init-aa136468-71bb-40a5-9d6b-0284db0eaa86:pg_ctl thread=log:pid(64884) event_description="waiting for server to shut down.... done"
2019-06-04 02:51:52,239 INFO class=EmbeddedPostgres thread=main event_description="aa136468-71bb-40a5-9d6b-0284db0eaa86 shut down postmaster in 00:00:00.110"
输出的日志虽然删除了不少内容还是占了很大篇幅,上面粗体分别是 PostgreSQL 程序安装目录,数据库目录(测试完会被删除),和启动 PostgreSQL 数据库的命令,以及启动后各种连接方式(u端口号, Unix socket 连接等)
进到 PostgreSQL 的二进制目录查看到该组件 0.13.1 对应的 PostgreSQL 版本为 10.6
@Rule public PreparedDbRule db = EmbeddedPostgresRules.preparedDatabase( FlywayPreparer.forClasspathLocation("db/my-db-schema"));
也能由 db 得到数据源。
前面是用 @Rule 或 @ClassRule 来控制 PostgreSQL,到了 JUnit5 后摒弃了 @Rule 和 @ClassRule, 要迁移到 JUnit5 的 @ExtendWith,或是简单的用代码来控制
private static EmbeddedPostgres pg; @BeforeClass public static void initDb() throws IOException { pg = EmbeddedPostgres.builder().setCleanDataDirectory(true).start(); //初始化数据库 ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator(); DefaultResourceLoader resourceLoader = new DefaultResourceLoader(); databasePopulator.addScripts( resourceLoader.getResource("schema.sql"), resourceLoader.getResource("data.sql")); databasePopulator.execute(ps.getPostgresDatabase()); } @AfterClass public static void shutdownDb() throws IOException { pg.close(); }
除了 OpenTable Embedded PostgreSQL Component 外的另一个选择,首先 Maven 项目的话加上依赖
<dependency> <groupId>ru.yandex.qatools.embed</groupId> <artifactId>postgresql-embedded</artifactId> <version>2.10</version> <!-- 当前版本 --> </dependency>
它没有提供相就的 @Rule, 需要用代码控制启停
private static EmbeddedPostgres postgres; @BeforeClass public static void initDb() throws Exception { postgres = new EmbeddedPostgres(Version.V11_1/*, "/path/to/predefined/data/directory"*/); String url = postgres.start(); Connection conn = DriverManager.getConnection(url); //..... } @AfterClass public static void shutdonwDb() { postgres.stop(); }
启动时可以选择数据文件的目录或用默认的目录,由 EmbeddedPostgres
启动数据库后可获得 JDBC 连接字符串,不能直接得到数据源。它有多个 PostgreSQL 版本可选,9.5, 9.6, 10.6, 11.1 可选。它唯有一个长处是由 EmbeddedPostgres
可直捣 PostgreSQL 的数据库进程,从而进行某些 postgres
命令能进行的操作,见下图
也来窥探一下它的启停过程
Download Version{11.1-1}:OS_X:B64 START
Download Version{11.1-1}:OS_X:B64 DownloadSize: 242187339
Download Version{11.1-1}:OS_X:B64 0% 1% 2% 3% 4%............ 97% 98% 99% 100% Download Version{11.1-1}:OS_X:B64 downloaded with 3331kb/s
Download Version{11.1-1}:OS_X:B64 DONE
Extract /Users/yanbin/.embedpostgresql/postgresql-11.1-1-osx-binaries.zip START
........................................................................................Extract /Users/yanbin/.embedpostgresql/postgresql-11.1-1-osx-binaries.zip DONE
2019-06-04 03:52:43,645 INFO class=Executable thread=main event_description="start AbstractPostgresConfig{storage=Storage{dbDir= /var/folders/xz/vqv039517flcxtqzrq_jjy1xqzfzc0/T/postgresql-embed-821b4ee9-ccaa-45b1-a1b2-dfa6e0359869/db-content-0e69f370-ac09-4b33-be86-e3097670189f , dbName='postgres', isTmpDir=true}, network=Net{host='localhost', port=61845 }, timeout=Timeout{startupTimeout=15000}, credentials=Credentials{username='postgres', password='postgres'}, args=[], additionalInitDbParams=[-E, SQL_ASCII, --locale=C, --lc-collate=C, --lc-ctype=C]}"
2019-06-04 03:52:44,516 INFO class=Executable thread=main event_description="start AbstractPostgresConfig{storage=Storage{dbDir=/var/folders/xz/vqv039517flcxtqzrq_jjy1xqzfzc0/T/postgresql-embed-821b4ee9-ccaa-45b1-a1b2-dfa6e0359869/db-content-0e69f370-ac09-4b33-be86-e3097670189f, dbName='postgres', isTmpDir=true}, network=Net{host='localhost', port=61845}, timeout=Timeout{startupTimeout=15000}, credentials=Credentials{username='postgres', password='postgres'}, args=[postgres], additionalInitDbParams=[]}"
2019-06-04 03:52:44,542 INFO class=Executable thread=main event_description="start AbstractPostgresConfig{storage=Storage{dbDir=/var/folders/xz/vqv039517flcxtqzrq_jjy1xqzfzc0/T/postgresql-embed-821b4ee9-ccaa-45b1-a1b2-dfa6e0359869/db-content-0e69f370-ac09-4b33-be86-e3097670189f, dbName='postgres', isTmpDir=true}, network=Net{host='localhost', port=61845}, timeout=Timeout{startupTimeout=15000}, credentials=Credentials{username='postgres', password='postgres'}, args=[], additionalInitDbParams=[-E, SQL_ASCII, --locale=C, --lc-collate=C, --lc-ctype=C]}"
2019-06-04 03:52:44,664 INFO class=PostgresProcess thread=main event_description="trying to stop postgresql"
2019-06-04 03:52:44,741 INFO class=Executable thread=main event_description="start AbstractPostgresConfig{storage=Storage{dbDir=/var/folders/xz/vqv039517flcxtqzrq_jjy1xqzfzc0/T/postgresql-embed-821b4ee9-ccaa-45b1-a1b2-dfa6e0359869/db-content-0e69f370-ac09-4b33-be86-e3097670189f, dbName='postgres', isTmpDir=true}, network=Net{host='localhost', port=61845}, timeout=Timeout{startupTimeout=15000}, credentials=Credentials{username='postgres', password='postgres'}, args=[stop], additionalInitDbParams=[]}"
2019-06-04 03:52:44,868 INFO class=ProcessControl thread=main event_description="execSuccess: false [kill, 67796]"
第一次运行需要下载相应版本的 PostgreSQL 二进制包(240 多M),临时目录 ~/.embedpostgresql/
下有则无需下载,由以上日志也能看出实现原理与 OpenTable Embedded PostgreSQL Component 基本是一样的。
以下是两个组件的对比
OpenTable Embedded PostgreSQL
Embedded PostgreSQL Server
综上对比本人还是会选择 OpenTable Embedded PostgreSQL,再其次可能是 Testcontainers + PostgreSQL 模块。