Skip to content

Commit 572b7e3

Browse files
authoredApr 21, 2021
Merge pull request #30 from fengcharly/master
使用PowerMockRunner和Mockito编写单元测试用例
2 parents 9053611 + 0dffa1a commit 572b7e3

File tree

2 files changed

+373
-0
lines changed

2 files changed

+373
-0
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
Spring Security的登录主要是由一系列的过滤器组成,我们如果需要修改登录的校验逻辑,只需要在过滤器链路上添加修改相关的逻辑即可。这里主要通过Spring Security的源码来了解相关的认证登录的逻辑。
2+
3+
#### 1.Spring Security的认证流程
4+
5+
主要分析:
6+
7+
1. 认证用户的流程
8+
2. 如何进行认证校验
9+
3. 认证成功后怎么获取用户信息
10+
11+
具体的过滤器链路如下所示:
12+
13+
[![cT2G4g.png](https://z3.ax1x.com/2021/04/19/cT2G4g.png)](https://imgtu.com/i/cT2G4g)
14+
15+
Spring Security的认证流程图如下,认证的主要过程有:
16+
17+
1. 用户提交用户名和密码,然后通过UsernamePasswordAuthenticationFilter对其进行封装成为UsernamePasswordAuthenticationToken对象,这个是AbstractAuthenticationToken的子类,而AbstractAuthenticationToken又是Authentication的一个实现,所以可以看到后续获取的都是Authentication类型的对象实例;
18+
2. 将第一步的UsernamePasswordAuthenticationToken对象传递给AuthenticationManager;
19+
3. 通过AbstractUserDetailsAuthenticationProvider的默认实现类DaoAuthenticationProvider的retrieveUser方法,这个方法会调用UserDetailsService的loadUserByUsername方法来进行用户名和密码的判断,使用的默认的逻辑进行处理;
20+
4. 将成功认证后的用户信息放入到SecurityContextHolder中,之后可以通过SecurityContext获取用户的相关信息。
21+
22+
[![coGpvR.png](https://z3.ax1x.com/2021/04/19/coGpvR.png)](https://imgtu.com/i/coGpvR)
23+
24+
spring-security源码下载地址:
25+
26+
```java
27+
https://github.com/spring-projects/spring-security
28+
```
29+
30+
#### 2.Spring Security的认证源码分析
31+
32+
##### 2.1 搭建项目并访问
33+
34+
首先我们搭建一个Spring Security的项目,使用Spring Boot可以很方便的进行集成开发,主要引入如下的依赖即可(当然也可以查看官网,选择合适的版本):
35+
36+
```java
37+
<dependency>
38+
<groupId>org.springframework.boot</groupId>
39+
<artifactId>spring-boot-starter-security</artifactId>
40+
</dependency>
41+
```
42+
43+
启动项目后会随机生成一个密码串,这里需要复制保存以便登录的时候使用:
44+
45+
[![coJ0ld.png](https://z3.ax1x.com/2021/04/19/coJ0ld.png)](https://imgtu.com/i/coJ0ld)
46+
47+
访问登录地址:
48+
49+
```java
50+
http://localhost:8080/login
51+
```
52+
53+
[![coJfpQ.png](https://z3.ax1x.com/2021/04/19/coJfpQ.png)](https://imgtu.com/i/coJfpQ)
54+
55+
默认的账户名和密码:
56+
57+
```java
58+
账户名: user
59+
密码: 项目启动时生成的密码串
60+
```
61+
62+
##### 2.2 进行源码分析
63+
64+
1. 进行断点后会发现首先进入的是UsernamePasswordAuthenticationFilter的attemptAuthentication(HttpServletRequest request, HttpServletResponse response)方法,会对用户名和密码进行封装成UsernamePasswordAuthenticationToken对象,然后调用this.getAuthenticationManager().authenticate(authRequest)方法进入到AuthenticationManager中。
65+
66+
attemptAuthentication方法源码如下所示:
67+
68+
```java
69+
@Override
70+
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
71+
throws AuthenticationException {
72+
if (this.postOnly && !request.getMethod().equals("POST")) {
73+
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
74+
}
75+
String username = obtainUsername(request);
76+
username = (username != null) ? username : "";
77+
username = username.trim();
78+
String password = obtainPassword(request);
79+
password = (password != null) ? password : "";
80+
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
81+
// Allow subclasses to set the "details" property
82+
setDetails(request, authRequest);
83+
return this.getAuthenticationManager().authenticate(authRequest);
84+
}
85+
```
86+
87+
2. 随后请求进入到WebSecurityConfigurerAdapter的AuthenticationManagerDelegator中,AuthenticationManagerDelegator是AuthenticationManager的一个子类,最后封装成为UsernamePasswordAuthenticationToken对象,供DaoAuthenticationProvider使用。
88+
89+
AuthenticationManagerDelegator的源码如下:
90+
91+
```java
92+
static final class AuthenticationManagerDelegator implements AuthenticationManager {
93+
private AuthenticationManagerBuilder delegateBuilder;
94+
private AuthenticationManager delegate;
95+
private final Object delegateMonitor = new Object();
96+
private Set<String> beanNames;
97+
98+
AuthenticationManagerDelegator(AuthenticationManagerBuilder delegateBuilder, ApplicationContext context) {
99+
Assert.notNull(delegateBuilder, "delegateBuilder cannot be null");
100+
Field parentAuthMgrField = ReflectionUtils.findField(AuthenticationManagerBuilder.class, "parentAuthenticationManager");
101+
ReflectionUtils.makeAccessible(parentAuthMgrField);
102+
this.beanNames = getAuthenticationManagerBeanNames(context);
103+
validateBeanCycle(ReflectionUtils.getField(parentAuthMgrField, delegateBuilder), this.beanNames);
104+
this.delegateBuilder = delegateBuilder;
105+
}
106+
107+
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
108+
if (this.delegate != null) {
109+
return this.delegate.authenticate(authentication);
110+
} else {
111+
synchronized(this.delegateMonitor) {
112+
if (this.delegate == null) {
113+
this.delegate = (AuthenticationManager)this.delegateBuilder.getObject();
114+
this.delegateBuilder = null;
115+
}
116+
}
117+
118+
return this.delegate.authenticate(authentication);
119+
}
120+
}
121+
122+
private static Set<String> getAuthenticationManagerBeanNames(ApplicationContext applicationContext) {
123+
String[] beanNamesForType = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, AuthenticationManager.class);
124+
return new HashSet(Arrays.asList(beanNamesForType));
125+
}
126+
127+
private static void validateBeanCycle(Object auth, Set<String> beanNames) {
128+
if (auth != null && !beanNames.isEmpty() && auth instanceof Advised) {
129+
TargetSource targetSource = ((Advised)auth).getTargetSource();
130+
if (targetSource instanceof LazyInitTargetSource) {
131+
LazyInitTargetSource lits = (LazyInitTargetSource)targetSource;
132+
if (beanNames.contains(lits.getTargetBeanName())) {
133+
throw new FatalBeanException("A dependency cycle was detected when trying to resolve the AuthenticationManager. Please ensure you have configured authentication.");
134+
}
135+
}
136+
}
137+
}
138+
}
139+
```
140+
141+
org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration.AuthenticationManagerDelegator#authenticate
142+
143+
```java
144+
@Override
145+
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
146+
if (this.delegate != null) {
147+
return this.delegate.authenticate(authentication);
148+
}
149+
synchronized (this.delegateMonitor) {
150+
if (this.delegate == null) {
151+
this.delegate = this.delegateBuilder.getObject();
152+
this.delegateBuilder = null;
153+
}
154+
}
155+
return this.delegate.authenticate(authentication);
156+
}
157+
```
158+
159+
3. 进入到DaoAuthenticationProvider的retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)方法进行用户的认证,这里的认证主要会调用默认的UserDetailsService对用户名和密码进行校验,如果是使用的类似于Mysql的数据源,其默认的实现是JdbcDaoImpl。
160+
161+
org.springframework.security.authentication.dao.DaoAuthenticationProvider#retrieveUser
162+
163+
```java
164+
@Override
165+
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
166+
throws AuthenticationException {
167+
prepareTimingAttackProtection();
168+
try {
169+
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
170+
if (loadedUser == null) {
171+
throw new InternalAuthenticationServiceException(
172+
"UserDetailsService returned null, which is an interface contract violation");
173+
}
174+
return loadedUser;
175+
}
176+
catch (UsernameNotFoundException ex) {
177+
mitigateAgainstTimingAttack(authentication);
178+
throw ex;
179+
}
180+
catch (InternalAuthenticationServiceException ex) {
181+
throw ex;
182+
}
183+
catch (Exception ex) {
184+
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
185+
}
186+
}
187+
188+
```
189+
190+
4. 将上一步认证后的用户实例放入SecurityContextHolder中,至此我们可以很方便的从SecurityContextHolder中获取用户信息,方法如下:
191+
192+
```java
193+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
194+
```
195+
196+
+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
单元测试可以提高测试开发的效率,减少代码错误率,提高代码健壮性,提高代码质量。在Spring框架中常用的两种测试框架:PowerMockRunner和SpringRunner两个单元测试,鉴于SpringRunner启动的一系列依赖和数据连接的问题,推荐使用PowerMockRunner,这样能有效的提高测试的效率,并且其提供的API能覆盖的场景广泛,使用方便,可谓是Java单元测试之模拟利器。
2+
3+
#### 1. PowerMock是什么?
4+
5+
PowerMock是一个Java模拟框架,可用于解决通常认为很难甚至无法测试的测试问题。使用PowerMock,可以模拟静态方法,删除静态初始化程序,允许模拟而不依赖于注入,等等。PowerMock通过在执行测试时在运行时修改字节码来完成这些技巧。PowerMock还包含一些实用程序,可让您更轻松地访问对象的内部状态。
6+
7+
8+
举个例子,你在使用Junit进行单元测试时,并不想让测试数据进入数据库,怎么办?这个时候就可以使用PowerMock,拦截数据库操作,并模拟返回参数。
9+
10+
#### 2. PowerMock包引入
11+
12+
```xml
13+
<!-- 单元测试 依赖-->
14+
<dependency>
15+
<groupId>org.powermock</groupId>
16+
<artifactId>powermock-core</artifactId>
17+
<version>2.0.2</version>
18+
<scope>test</scope>
19+
</dependency>
20+
<dependency>
21+
<groupId>org.mockito</groupId>
22+
<artifactId>mockito-core</artifactId>
23+
<version>2.23.0</version>
24+
</dependency>
25+
<dependency>
26+
<groupId>org.powermock</groupId>
27+
<artifactId>powermock-module-junit4</artifactId>
28+
<version>2.0.4</version>
29+
<scope>test</scope>
30+
</dependency>
31+
<dependency>
32+
<groupId>org.powermock</groupId>
33+
<artifactId>powermock-api-mockito2</artifactId>
34+
<version>2.0.2</version>
35+
<scope>test</scope>
36+
</dependency>
37+
<dependency>
38+
<groupId>com.github.jsonzou</groupId>
39+
<artifactId>jmockdata</artifactId>
40+
<version>4.3.0</version>
41+
</dependency>
42+
<!-- 单元测试 依赖-->
43+
```
44+
45+
#### 3. 重要注解说明
46+
47+
```java
48+
@RunWith(PowerMockRunner.class) // 告诉JUnit使用PowerMockRunner进行测试
49+
@PrepareForTest({RandomUtil.class}) // 所有需要测试的类列在此处,适用于模拟final类或有final, private, static, native方法的类
50+
@PowerMockIgnore("javax.management.*") //为了解决使用powermock后,提示classloader错误
51+
```
52+
53+
54+
55+
#### 4. 使用示例
56+
57+
##### 4.1 模拟接口返回
58+
59+
首先对接口进行mock,然后录制相关行为
60+
61+
```java
62+
InterfaceToMock mock = Powermockito.mock(InterfaceToMock.class)
63+
64+
Powermockito.when(mock.method(Params…)).thenReturn(value)
65+
66+
Powermockito.when(mock.method(Params..)).thenThrow(Exception)
67+
```
68+
69+
##### 4.2 设置对象的private属性
70+
71+
需要使用whitebox向class或者对象中赋值。
72+
73+
如我们已经对接口尽心了mock,现在需要将此mock加入到对象中,可以采用如下方法:
74+
75+
```java
76+
Whitebox.setInternalState(Object object, String fieldname, Object… value);
77+
```
78+
79+
其中object为需要设置属性的静态类或对象。
80+
81+
##### 4.3 模拟构造函数
82+
83+
对于模拟构造函数,也即当出现new InstanceClass()时可以将此构造函数拦截并替换结果为我们需要的mock对象。
84+
85+
注意:使用时需要加入标记:
86+
87+
```java
88+
@RunWith(PowerMockRunner.class)
89+
90+
@PrepareForTest({ InstanceClass.class })
91+
92+
@PowerMockIgnore("javax.management.\*")
93+
94+
Powermockito.whenNew(InstanceClass.class).thenReturn(Object value)
95+
```
96+
97+
##### 4.4 模拟静态方法
98+
99+
模拟静态方法类似于模拟构造函数,也需要加入注释标记。
100+
101+
```java
102+
@RunWith(PowerMockRunner.class)
103+
104+
@PrepareForTest({ StaticClassToMock.class })
105+
106+
@PowerMockIgnore("javax.management.\*")
107+
108+
Powermockito.mockStatic(StaticClassToMock.class);
109+
110+
Powermockito.when(StaticClassToMock.method(Object.. params)).thenReturn(Object value)
111+
```
112+
113+
##### 4.5 模拟final方法
114+
115+
Final方法的模拟类似于模拟静态方法。
116+
117+
```java
118+
@RunWith(PowerMockRunner.class)
119+
120+
@PrepareForTest({ FinalClassToMock.class })
121+
122+
@PowerMockIgnore("javax.management.\*")
123+
124+
Powermockito.mockStatic(FinalClassToMock.class);
125+
126+
Powermockito.when(StaticClassToMock.method(Object.. params)).thenReturn(Object value)
127+
```
128+
129+
##### 4.6 模拟静态类
130+
131+
模拟静态类类似于模拟静态方法。
132+
133+
##### 4.7 使用spy方法避免执行被测类中的成员函数
134+
135+
如被测试类为:TargetClass,想要屏蔽的方法为targetMethod.
136+
137+
```java
138+
1PowerMockito.spy(TargetClass.class);
139+
140+
2Powemockito.when(TargetClass.targetMethod()).doReturn()
141+
142+
3) 注意加入
143+
144+
@RunWith(PowerMockRunner.class)
145+
146+
@PrepareForTest(DisplayMoRelationBuilder.class)
147+
148+
@PowerMockIgnore("javax.management.*")
149+
```
150+
151+
##### 4.8 参数匹配器
152+
153+
有时我们在处理doMethod(Param param)时,不想进行精确匹配,这时可以使用Mockito提供的模糊匹配方式。
154+
155+
如:Mockito.anyInt(),Mockito.anyString()
156+
157+
##### 4.9 处理public void型的静态方法
158+
159+
```java
160+
Powermockito.doNothing.when(T class2mock, String method, <T>… params>
161+
```
162+
163+
#### 5. 单元测试用例可选清单
164+
165+
输入数据验证:这些检查通常可以对输入到应用程序系统中的数据采用。
166+
167+
- 必传项测试
168+
- 唯一字段值测试
169+
- 空值测试
170+
- 字段只接受允许的字符
171+
- 负值测试
172+
- 字段限于字段长度规范
173+
- 不可能的值
174+
- 垃圾值测试
175+
- 检查字段之间的依赖性
176+
- 等效类划分和边界条件测试
177+
- 错误和异常处理测试

0 commit comments

Comments
 (0)
Please sign in to comment.