-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
2. unit-testing-overview.md 3. copy code
- Loading branch information
levy
committed
Sep 24, 2023
1 parent
bb0a6a5
commit 13ab49c
Showing
4 changed files
with
207 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
--- | ||
date: 2023-09-24 | ||
tag: | ||
- Daily | ||
--- | ||
|
||
# 复制代码也许不是罪 | ||
## 前言 | ||
熟悉我的人都知道,我对代码是有追求的。 | ||
|
||
正式参考工作后,我就知道,复制粘贴是坏的实践,自己一直极力避免做这样的事。要是遇到了别人复制粘贴,要么喷,要么自己改。 | ||
|
||
我早期认为:复制代码就是菜。 | ||
|
||
后来认为:复制代码可能不是菜,而是懒,没有素养,自我要求。 | ||
|
||
而现在:代码其实也没那么重要;某些情况下复制粘贴是可以接受的。 | ||
|
||
编码经过七个年头,我思想上为何会有如此改变?难道这就是传说中的七年之痒? | ||
|
||
<!-- more --> | ||
|
||
![](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) |
39 changes: 39 additions & 0 deletions
39
src/daily/you-dont-need-to-add-tenant_id-to-every-table.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
--- | ||
date: 2023-09-18 | ||
tag: | ||
- Design | ||
- Daily | ||
--- | ||
|
||
# 技术点评:别每张表都加tenant_id | ||
## 前言 | ||
系统满足多租户需求,是很常见的场景。本文主要聊一下在维护旧系统过程中,发现的前人多租户方案中设计、实现不合理的地方。 | ||
|
||
<!-- more --> | ||
## 背景 | ||
在维护旧系统时,踩了各种坑,终于忍不住在群里吐槽了下,于是有以下对话。 | ||
![](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 的设计很傻逼! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. 形成资产,方便回归测试,后续迭代重构、维护有保障 | ||
|
||
<!-- more --> | ||
|
||
以上两点,是研发人员写测试代码的本质理由,无论什么类型的测试代码、研发人员用的什么语言、框架都适用。 | ||
## 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)) | ||
; | ||
} | ||
|
||
``` |