Skip to content

Commit

Permalink
1. tenant_id
Browse files Browse the repository at this point in the history
2. unit-testing-overview.md
3. copy code
  • Loading branch information
levy committed Sep 24, 2023
1 parent bb0a6a5 commit 13ab49c
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/.vuepress/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
25 changes: 25 additions & 0 deletions src/daily/copy-code-may-not-be-guilty.md
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 src/daily/you-dont-need-to-add-tenant_id-to-every-table.md
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 的设计很傻逼!
142 changes: 142 additions & 0 deletions src/software-testing/unit-testing-overview.md
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))
;
}

```

0 comments on commit 13ab49c

Please sign in to comment.