diff --git a/src/.vuepress/sidebar.ts b/src/.vuepress/sidebar.ts index 89262005..0b150adf 100644 --- a/src/.vuepress/sidebar.ts +++ b/src/.vuepress/sidebar.ts @@ -43,6 +43,7 @@ export default sidebar({ text: "软件测试", prefix: "/software-testing/", children: [ + 'unit-testing-overview', "use-postman-for-api-testing", "use-RestAssured-for-api-testing", "use-jest-for-test-driven-development", diff --git a/src/daily/copy-code-may-not-be-guilty.md b/src/daily/copy-code-may-not-be-guilty.md new file mode 100644 index 00000000..f6cbe833 --- /dev/null +++ b/src/daily/copy-code-may-not-be-guilty.md @@ -0,0 +1,25 @@ +--- +date: 2023-09-24 +tag: +- Daily +--- + +# 复制代码也许不是罪 +## 前言 +熟悉我的人都知道,我对代码是有追求的。 + +正式参考工作后,我就知道,复制粘贴是坏的实践,自己一直极力避免做这样的事。要是遇到了别人复制粘贴,要么喷,要么自己改。 + +我早期认为:复制代码就是菜。 + +后来认为:复制代码可能不是菜,而是懒,没有素养,自我要求。 + +而现在:代码其实也没那么重要;某些情况下复制粘贴是可以接受的。 + +编码经过七个年头,我思想上为何会有如此改变?难道这就是传说中的七年之痒? + + + +![](https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1695539182620-f3e4d2e3-bd24-4211-bb61-f5104b0e7ef3.jpeg) +![](https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1695539182844-04210738-e753-43c8-8917-a1c98e8f4d77.jpeg) +![](https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1695539128677-28080825-d512-41fe-85e4-6a56553d25f1.jpeg) diff --git a/src/daily/you-dont-need-to-add-tenant_id-to-every-table.md b/src/daily/you-dont-need-to-add-tenant_id-to-every-table.md new file mode 100644 index 00000000..c8796deb --- /dev/null +++ b/src/daily/you-dont-need-to-add-tenant_id-to-every-table.md @@ -0,0 +1,39 @@ +--- +date: 2023-09-18 +tag: +- Design +- Daily +--- + +# 技术点评:别每张表都加tenant_id +## 前言 +系统满足多租户需求,是很常见的场景。本文主要聊一下在维护旧系统过程中,发现的前人多租户方案中设计、实现不合理的地方。 + + +## 背景 +在维护旧系统时,踩了各种坑,终于忍不住在群里吐槽了下,于是有以下对话。 +![](https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1695539128515-cca8f7a3-6846-48ca-9eef-d7395c186ae2.jpeg) +![](https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1695539128672-66e5d124-51c2-4327-b9c2-a0e72c1ba9f4.jpeg) +![](https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1695539128413-7a5ccc98-e7ec-4fb7-94ee-d78bdc5b0bcf.jpeg) +既然群友有疑问,我索性就整理了一下,把前因后果清楚。 + +## 正文 +1.首先,租户数据隔离级别应该如何,没有唯一标准,评价只有是否合适。因此,逻辑隔离、物理隔离,都不是吐槽点。 + +2.原来的设计是,采取逻辑隔离方案,具体做法是,给每一张表都加上了tenant_id字段。问题就在于,有必要每一张表加吗?系统的权限设计是:先有租户,再有应用,用户、角色、资源、权限设置等内容都挂在应用下,所以,应用下的内容,关联 app_id 就行了,根本不需要tenant_id。 + +3.冗余多一个字段,会出现什么问题呢?先不提查询性能、存储空间等细节,就说很实际的场景: +3.1 每张表都加 tenant_id,几乎每条 sql 都要加 where tenant_id = ? ,那程序员会怎么做?首先想到的是用框架自动注入 sql +3.2 在后台管理端,有一个超级管理员,能够查询出所有租户的内容,也就是说,此时查询不能带 where tenant_id = ?,也即不能让框架注入 sql + +要同时兼容上述逻辑,程序员又会怎么做呢?于是就引入了万恶的全局变量,类似于 injectSql = true,就添加 tenant_id 作为过滤条件。但默认是不是要 injectSql = true 呢?每个项目代码又不一样,你不运行,你都不知道。 + +更恶心的是,需求变化后,是否需要带上 tenant_id 的逻辑与原来不一致时,你得在某行代码执行前,手动设置 inejctSql 的值,在该行代码之后,再手动复原——因为如果不复原,作为全局变量,会影响到后面的代码! + +md,这时候后你才会知道,还不如老老实实地设置 tenant_id,显示地设置,好过这种隐蔽的依赖。 + +4.但这还不是最难搞的。因为上述的是代码问题,真正难搞的是数据问题。考虑一种场景:超级管理员在后台管理某租户的应用,手动为租户添加数据,请问,新增的数据 tenant_id 的值是什么,某租户的 id,还是超级管理员的id?按逻辑来说,应该是某租户的 tenant_id。但问题在于,由于理解不同,或由于疏忽让框架自动注入了 tenant_id,导致上述场景,有些数据的 tenant_id 是超级管理员的id。而又因为超级管理员进行查询时,是不带 tenant_id 作为过滤条件的,因此即使 tenant_id 的值设置错误,依然在界面上能显示,使得这个问题一直存在着,旧数据一直被保留并使用。 + +5.现在,有需求要导出某个租户下的数据,结果发现 tenant_id 乱七八糟,你难不难受? + +6.那么,梳理完逻辑链条,我认为,虽然某些程序员的在实现上犯了低级错误,但不是主要原因,罪魁祸首应该是设计上的懒惰。设计精细一点,明确好 tenant_id 到哪张表为止,也就没有后面的 sql 注入、数据错误那么多事了。所以,我才说,给租户隔离等于给每张表加 tenant_id 的设计很傻逼! diff --git a/src/software-testing/unit-testing-overview.md b/src/software-testing/unit-testing-overview.md new file mode 100644 index 00000000..ddecbdda --- /dev/null +++ b/src/software-testing/unit-testing-overview.md @@ -0,0 +1,142 @@ +--- +date: 2023-07-28 +tag: +- Testing +--- + +# 单元测试概述 +## Why +为什么要做单元测试?或者说,为什么要写测试代码? + +个人总结为以下两点: + +1. [测试左移](https://www.stickyminds.com/article/shift-left-approach-software-testing),降低修复bug的成本 +![](https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1690532448643-e09bebb0-66f2-49f9-8686-d4a8c6b5d590.png) +2. 形成资产,方便回归测试,后续迭代重构、维护有保障 + + + +以上两点,是研发人员写测试代码的本质理由,无论什么类型的测试代码、研发人员用的什么语言、框架都适用。 +## What +写测试代码究竟是写什么? + +个人认为测试代码主要是为了搞清楚两件事: + +1. 源码到底会不会在目标环境执行? +2. 源码的执行结果是否符合预期? + +第一件事,引出了 code coverage 代码覆盖率的概念;第二件事,则引出了 assert 断言的概念。 +## How +### 测试代码的风格 +[AAA](https://medium.com/@pjbgf/title-testing-code-ocd-and-the-aaa-pattern-df453975ab80) 风格: + +1. 组装参数 +2. 执行目标方法 +3. 执行断言 +```java + @Test + public void testHash() throws Exception { + // Arrange + String plainText = JSON.toJSONString(licenseRequest); + + // Act + String digest = hash(plainText); + + // Assert + Assert.assertEquals(digest, "myhash"); + } + +``` + +尤其注意最后的断言,如果没有断言,不叫测试。 + +常见的错误就是,不写断言,而使用 `System.out.println()`来判断执行结果。 +这样做无法结合 CI 形成有效的自动化测试。 因为这种做法只能让编译通过,源码逻辑也许已经错误了,但测试结果仍然 100% 通过,这是没有意义的。 + +### 测试难点 +以函数的观点来看。 + +输入: + +1. 内存数据 +2. 外部数据 + +输出: + +1. 内存数据 +2. 数据库 +3. 文件系统 +4. 网络调用 + +单元测试从严格意义上来说需要满足三个No: + +1. No DB +2. No Network +3. No I/O + +由此,引出了 Mock 的概念及技术。作为单元测试,需要 Mock 依赖,准备好输入数据,并想办法在内存中验证外部输出。 + +也即,重要的是隔离依赖,让测试可重复执行。 +### 常用工具 + +1. [Junit](https://junit.org/junit5/) +2. [Mocktio](https://site.mockito.org/) +3. [TestMe](https://plugins.jetbrains.com/plugin/9471-testme) + +## Bad Examples +以下是常见的错误测试示例,它们都不是合格的单元测试。 +### 没有测试类 +```java +public static void main(String[] args) { + // write a lot code to test +} +``` +经典错误:写一个 main 方法,把所有测试代码都放进去。这样做的后果是,无论是人还是机器,都不知道原来这里还有测试代码。 +### 没有断言 +```java +@Test +public void decryptPwdTest(){ + String pwdStr = "YT08KDijKt/rqhhKv9NrLA=="; + String decrypt = DatasourcePasswordUtils.decrypt(pwdStr); + System.out.println(decrypt); +} +``` +经典错误:(很可能是单纯地把测试代码从 main 方法移过来)没有断言,依赖人用肉眼判断输出正确与否。 + +```java +@Test +public void testGetSummary() throws Exception { + when(dao.countWithNoTenant(any())).thenReturn(0); + when(dao.countEnableWithNoTenant()).thenReturn(0); + when(dao.countWithNoTenant()).thenReturn(0); + + Result result = service.getResult(); +} +``` +这个例子虽然用上了 Mock 技术,但依赖掩盖不了没有断言的事实。这也许是为了达到测试覆盖率百分百而进行的投机取巧。 +### 无法重复执行 +```java +@Test +public void testAppendFile() throws Exception { + File file = new File("D://appendtest.txt"); + minioFileStorage.append(file, "/appendtest.txt"); + Assert.isTrue(file.exists(file)); +} +``` +如果代码 Linux 环境运行怎么办?哪里来的 D 盘? + +这种情况,正确的做法应该是把依赖的文件作为测试夹具,与测试代码一起放入版本控制中。 +![](https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1695537638532-e8092338-3de2-4019-99a2-03bfb98f781f.png) +参考代码如下: +```java + @Test + public void importSuccess() { + File file = new File("src/test/fixtures/file-import"); + + getImportResp(file) + .assertThat().body("code", org.hamcrest.Matchers.equalTo("0")) + .assertThat().body("payload", equalTo(true)) + ; + } + +```