试题管理系统的安全模块使用 Spring Security
,代码从原华软仓库移植,在移植的过程中,发现原测试编写的不好,遂在新系统中对安全模块测试进行了重构。
添加 @SpringBootTest
注解,意为这是一个基于 SpringBoot
的单元测试。
SpringBoot
在官方的 Guide
中提供了多种测试方式。
@SpringBootTest
注解中的 webEnvironment
属性可以配置测试环境,默认为 MOCK
环境。
/** * The type of web environment to create when applicable. Defaults to * {@link WebEnvironment#MOCK}. * @return the type of web environment */ WebEnvironment webEnvironment() default WebEnvironment.MOCK;
启用 Spring Security
后,单元测试中对 api
的测试会被 Spring Security
的 Filter
进行拦截,所以测试之前需要进行用户登录操作。
之前都是使用比较笨重的方法,写一个 @Before
, @Before
里进行登录,之后再执行测试方法。
最近在阅读 Spring Security Test
文档之后,终于找到一种模拟登录十分简便的方法, @WithMockUser
。
test method with mock user - spring security test
引入 Spring Security Test
依赖:
<!-- Spring Security Test --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency>
示例代码如下:
@SpringBootTest @RunWith(SpringRunner.class) @AutoConfigureMockMvc @WithMockUser(username = "admin", password = "admin") public class ControllerTest { @Autowired protected MockMvc mockMvc; @Test void contextLoads() { } }
注: @RunWith(SpringRunner.class)
表示当前测试使用 org.springframework.test.context.junit4.SpringRunner
类来执行,最新的 SpringBoot
版本中已经全面启用 junit5
,不推荐使用 junit4.SpringRunner
,因为未经过内部学习与测试,未在生产项目中使用。
为了减少学习与沟通的成本,之前,所有的测试规定都在 MOCK
环境下,使用 MockMVC
进行 api
测试。
虽然 MOCK
环境能解决大部分的问题,并且可以在不启动 Server
的情况下直接进行测试,但在某些场景下,仍需要真实环境下的 HTTP
服务与请求测试。
启用 Spring Security
后, MockMVC
是直接测试控制器,并非在真实的 HTTP
服务器下进行测试, MOCK
环境中使用的是 MockHttpSession
,这不是标准的 Session
实现,没有加入对 COOKIE
的支持,所以在测试安全模块时,无法像浏览器一样测试 COOKIE
等认证信息。
spring mockmvc doesn't contain cookies - stackoverflow
去 StackOverflow
上也没有解决方案,答案推荐使用 TestRestTemplate
+真实的服务器环境进行单元测试。
将 webEnvironment
配置为 SpringBootTest.WebEnvironment.RANDOM_PORT
,即表示当前测试在一个随机端口的真实 Web
环境下运行。
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class AuthControllerTest { @Autowired private TestRestTemplate restTemplate; }
测试时使用 TestRestTemplate
进行网络请求的发送,真实模拟 Web
服务器环境。
示例代码如下:
logger.debug("3: 测试用户名密码正确"); username = RIGHT_USERNAME; password = RIGHT_PASSWORD; response = this.restTemplate .withBasicAuth(username, password) .getForEntity(CONFIG_LOGIN, Void.class); logger.debug("断言: 状态码为200"); assertThat(response.getStatusCode().value()).isEqualTo(HttpStatus.OK.value()); logger.debug("获取 response 的 Set-Cookie 信息,并断言"); String setCookie = response.getHeaders().getFirst(HttpHeaders.SET_COOKIE); assertThat(setCookie).isNotNull();
两个各有优点,之前我们一直使用简单方便的 Mock
环境进行测试,但当我们有一天,发现这个 Mock
环境测试下的 MockHttpSession
无法满足需求的时候,我们开始探索其他的测试方案。
真正的掌握,不是知道什么时候用,而是知道什么时候不用。