在本週進行後臺用戶登陸單元測試的過程當中,因爲與以前的結構產生了較大的變化,所以出現了不少的問題,在衆多問題中,因爲事務出現的問題可謂是觸及到知識盲區,甚至老師說了問題出在事務上,本身也仍是處於很懵的狀態,所以特地瞭解了一下事務對該測試的影響,力求下次再出現問題時,能知道問題的根本。html
在本週對後臺登陸方法進行單元測試的過程當中,出現了斷言錯誤:java
ERROR:web
測試方法:spring
@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); }
測試原理:json
一時間竟不知從何下手,而後只能對方法打斷點進行DEBUG測試:
因爲在執行登陸方法時調用了loadUserByUsername()方法, 所以對該方法進行DEBUG測試:segmentfault
結果:api
在測試方法中執行findByUsername()方法, 進行DEBUG測試:cookie
結果:app
newUser存在, 說明findByUsername()方法沒有問題, 一時半會竟沒有思路,只好求助於潘老師, 最後老師給出的結論是:
因爲以前的測試類繼承於ControllerTest,而ControllerTest類上有@Transactional註解:
在刪除了對ControllerTest的繼承後,問題得以解決:
爲何事務會對測試產生影響呢, 下面咱們來分析一下, 關於事務的概念與特性,上篇文章《@Transactional 事務註解》已經介紹的很清楚了,本次咱們來深刻一下:
咱們分別在有事務和沒有事務的狀況下進行單元測試, 並觀察數據庫的數據變化:
有事務:
沒有事務:
原本Blog寫到這裏就中止了,由於發現本身仍是沒理解老師的意思,只是看明白了,可是內在的機制仍是不清楚,後來彙報時老師又講了一遍, 茅塞頓開,發現本身仍是瞭解的太少了。
(因爲翻譯可能會曲解官方的意思, 所以此處直接引用官方的說明)
對於TestRestTemplate, Spring官方文檔的說明是:
Convenient alternative ofRestTemplate
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 underlyingRestTemplate
usegetRestTemplate()
.If you are using the
@SpringBootTest
annotation with an embedded server, aTestRestTemplate
is automatically available and can be@Autowired
into your test. If you need customizations (for example to adding additional message converters) use aRestTemplateBuilder
@Bean
.
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 事務處理主要有兩種方法:
一、用 BEGIN, ROLLBACK, COMMIT來實現
- BEGIN 開始一個事務
- ROLLBACK 事務回滾
- COMMIT 事務確認
二、直接用 SET 來改變 MySQL 的自動提交模式:
- SET AUTOCOMMIT=0 禁止自動提交
- SET AUTOCOMMIT=1 開啓自動提交
這樣咱們能夠了解到, 在事務不進行commit操做的狀況下, 數據庫是不會進行存取數據的操做的, 若是事務提交,相應的語句纔會執行。
在進行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, 卻沒有了解控制檯信息, 想來也是十分慚愧, 本次也對事務有了新的瞭解, 相信之後會更深刻吧。紙上得來終覺淺,絕知此事要躬行。