Skip to content

Commit f6a3815

Browse files
committed
learned Unit Test and Project Inspection
1 parent a8ae6ff commit f6a3815

File tree

9 files changed

+357
-2
lines changed

9 files changed

+357
-2
lines changed

help/8-WindUp/1-UnitTests.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# 单元测试
2+
3+
项目上线前的单元测试——让项目质量有保障
4+
5+
当然,之前我们每开发一个功能,也都进行了单元测试
6+
7+
- Spring Boot Testing
8+
- 依赖:spring-boot-starter-test
9+
- 包括:Junit,Spring Test,AssertJ……
10+
- Test Case(测试用例)
11+
- 要求:保证测试方法的独立性(A方法和B方法不能有数据依赖)
12+
- 步骤:初始化数据、执行测试代码、验证测试结果、清理测试数据 `数据单独为测试服务`
13+
- 常用注解:`@BeforeClass 类初始化前`, `@AfterClass 类销毁前`, `@Before`, `@After`
14+
15+
## 执行流程的例子
16+
17+
新建测试类
18+
19+
```java
20+
package com.nowcoder.community;
21+
22+
import org.junit.*;
23+
import org.junit.runner.RunWith;
24+
import org.springframework.boot.test.context.SpringBootTest;
25+
import org.springframework.test.context.ContextConfiguration;
26+
import org.springframework.test.context.junit4.SpringRunner;
27+
28+
@RunWith(SpringRunner.class)
29+
@SpringBootTest
30+
@ContextConfiguration(classes = CommunityApplication.class)
31+
public class SpringBootTests {
32+
33+
// 这个注解只能修饰静态方法,因为在类初始化之前执行
34+
@BeforeClass
35+
public static void beforeClass(){
36+
System.out.println("before class");
37+
}
38+
39+
@AfterClass
40+
public static void afterClass(){
41+
System.out.println("after class");
42+
}
43+
44+
// 这个注解在每个测试方法执行之前执行
45+
@Before
46+
public void before(){
47+
System.out.println("before");
48+
}
49+
50+
@After
51+
public void after(){
52+
System.out.println("after");
53+
}
54+
55+
@Test
56+
public void test1(){
57+
System.out.println("test1");
58+
}
59+
60+
@Test
61+
public void test2(){
62+
System.out.println("test2");
63+
}
64+
}
65+
```
66+
67+
可以若仅测试test1,看到控制台仅输出:
68+
69+
```
70+
before
71+
test1
72+
after
73+
```
74+
75+
而before class和after class是在测试类加载前、销毁前输出的
76+
77+
即:`before class ==> ( before ==> test ==> after) * n ==> after class`
78+
79+
## 一个实际的例子
80+
81+
在刚刚的测试方法中:
82+
83+
```java
84+
@Autowired
85+
private DiscussPostService discussPostService;
86+
87+
private DiscussPost data;
88+
89+
@Before
90+
public void before(){
91+
System.out.println("before");
92+
93+
// 初始化测试数据
94+
data = new DiscussPost();
95+
data.setUserId(111);
96+
data.setTitle("Test");
97+
data.setContent("Test content");
98+
data.setCreateTime(new Date());
99+
discussPostService.addDiscussPost(data);
100+
}
101+
102+
@After
103+
public void after(){
104+
System.out.println("after");
105+
106+
// 删除测试数据
107+
discussPostService.updateStatus(data.getId(), 2);
108+
}
109+
110+
@Test
111+
public void testFindById() {
112+
DiscussPost post = discussPostService.findDiscussPostById(data.getId()); // data是期望,post是实际
113+
// 通过断言判断是否符合预期 断言:成立什么都不发生,不成立抛异常
114+
Assertions.assertNotNull(post); // 判断查询结果是否非空,抛异常表示帖子为空
115+
Assertions.assertEquals(data.getTitle(), post.getTitle());
116+
Assertions.assertEquals(data.getContent(), post.getContent());
117+
}
118+
119+
@Test
120+
public void testUpdateScore() {
121+
int rows = discussPostService.updateScore(data.getId(), 2000.00);
122+
Assertions.assertEquals(1, rows);
123+
124+
DiscussPost post = discussPostService.findDiscussPostById(data.getId());
125+
// 小数比较时,需要指定一个误差范围:2000 +- 2
126+
Assertions.assertEquals(2000.00, post.getScore(), 2);
127+
}
128+
```
129+
130+
这样,调test1方法,就会为test1生成一份数据,用完即删。调test2时又会新生成一份数据。坏处是效率低,好处是相互独立,变量名相同但是数据不同
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# 项目监控
2+
3+
怎么判断项目运行时是否健康而稳定?
4+
5+
- Spring Boot Actuator —— 弥补了Spring Boot的过度封装,使底层暴露出来
6+
- EndPoints:监控应用的入口,Spring Boot内置了很多端点,也支持自定义端点
7+
- 监控方式:HTTP或JMX
8+
- 访问路径:例如`/actuator/health`
9+
- 注意事项:按需配置暴露的端点(如果一个端点从来不用就不需要配了),并对所有的端点进行权限控制
10+
11+
## 例子
12+
13+
1. 导包
14+
15+
```xml
16+
<dependency>
17+
<groupId>org.springframework.boot</groupId>
18+
<artifactId>spring-boot-starter-actuator</artifactId>
19+
</dependency>
20+
```
21+
22+
2. 导包完成后,立即使得路径生效,但是不是所有的端点都是默认暴露的(仅停止服务器的端点未配置,一般不建议配置),默认仅暴露2个端点,共20+个端点
23+
24+
默认暴露:`http://localhost:8080/community/actuator/health` 返回up则健康
25+
26+
`http://localhost:8080/community/actuator/info` 默认为`[]`
27+
28+
3. 配置
29+
30+
在application.properties中:
31+
32+
```
33+
# actuator
34+
management.endpoints.web.exposure.include=* # 启动端点
35+
management.endpoints.web.exposure.exclude=info,caches # 禁用端点
36+
```
37+
38+
4. 自制端点——监控数据库连接是否正常
39+
40+
a. 新建软件包actuator
41+
42+
b. 新建DataBaseEndPoint
43+
44+
```java
45+
package com.nowcoder.community.actuator;
46+
47+
import com.nowcoder.community.util.CommunityUtil;
48+
import org.slf4j.Logger;
49+
import org.slf4j.LoggerFactory;
50+
import org.springframework.beans.factory.annotation.Autowired;
51+
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
52+
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
53+
import org.springframework.stereotype.Component;
54+
55+
import javax.sql.DataSource;
56+
57+
@Component
58+
@Endpoint(id = "database")
59+
public class DataBaseEndPoint {
60+
61+
private final Logger logger = LoggerFactory.getLogger(DataBaseEndPoint.class);
62+
63+
// 连接池顶层接口
64+
@Autowired
65+
private DataSource dataSource;
66+
67+
// 尝试访问链接,失败则数据库链接有问题
68+
// @ReadOperation代表GET请求访问该端点时,会调用该方法
69+
@ReadOperation
70+
public String checkConnection() {
71+
try (
72+
var ignored = dataSource.getConnection();
73+
) {
74+
return CommunityUtil.getJSONString(0, "获取连接成功!");
75+
} catch (Exception e) {
76+
logger.error("获取连接失败: " + e.getMessage());
77+
return CommunityUtil.getJSONString(1, "获取连接失败!");
78+
}
79+
}
80+
}
81+
```
82+
83+
c. 权限配置
84+
85+
```java
86+
.requestMatchers(
87+
"/discuss/delete",
88+
"/data/**",
89+
"/user/updatetype",
90+
"/actuator/**"
91+
).hasAnyAuthority(
92+
AUTHORITY_ADMIN
93+
)
94+
```

pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@
130130
<groupId>com.github.ben-manes.caffeine</groupId>
131131
<artifactId>caffeine</artifactId>
132132
</dependency>
133+
<dependency>
134+
<groupId>org.springframework.boot</groupId>
135+
<artifactId>spring-boot-starter-actuator</artifactId>
136+
</dependency>
133137
</dependencies>
134138

135139
<build>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.nowcoder.community.actuator;
2+
3+
import com.nowcoder.community.util.CommunityUtil;
4+
import org.slf4j.Logger;
5+
import org.slf4j.LoggerFactory;
6+
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
8+
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
9+
import org.springframework.stereotype.Component;
10+
11+
import javax.sql.DataSource;
12+
13+
@Component
14+
@Endpoint(id = "database")
15+
public class DataBaseEndPoint {
16+
17+
private final Logger logger = LoggerFactory.getLogger(DataBaseEndPoint.class);
18+
19+
// 连接池顶层接口
20+
@Autowired
21+
private DataSource dataSource;
22+
23+
// 尝试访问链接,失败则数据库链接有问题
24+
// @ReadOperation代表GET请求访问该端点时,会调用该方法
25+
@ReadOperation
26+
public String checkConnection() {
27+
try (
28+
var ignored = dataSource.getConnection();
29+
) {
30+
return CommunityUtil.getJSONString(0, "获取连接成功!");
31+
} catch (Exception e) {
32+
logger.error("获取连接失败: " + e.getMessage());
33+
return CommunityUtil.getJSONString(1, "获取连接失败!");
34+
}
35+
}
36+
}

src/main/java/com/nowcoder/community/config/SecurityConfig.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
5757
.requestMatchers(
5858
"/discuss/delete",
5959
"/data/**",
60-
"/user/updatetype"
60+
"/user/updatetype",
61+
"/actuator/**"
6162
).hasAnyAuthority(
6263
AUTHORITY_ADMIN
6364
)

src/main/resources/application.properties

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,8 @@ qiniu.bucket.share.url = http://sdz6flgi0.hd-bkt.clouddn.com
8181

8282
# caffeine
8383
caffeine.posts.max-size=15
84-
caffeine.posts.expire-seconds=180
84+
caffeine.posts.expire-seconds=180
85+
86+
# actuator
87+
management.endpoints.web.exposure.include=*
88+
management.endpoints.web.exposure.exclude=info,caches

src/main/resources/templates/error/404.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<div class="main">
1919
<div class="container pl-5 pr-5 pt-3 pb-3 mt-3 mb-3" style="text-align: center">
2020
<img th:src="@{/img/404.png}">
21+
<p class="text-center mt-3">呃,访问该资源失败!</p>
2122
</div>
2223
</div>
2324

src/main/resources/templates/error/500.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<div class="main">
1919
<div class="container pl-5 pr-5 pt-3 pb-3 mt-3 mb-3" style="text-align: center">
2020
<img th:src="@{/img/error.png}" >
21+
<p class="text-center mt-3">呃,服务器发生异常!</p>
2122
</div>
2223
</div>
2324

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.nowcoder.community;
2+
3+
import com.nowcoder.community.entity.DiscussPost;
4+
import com.nowcoder.community.service.DiscussPostService;
5+
import org.junit.*;
6+
import org.junit.jupiter.api.Assertions;
7+
import org.junit.runner.RunWith;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.boot.test.context.SpringBootTest;
10+
import org.springframework.test.context.ContextConfiguration;
11+
import org.springframework.test.context.junit4.SpringRunner;
12+
13+
import java.util.Date;
14+
15+
@RunWith(SpringRunner.class)
16+
@SpringBootTest
17+
@ContextConfiguration(classes = CommunityApplication.class)
18+
public class SpringBootTests {
19+
20+
@Autowired
21+
private DiscussPostService discussPostService;
22+
23+
private DiscussPost data;
24+
25+
@BeforeClass
26+
public static void beforeClass(){
27+
System.out.println("before class");
28+
}
29+
30+
@AfterClass
31+
public static void afterClass(){
32+
System.out.println("after class");
33+
}
34+
35+
@Before
36+
public void before(){
37+
System.out.println("before");
38+
39+
// 初始化测试数据
40+
data = new DiscussPost();
41+
data.setUserId(111);
42+
data.setTitle("Test");
43+
data.setContent("Test content");
44+
data.setCreateTime(new Date());
45+
discussPostService.addDiscussPost(data);
46+
}
47+
48+
@After
49+
public void after(){
50+
System.out.println("after");
51+
52+
// 删除测试数据
53+
discussPostService.updateStatus(data.getId(), 2);
54+
}
55+
56+
@Test
57+
public void test1(){
58+
System.out.println("test1");
59+
}
60+
61+
@Test
62+
public void test2(){
63+
System.out.println("test2");
64+
}
65+
66+
@Test
67+
public void testFindById() {
68+
DiscussPost post = discussPostService.findDiscussPostById(data.getId()); // data是期望,post是实际
69+
// 通过断言判断是否符合预期 断言:成立什么都不发生,不成立抛异常
70+
Assertions.assertNotNull(post); // 判断查询结果是否非空,抛异常表示帖子为空
71+
Assertions.assertEquals(data.getTitle(), post.getTitle());
72+
Assertions.assertEquals(data.getContent(), post.getContent());
73+
}
74+
75+
@Test
76+
public void testUpdateScore() {
77+
int rows = discussPostService.updateScore(data.getId(), 2000.00);
78+
Assertions.assertEquals(1, rows);
79+
80+
DiscussPost post = discussPostService.findDiscussPostById(data.getId());
81+
// 小数比较时,需要指定一个误差范围:2000 +- 2
82+
Assertions.assertEquals(2000.00, post.getScore(), 2);
83+
}
84+
}

0 commit comments

Comments
 (0)