关于springboot:SpringBoot事务对TestRestTemplate的影响

37次阅读

共计 9079 个字符,预计需要花费 23 分钟才能阅读完成。

前言

在本周进行后盾用户登录单元测试的过程中,因为与之前的构造产生了较大的变动,因而呈现了很多的问题,在泛滥问题中,因为事务呈现的问题堪称是涉及到常识盲区,甚至老师说了问题出在事务上,本人也还是处于很懵的状态,因而特意理解了一下事务对该测试的影响,力求下次再呈现问题时,能晓得问题的基本。

问题

在本周对后盾登录办法进行单元测试的过程中,呈现了断言谬误:

ERROR:

测试方法:

@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);
    }

测试原理:

一时间竟不知从何下手,而后只能对办法打断点进行 DEBUG 测试:
因为在执行登录办法时调用了 loadUserByUsername() 办法,因而对该办法进行 DEBUG 测试:

后果:

在测试方法中执行 findByUsername() 办法,进行 DEBUG 测试:

后果:

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

解决

因为之前的测试类继承于 ControllerTest,而 ControllerTest 类上有 @Transactional 注解:

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

为什么

为什么事务会对测试产生影响呢, 上面咱们来剖析一下,对于事务的概念与个性,上篇文章《@Transactional 事务注解》曾经介绍的很分明了,本次咱们来深刻一下:


咱们别离在有事务和没有事务的状况下进行单元测试,并察看数据库的数据变动:
有事务:

没有事务:

原本 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 测试仅应用一个线程就能够实现测试:

然而 TestTemplate 在测试时,须要启动两个线程,一个为 SpringBootTest,另一个为 SpringBoot 利用,测试机制如下:

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

仿真测试

首先开启 TestTemplate 线程的事务:

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

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

而后咱们在以后事务执行查问操作:

SELECT * FROM role;

RESULT:

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

而后执行查问操作:

SELECT * FROM role;

RESULT:

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

COMMIT

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

代码如下:

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

总结

之前认为控制台的信息是没有作用的,也就没有去关注过,然而通过此次呈现问题发现控制台的信息非常重要,想想本人应用了一年多 IDEA,却没有理解控制台信息,想来也是非常羞愧,本次也对事务有了新的理解,置信当前会更深刻吧。
纸上得来终觉浅,绝知此事要躬行。

正文完
 0