Skip to content

使用PowerMockRunner和Mockito编写单元测试用例 #30

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions docs/advanced/spring-security-login-authentication-process.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
Spring Security的登录主要是由一系列的过滤器组成,我们如果需要修改登录的校验逻辑,只需要在过滤器链路上添加修改相关的逻辑即可。这里主要通过Spring Security的源码来了解相关的认证登录的逻辑。

#### 1.Spring Security的认证流程

主要分析:

1. 认证用户的流程
2. 如何进行认证校验
3. 认证成功后怎么获取用户信息

具体的过滤器链路如下所示:

[![cT2G4g.png](https://z3.ax1x.com/2021/04/19/cT2G4g.png)](https://imgtu.com/i/cT2G4g)

Spring Security的认证流程图如下,认证的主要过程有:

1. 用户提交用户名和密码,然后通过UsernamePasswordAuthenticationFilter对其进行封装成为UsernamePasswordAuthenticationToken对象,这个是AbstractAuthenticationToken的子类,而AbstractAuthenticationToken又是Authentication的一个实现,所以可以看到后续获取的都是Authentication类型的对象实例;
2. 将第一步的UsernamePasswordAuthenticationToken对象传递给AuthenticationManager;
3. 通过AbstractUserDetailsAuthenticationProvider的默认实现类DaoAuthenticationProvider的retrieveUser方法,这个方法会调用UserDetailsService的loadUserByUsername方法来进行用户名和密码的判断,使用的默认的逻辑进行处理;
4. 将成功认证后的用户信息放入到SecurityContextHolder中,之后可以通过SecurityContext获取用户的相关信息。

[![coGpvR.png](https://z3.ax1x.com/2021/04/19/coGpvR.png)](https://imgtu.com/i/coGpvR)

spring-security源码下载地址:

```java
https://github.com/spring-projects/spring-security
```

#### 2.Spring Security的认证源码分析

##### 2.1 搭建项目并访问

首先我们搭建一个Spring Security的项目,使用Spring Boot可以很方便的进行集成开发,主要引入如下的依赖即可(当然也可以查看官网,选择合适的版本):

```java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
```

启动项目后会随机生成一个密码串,这里需要复制保存以便登录的时候使用:

[![coJ0ld.png](https://z3.ax1x.com/2021/04/19/coJ0ld.png)](https://imgtu.com/i/coJ0ld)

访问登录地址:

```java
http://localhost:8080/login
```

[![coJfpQ.png](https://z3.ax1x.com/2021/04/19/coJfpQ.png)](https://imgtu.com/i/coJfpQ)

默认的账户名和密码:

```java
账户名: user
密码: 项目启动时生成的密码串
```

##### 2.2 进行源码分析

1. 进行断点后会发现首先进入的是UsernamePasswordAuthenticationFilter的attemptAuthentication(HttpServletRequest request, HttpServletResponse response)方法,会对用户名和密码进行封装成UsernamePasswordAuthenticationToken对象,然后调用this.getAuthenticationManager().authenticate(authRequest)方法进入到AuthenticationManager中。

attemptAuthentication方法源码如下所示:

```java
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
```

2. 随后请求进入到WebSecurityConfigurerAdapter的AuthenticationManagerDelegator中,AuthenticationManagerDelegator是AuthenticationManager的一个子类,最后封装成为UsernamePasswordAuthenticationToken对象,供DaoAuthenticationProvider使用。

AuthenticationManagerDelegator的源码如下:

```java
static final class AuthenticationManagerDelegator implements AuthenticationManager {
private AuthenticationManagerBuilder delegateBuilder;
private AuthenticationManager delegate;
private final Object delegateMonitor = new Object();
private Set<String> beanNames;

AuthenticationManagerDelegator(AuthenticationManagerBuilder delegateBuilder, ApplicationContext context) {
Assert.notNull(delegateBuilder, "delegateBuilder cannot be null");
Field parentAuthMgrField = ReflectionUtils.findField(AuthenticationManagerBuilder.class, "parentAuthenticationManager");
ReflectionUtils.makeAccessible(parentAuthMgrField);
this.beanNames = getAuthenticationManagerBeanNames(context);
validateBeanCycle(ReflectionUtils.getField(parentAuthMgrField, delegateBuilder), this.beanNames);
this.delegateBuilder = delegateBuilder;
}

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (this.delegate != null) {
return this.delegate.authenticate(authentication);
} else {
synchronized(this.delegateMonitor) {
if (this.delegate == null) {
this.delegate = (AuthenticationManager)this.delegateBuilder.getObject();
this.delegateBuilder = null;
}
}

return this.delegate.authenticate(authentication);
}
}

private static Set<String> getAuthenticationManagerBeanNames(ApplicationContext applicationContext) {
String[] beanNamesForType = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, AuthenticationManager.class);
return new HashSet(Arrays.asList(beanNamesForType));
}

private static void validateBeanCycle(Object auth, Set<String> beanNames) {
if (auth != null && !beanNames.isEmpty() && auth instanceof Advised) {
TargetSource targetSource = ((Advised)auth).getTargetSource();
if (targetSource instanceof LazyInitTargetSource) {
LazyInitTargetSource lits = (LazyInitTargetSource)targetSource;
if (beanNames.contains(lits.getTargetBeanName())) {
throw new FatalBeanException("A dependency cycle was detected when trying to resolve the AuthenticationManager. Please ensure you have configured authentication.");
}
}
}
}
}
```

org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration.AuthenticationManagerDelegator#authenticate

```java
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (this.delegate != null) {
return this.delegate.authenticate(authentication);
}
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
this.delegate = this.delegateBuilder.getObject();
this.delegateBuilder = null;
}
}
return this.delegate.authenticate(authentication);
}
```

3. 进入到DaoAuthenticationProvider的retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)方法进行用户的认证,这里的认证主要会调用默认的UserDetailsService对用户名和密码进行校验,如果是使用的类似于Mysql的数据源,其默认的实现是JdbcDaoImpl。

org.springframework.security.authentication.dao.DaoAuthenticationProvider#retrieveUser

```java
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}

```

4. 将上一步认证后的用户实例放入SecurityContextHolder中,至此我们可以很方便的从SecurityContextHolder中获取用户信息,方法如下:

```java
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
```


177 changes: 177 additions & 0 deletions docs/basis/PowerMockRunnerAndMockito.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
单元测试可以提高测试开发的效率,减少代码错误率,提高代码健壮性,提高代码质量。在Spring框架中常用的两种测试框架:PowerMockRunner和SpringRunner两个单元测试,鉴于SpringRunner启动的一系列依赖和数据连接的问题,推荐使用PowerMockRunner,这样能有效的提高测试的效率,并且其提供的API能覆盖的场景广泛,使用方便,可谓是Java单元测试之模拟利器。

#### 1. PowerMock是什么?

PowerMock是一个Java模拟框架,可用于解决通常认为很难甚至无法测试的测试问题。使用PowerMock,可以模拟静态方法,删除静态初始化程序,允许模拟而不依赖于注入,等等。PowerMock通过在执行测试时在运行时修改字节码来完成这些技巧。PowerMock还包含一些实用程序,可让您更轻松地访问对象的内部状态。


举个例子,你在使用Junit进行单元测试时,并不想让测试数据进入数据库,怎么办?这个时候就可以使用PowerMock,拦截数据库操作,并模拟返回参数。

#### 2. PowerMock包引入

```xml
<!-- 单元测试 依赖-->
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-core</artifactId>
<version>2.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.0</version>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.jsonzou</groupId>
<artifactId>jmockdata</artifactId>
<version>4.3.0</version>
</dependency>
<!-- 单元测试 依赖-->
```

#### 3. 重要注解说明

```java
@RunWith(PowerMockRunner.class) // 告诉JUnit使用PowerMockRunner进行测试
@PrepareForTest({RandomUtil.class}) // 所有需要测试的类列在此处,适用于模拟final类或有final, private, static, native方法的类
@PowerMockIgnore("javax.management.*") //为了解决使用powermock后,提示classloader错误
```



#### 4. 使用示例

##### 4.1 模拟接口返回

首先对接口进行mock,然后录制相关行为

```java
InterfaceToMock mock = Powermockito.mock(InterfaceToMock.class)

Powermockito.when(mock.method(Params…)).thenReturn(value)

Powermockito.when(mock.method(Params..)).thenThrow(Exception)
```

##### 4.2 设置对象的private属性

需要使用whitebox向class或者对象中赋值。

如我们已经对接口尽心了mock,现在需要将此mock加入到对象中,可以采用如下方法:

```java
Whitebox.setInternalState(Object object, String fieldname, Object… value);
```

其中object为需要设置属性的静态类或对象。

##### 4.3 模拟构造函数

对于模拟构造函数,也即当出现new InstanceClass()时可以将此构造函数拦截并替换结果为我们需要的mock对象。

注意:使用时需要加入标记:

```java
@RunWith(PowerMockRunner.class)

@PrepareForTest({ InstanceClass.class })

@PowerMockIgnore("javax.management.\*")

Powermockito.whenNew(InstanceClass.class).thenReturn(Object value)
```

##### 4.4 模拟静态方法

模拟静态方法类似于模拟构造函数,也需要加入注释标记。

```java
@RunWith(PowerMockRunner.class)

@PrepareForTest({ StaticClassToMock.class })

@PowerMockIgnore("javax.management.\*")

Powermockito.mockStatic(StaticClassToMock.class);

Powermockito.when(StaticClassToMock.method(Object.. params)).thenReturn(Object value)
```

##### 4.5 模拟final方法

Final方法的模拟类似于模拟静态方法。

```java
@RunWith(PowerMockRunner.class)

@PrepareForTest({ FinalClassToMock.class })

@PowerMockIgnore("javax.management.\*")

Powermockito.mockStatic(FinalClassToMock.class);

Powermockito.when(StaticClassToMock.method(Object.. params)).thenReturn(Object value)
```

##### 4.6 模拟静态类

模拟静态类类似于模拟静态方法。

##### 4.7 使用spy方法避免执行被测类中的成员函数

如被测试类为:TargetClass,想要屏蔽的方法为targetMethod.

```java
1) PowerMockito.spy(TargetClass.class);

2) Powemockito.when(TargetClass.targetMethod()).doReturn()

3) 注意加入

@RunWith(PowerMockRunner.class)

@PrepareForTest(DisplayMoRelationBuilder.class)

@PowerMockIgnore("javax.management.*")
```

##### 4.8 参数匹配器

有时我们在处理doMethod(Param param)时,不想进行精确匹配,这时可以使用Mockito提供的模糊匹配方式。

如:Mockito.anyInt(),Mockito.anyString()

##### 4.9 处理public void型的静态方法

```java
Powermockito.doNothing.when(T class2mock, String method, <T>… params>
```

#### 5. 单元测试用例可选清单

输入数据验证:这些检查通常可以对输入到应用程序系统中的数据采用。

- 必传项测试
- 唯一字段值测试
- 空值测试
- 字段只接受允许的字符
- 负值测试
- 字段限于字段长度规范
- 不可能的值
- 垃圾值测试
- 检查字段之间的依赖性
- 等效类划分和边界条件测试
- 错误和异常处理测试