4

前言

在本周进行后台用户登录单元测试的过程中,由于与之前的结构产生了较大的变化,因此出现了很多的问题,在众多问题中,由于事务出现的问题可谓是触及到知识盲区,甚至老师说了问题出在事务上,自己也还是处于很懵的状态,因此特意了解了一下事务对该测试的影响,力求下次再出现问题时,能知道问题的根本。

问题

在本周对后台登录方法进行单元测试的过程中,出现了断言错误:

ERROR:
image.png

测试方法:

@BeforeEach
public void addCurrentLoginUser() {
        this.username = "188" + String.valueOf(CommonService.getRandomNumberLongs(10000000, 99999999));
        this.password = RandomString.make(40);
        this.user = new User();
        this.user.setUsername(this.username);
        this.user.setPassword(this.password);
        this.userRepository.save(this.user);
    }
    
 @Test
    void getCurrentLoginUser() {
        logger.debug("初始化基础数据");
        HttpHeaders headers;
        HttpEntity<Void> entity;
        ResponseEntity<Void> response;

        logger.debug("1: 测试用户名密码正确");
        headers = new HttpHeaders();
        entity = new HttpEntity<>(null, headers);
        response = this.restTemplate
                .withBasicAuth(this.username, this.password)
                .exchange(CONFIG_LOGIN, HttpMethod.GET, entity, Void.class);

        logger.debug("断言: 状态码为200");
        assertThat(response.getStatusCode().value()).isEqualTo(HttpStatus.OK.value());
}

执行方法:

@Transactional
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.debug("根据用户名查询用户");
        logger.debug(username);
        User user = this.userRepository.findByUsername(username).orElseThrow(() -> new ObjectNotFoundException("user实体未找到"));
        if (user == null) {
            logger.error("用户名不存在");
            throw new UsernameNotFoundException("用户名不存在");
        }

        logger.debug("构造用户");
        return new org.springframework.security.core.userdetails.User(username, user.getPassword(), authorities);
    }

测试原理:

image.png

一时间竟不知从何下手,然后只能对方法打断点进行DEBUG测试:
由于在执行登录方法时调用了loadUserByUsername()方法, 因此对该方法进行DEBUG测试:
image.png

结果:

image.png

在测试方法中执行findByUsername()方法, 进行DEBUG测试:
image.png

结果:

image.png

newUser存在, 说明findByUsername()方法没有问题, 一时半会竟没有思路,只好求助于潘老师, 最后老师给出的结论是:

image.png

解决

由于之前的测试类继承于ControllerTest,而ControllerTest类上有@Transactional注解:
image.png

image.png

在删除了对ControllerTest的继承后,问题得以解决:

image.png

为什么

为什么事务会对测试产生影响呢, 下面我们来分析一下, 关于事务的概念与特性,上篇文章《@Transactional 事务注解》已经介绍的很清楚了,本次我们来深入一下:

image.png
我们分别在有事务和没有事务的情况下进行单元测试, 并观察数据库的数据变化:
有事务:
image.png

没有事务:
image.png

本来Blog写到这里就停止了,因为发现自己还是没理解老师的意思,只是看明白了,但是内在的机制还是不清楚,后来汇报时老师又讲了一遍, 茅塞顿开,发现自己还是了解的太少了。

TestRestTemplate

(由于翻译可能会曲解官方的意思, 因此此处直接引用官方的说明)
对于TestRestTemplate, Spring官方文档的说明是:

Convenient alternative of RestTemplate that is suitable for integration tests. They are fault tolerant, and optionally can carry Basic authentication headers. If Apache Http Client 4.3.2 or better is available (recommended) it will be used as the client, and by default configured to ignore cookies and redirects.

Note: To prevent injection problems this class intentionally does not extend RestTemplate. If you need access to the underlying RestTemplate use getRestTemplate().

If you are using the @SpringBootTest annotation with an embedded server, a TestRestTemplate is automatically available and can be @Autowired into your test. If you need customizations (for example to adding additional message converters) use a RestTemplateBuilder @Bean.

IDEA控制台信息

2021-01-03 23:19:46.130  INFO 1301 --- [           main] o.s.t.c.transaction.TransactionContext   : Began transaction (1) for test context [DefaultTestContext@561d88ee testClass = UserControllerTest, testInstance = club.yunzhi.questionnaire.Controller.UserControllerTest@2c34402, testMethod = getUserById@UserControllerTest, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@5f883d90 testClass = UserControllerTest, locations = '{}', classes = '{class club.yunzhi.questionnaire.QuestionnaireApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[[ImportsContextCustomizer@7e58f697 key = [org.springframework.boot.test.autoconfigure.web.servlet.MockMvcAutoConfiguration, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcWebClientAutoConfiguration, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcWebDriverAutoConfiguration, org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration, org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcSecurityConfiguration]], org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@1165b38, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@3b69e7d1, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@5ac1576e, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@79079097, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@4b3fa0b3, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@710f4dc7, org.springframework.boot.test.context.SpringBootTestArgs@1], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]; transaction manager [org.springframework.orm.jpa.JpaTransactionManager@f238e4f]; rollback [true]
2021-01-03 23:19:46.370  INFO 1301 --- [           main] o.s.t.c.transaction.TransactionContext   : Rolled back transaction for test: [DefaultTestContext@561d88ee testClass = UserControllerTest, testInstance = club.yunzhi.questionnaire.Controller.UserControllerTest@2c34402, testMethod = getUserById@UserControllerTest, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@5f883d90 testClass = UserControllerTest, locations = '{}', classes = '{class club.yunzhi.questionnaire.QuestionnaireApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[[ImportsContextCustomizer@7e58f697 key = [org.springframework.boot.test.autoconfigure.web.servlet.MockMvcAutoConfiguration, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcWebClientAutoConfiguration, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcWebDriverAutoConfiguration, org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration, org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcSecurityConfiguration]], org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@1165b38, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@3b69e7d1, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@5ac1576e, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@79079097, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@4b3fa0b3, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@710f4dc7, org.springframework.boot.test.context.SpringBootTestArgs@1], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]

以上是一段IDEA在测试时控制台的信息,接触IDEA已经一年有余,但当老师问起这些信息都表示什么时,发现自己根本不了解这些信息,在老师的讲解下,发现这些信息真的挺有用的, 以上信息分别为:

年-月-日 时:分:秒.毫秒 日志级别 进程号 --- [线程号] 测试信息等

MySQL事务

MYSQL 事务处理主要有两种方法:

1、用 BEGIN, ROLLBACK, COMMIT来实现

  • BEGIN 开始一个事务
  • ROLLBACK 事务回滚
  • COMMIT 事务确认

2、直接用 SET 来改变 MySQL 的自动提交模式:

  • SET AUTOCOMMIT=0 禁止自动提交
  • SET AUTOCOMMIT=1 开启自动提交

这样我们可以了解到, 在事务不进行commit操作的情况下, 数据库是不会进行存取数据的操作的, 如果事务提交,相应的语句才会执行。

MockMVC测试及TestTemplate测试的区别

在进行MockMVC测试时, 由于使用的是Mock方法, 这使得Mock测试仅使用一个线程就可以完成测试:

image.png

但是TestTemplate在测试时, 需要启动两个线程, 一个为SpringBootTest, 另一个为SpringBoot应用, 测试机制如下:
image.png

由于线程的资源不共享, 所以在Test线程事务未提交的情况下, Application线程的事务是查不到任何数据的。

仿真测试

首先开启TestTemplate线程的事务:
image.png

然后向role表插入一条数据:

INSERT INTO role(id,deleted, `name`) VALUES (NULL,TRUE,'张三')

然后我们在当前事务执行查询操作:

SELECT * FROM role;

RESULT:
image.png

接着我们开启SpringBootApplication的线程事务:

image.png

然后执行查询操作:

SELECT * FROM role;

RESULT:
image.png

接着我们提交TestTemplate线程的事务:

COMMIT

然后再次在SpringBootApplication线程的事务中执行查询操作:

image.png

代码如下:

# TestTemplate 线程事务
BEGIN
INSERT INTO role(id,deleted, `name`) VALUES (NULL,TRUE,'张三');
SELECT * FROM role;
COMMIT
#SpringBootApplication 线程事务
BEGIN 
SELECT * FROM role

总结

之前以为控制台的信息是没有作用的, 也就没有去关注过, 但是经过此次出现问题发现控制台的信息十分重要, 想想自己使用了一年多IDEA, 却没有了解控制台信息, 想来也是十分惭愧, 本次也对事务有了新的了解, 相信以后会更深入吧。
纸上得来终觉浅,绝知此事要躬行。


锦城
854 声望21 粉丝

好好生活