test.html 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <meta name="generator" content="Asciidoctor 2.0.15">
  8. <title>Test</title>
  9. <link rel="stylesheet" href="css/site.css">
  10. <link href="css/custom.css" rel="stylesheet">
  11. <script src="js/setup.js"></script><script defer src="js/site.js"></script>
  12. </head>
  13. <body class="article toc2 toc-left"><div id="banner-container" class="container" role="banner">
  14. <div id="banner" class="contained" role="banner">
  15. <div id="switch-theme">
  16. <input type="checkbox" id="switch-theme-checkbox" />
  17. <label for="switch-theme-checkbox">Dark Theme</label>
  18. </div>
  19. </div>
  20. </div>
  21. <div id="tocbar-container" class="container" role="navigation">
  22. <div id="tocbar" class="contained" role="navigation">
  23. <button id="toggle-toc"></button>
  24. </div>
  25. </div>
  26. <div id="main-container" class="container">
  27. <div id="main" class="contained">
  28. <div id="doc" class="doc">
  29. <div id="header">
  30. <h1>Test</h1>
  31. <div id="toc" class="toc2">
  32. <div id="toctitle">Table of Contents</div>
  33. <span id="back-to-index"><a href="index.html">Back to index</a></span><ul class="sectlevel1">
  34. <li><a href="#_单元测试_vs_集成测试">1. 单元测试 vs 集成测试</a></li>
  35. <li><a href="#_快速开始">2. 快速开始</a></li>
  36. <li><a href="#_框架">3. 框架</a></li>
  37. <li><a href="#_junit">4. JUnit</a>
  38. <ul class="sectlevel2">
  39. <li><a href="#_参数测试">4.1. 参数测试</a></li>
  40. </ul>
  41. </li>
  42. <li><a href="#_mokito">5. Mokito</a>
  43. <ul class="sectlevel2">
  44. <li><a href="#_mock_vs_spy">5.1. mock vs spy</a></li>
  45. <li><a href="#_mock静态方法">5.2. mock静态方法</a></li>
  46. </ul>
  47. </li>
  48. <li><a href="#_测试controller">6. 测试Controller</a></li>
  49. <li><a href="#_数据库">7. 数据库</a></li>
  50. <li><a href="#_mybatis">8. MyBatis</a></li>
  51. <li><a href="#_mock外部服务">9. Mock外部服务</a></li>
  52. <li><a href="#_集成测试">10. 集成测试</a></li>
  53. <li><a href="#_spring_rest_docs">11. Spring Rest Docs</a></li>
  54. <li><a href="#_覆盖率">12. 覆盖率</a></li>
  55. <li><a href="#_最佳实践">13. 最佳实践</a></li>
  56. </ul>
  57. </div>
  58. </div>
  59. <div id="content">
  60. <div id="preamble">
  61. <div class="sectionbody">
  62. <div class="paragraph">
  63. <p>测试是软件开发中很重要的一部分工作,良好的测试可以在早期就发现系统存在的漏洞,保障系统发布后运行稳定。</p>
  64. </div>
  65. <div class="paragraph">
  66. <p>测试领域有着众多的方法论:单元测试、集成测试、功能测试、性能测试、压力测试、安全测试、回归测试、验收测试、全链路测试、混沌工程等等,
  67. 即使是有多年工作经验的测试人员也经常不能分辨清楚这么多概念。本文也不会对这些内容做过多说明,主要还是介绍对开发人员来说
  68. 比较重要的单元测试何集成测试方法</p>
  69. </div>
  70. <div class="imageblock">
  71. <div class="content">
  72. <img src="images/level-of-testing.png" alt="level of testing">
  73. </div>
  74. </div>
  75. <div class="paragraph">
  76. <p>之所以会要求开发掌握一定的测试能力是因为完全依赖测试人员人工测试是低效的也是不可靠的。在业务系统不断迭代过程中测试人员通常只会对新开发
  77. 的功能做测试,但新开发的功能是有可能影响到原来的功能的,最终导致客户使用过程中发现原来正常的功能突然又不可用了。
  78. 或者是开发人员盲目自信认为小的bug修复不会影响原有功能直接跳过了测试,结果上线运行后才发现异常。使用单元测试或者集成测试可以在项目构建时就发现问题,不论项目后期如何迭代,只要
  79. 原来的测试仍然能够执行通过就可以保证功能是正常的。</p>
  80. </div>
  81. </div>
  82. </div>
  83. <div class="sect1">
  84. <h2 id="_单元测试_vs_集成测试"><a class="anchor" href="#_单元测试_vs_集成测试"></a>1. 单元测试 vs 集成测试</h2>
  85. <div class="sectionbody">
  86. <div class="paragraph">
  87. <p>有些测试只对一些特殊的业务系统才有必要,但单元测试和集成测试是最通用的测试方法,任何业务系统都可以使用单元测试和集成测试来实现系统稳定性。</p>
  88. </div>
  89. <table class="tableblock frame-all grid-all stretch">
  90. <colgroup>
  91. <col style="width: 25%;">
  92. <col style="width: 25%;">
  93. <col style="width: 25%;">
  94. <col style="width: 25%;">
  95. </colgroup>
  96. <thead>
  97. <tr>
  98. <th class="tableblock halign-left valign-top">类型</th>
  99. <th class="tableblock halign-left valign-top">定义</th>
  100. <th class="tableblock halign-left valign-top">优点</th>
  101. <th class="tableblock halign-left valign-top">缺点</th>
  102. </tr>
  103. </thead>
  104. <tbody>
  105. <tr>
  106. <td class="tableblock halign-left valign-top"><p class="tableblock">单元测试</p></td>
  107. <td class="tableblock halign-left valign-top"><p class="tableblock">验证特定代码部分的功能的测试,最小的单元测试可能只是测试一个构造方法或者get/set方法,方法内存在if分支时通常会写多个单元测试去执行不同分支的代码</p></td>
  108. <td class="tableblock halign-left valign-top"><p class="tableblock">能保证某一部分代码完全正确,而且执行速度快</p></td>
  109. <td class="tableblock halign-left valign-top"><p class="tableblock">无法保证多个正确的代码段能否协同工作,而且每次对原方法修改后对应的单元测试都要修改,工作量比较大,还有涉及数据库操作的单元测试会使用内存数据库代替真实数据库,无法保证测试通过的代码在真实数据库上也能运行正常</p></td>
  110. </tr>
  111. <tr>
  112. <td class="tableblock halign-left valign-top"><p class="tableblock">集成测试</p></td>
  113. <td class="tableblock halign-left valign-top"><p class="tableblock">将各个代码块按照系统设计组合成完整的系统后进行的测试</p></td>
  114. <td class="tableblock halign-left valign-top"><p class="tableblock">更接近生产环境,在涉及数据库操作的系统中体现在会使用与生产环境一样的数据库进行集成测试,而且不需要经常重构,无论业务代码内部逻辑怎样变化,只要输入数据和预期的输出不变那么集成测试就不需要调整</p></td>
  115. <td class="tableblock halign-left valign-top"><p class="tableblock">执行速度慢,它要初始化系统的各个模块,调用时也需要执行各个模块的代码,正因为执行的代码更多,不能获得预期输出时定位问题也更难</p></td>
  116. </tr>
  117. </tbody>
  118. </table>
  119. <div class="paragraph">
  120. <p>综上,单元测试和集成测试各有优缺点,二者需要结合使用。</p>
  121. </div>
  122. </div>
  123. </div>
  124. <div class="sect1">
  125. <h2 id="_快速开始"><a class="anchor" href="#_快速开始"></a>2. 快速开始</h2>
  126. <div class="sectionbody">
  127. <div class="paragraph">
  128. <p>一个使用Junit 5开发的测试用例如下所示</p>
  129. </div>
  130. <div class="listingblock">
  131. <div class="content">
  132. <pre class="highlight"><code class="language-java" data-lang="java">class RoleServiceTest {
  133. @Test <i class="conum" data-value="1"></i><b>(1)</b>
  134. @DisplayName("测试用户组分页查询") <i class="conum" data-value="2"></i><b>(2)</b>
  135. void testFindRolePager() {
  136. Page&lt;Role&gt; rolePager = roleService.findRolePager(new Role(), PageRequest.of(0, 10)); <i class="conum" data-value="3"></i><b>(3)</b>
  137. assertEquals(2, rolePager.getTotalElements()); <i class="conum" data-value="4"></i><b>(4)</b>
  138. }
  139. }
  140. </code></pre>
  141. </div>
  142. </div>
  143. <div class="colist arabic">
  144. <table>
  145. <tr>
  146. <td><i class="conum" data-value="1"></i><b>1</b></td>
  147. <td>使用 <code>@Test</code> 注解声明这是一个测试方法</td>
  148. </tr>
  149. <tr>
  150. <td><i class="conum" data-value="2"></i><b>2</b></td>
  151. <td>使用 <code>@DisplayName</code> 注解描述测试内容,这不是必须的,使用方法名称描述是更好的选择</td>
  152. </tr>
  153. <tr>
  154. <td><i class="conum" data-value="3"></i><b>3</b></td>
  155. <td>执行业务方法</td>
  156. </tr>
  157. <tr>
  158. <td><i class="conum" data-value="4"></i><b>4</b></td>
  159. <td>执行断言方法</td>
  160. </tr>
  161. </table>
  162. </div>
  163. </div>
  164. </div>
  165. <div class="sect1">
  166. <h2 id="_框架"><a class="anchor" href="#_框架"></a>3. 框架</h2>
  167. <div class="sectionbody">
  168. <div class="ulist">
  169. <ul>
  170. <li>
  171. <p><a href="https://junit.org/junit5/">Junit 5</a> 基础测试框架</p>
  172. </li>
  173. <li>
  174. <p><a href="https://testng.org/doc/">TestNG</a> 基础测试框架</p>
  175. </li>
  176. <li>
  177. <p><a href="https://site.mockito.org/">Mockito</a> 用于生成Mock对象</p>
  178. </li>
  179. <li>
  180. <p><a href="https://github.com/wiremock/wiremock">wiremock</a> 用于服务端Mock</p>
  181. </li>
  182. <li>
  183. <p><a href="https://docs.spring.io/spring-framework/docs/current/reference/html/testing.html">Spring Test</a> 与Spring Framework配合使用的测试框架</p>
  184. </li>
  185. <li>
  186. <p><a href="https://docs.spring.io/spring-security/reference/servlet/test/index.html">Spring Security Test</a> 与Spring Security配合使用的测试工具,主要测试认证授权等功能</p>
  187. </li>
  188. <li>
  189. <p><a href="https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.testing">Spring Boot Test</a> Spring Boot 提供的测试模块,配置更少,功能更强大</p>
  190. </li>
  191. <li>
  192. <p><a href="https://www.testcontainers.org/">TestContainer</a> 用于在测试中创建容器</p>
  193. </li>
  194. <li>
  195. <p>&#8230;&#8203;</p>
  196. </li>
  197. </ul>
  198. </div>
  199. <div class="admonitionblock tip">
  200. <table>
  201. <tr>
  202. <td class="icon">
  203. <i class="fa icon-tip" title="Tip"></i>
  204. </td>
  205. <td class="content">
  206. 很多Java框架在核心模块之外都会提供一个单独的测试模块用来测试使用此框架实现的业务功能,所以在学习相关框架时也要关注它对测试的支持
  207. </td>
  208. </tr>
  209. </table>
  210. </div>
  211. <div class="paragraph">
  212. <p>眼花缭乱的测试框架也增加了编写测试的难度,所以建议从模仿做起,模仿开源项目的测试编写自己的业务测试,在使用中再去了解相关细节。</p>
  213. </div>
  214. </div>
  215. </div>
  216. <div class="sect1">
  217. <h2 id="_junit"><a class="anchor" href="#_junit"></a>4. JUnit</h2>
  218. <div class="sectionbody">
  219. <div class="sect2">
  220. <h3 id="_参数测试"><a class="anchor" href="#_参数测试"></a>4.1. 参数测试</h3>
  221. <div class="paragraph">
  222. <p>很多业务方法都是关于输入输出的,以特定的输入执行方法判断能否返回预期的输出,这种场景非常适合使用 <a href="https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests">参数测试</a></p>
  223. </div>
  224. <div class="paragraph">
  225. <p>下面是一个使用 <code>@ValueSource</code> 做参数测试的示例,<code>strings</code> 数组的值会分别作为参数执行测试方法</p>
  226. </div>
  227. <div class="exampleblock">
  228. <div class="content">
  229. <div class="listingblock">
  230. <div class="content">
  231. <pre class="highlight"><code class="language-java" data-lang="java">class MyTest {
  232. @ParameterizedTest
  233. @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
  234. void containsA(String candidate) {
  235. assertTrue(candidate.contains("a"));
  236. }
  237. }
  238. </code></pre>
  239. </div>
  240. </div>
  241. </div>
  242. </div>
  243. <div class="paragraph">
  244. <p>执行结果如下所示,三个参数的测试结果都是✔,表示测试通过</p>
  245. </div>
  246. <div class="listingblock">
  247. <div class="content">
  248. <pre>palindromes(String) ✔
  249. ├─ [1] candidate=racecar ✔
  250. ├─ [2] candidate=radar ✔
  251. └─ [3] candidate=able was I ere I saw elba ✔</pre>
  252. </div>
  253. </div>
  254. <div class="paragraph">
  255. <p>方法参数并不总是像上面示例那么简单,复杂参数列表可以使用 <code>@MethodSource</code> 实现</p>
  256. </div>
  257. <div class="exampleblock">
  258. <div class="content">
  259. <div class="listingblock">
  260. <div class="content">
  261. <pre class="highlight"><code class="language-java" data-lang="java">class MyTest {
  262. @ParameterizedTest
  263. @MethodSource("stringIntAndListProvider") <i class="conum" data-value="1"></i><b>(1)</b>
  264. void testWithMultiArgMethodSource(String str, int num, List&lt;String&gt; list) {
  265. assertEquals(5, str.length());
  266. assertTrue(num &gt;=1 &amp;&amp; num &lt;=2);
  267. assertEquals(2, list.size());
  268. }
  269. static Stream&lt;Arguments&gt; stringIntAndListProvider() {
  270. return Stream.of(
  271. arguments("apple", 1, Arrays.asList("a", "b")),
  272. arguments("lemon", 2, Arrays.asList("x", "y"))
  273. );
  274. }
  275. }
  276. </code></pre>
  277. </div>
  278. </div>
  279. <div class="colist arabic">
  280. <table>
  281. <tr>
  282. <td><i class="conum" data-value="1"></i><b>1</b></td>
  283. <td>使用静态方法stringIntAndListProvider的返回值作为方法参数</td>
  284. </tr>
  285. </table>
  286. </div>
  287. </div>
  288. </div>
  289. </div>
  290. </div>
  291. </div>
  292. <div class="sect1">
  293. <h2 id="_mokito"><a class="anchor" href="#_mokito"></a>5. Mokito</h2>
  294. <div class="sectionbody">
  295. <div class="sect2">
  296. <h3 id="_mock_vs_spy"><a class="anchor" href="#_mock_vs_spy"></a>5.1. mock vs spy</h3>
  297. <div class="paragraph">
  298. <p>spy是mock和真实对象的结合,可以mock真实对象的部分方法,实际用的比较少</p>
  299. </div>
  300. </div>
  301. <div class="sect2">
  302. <h3 id="_mock静态方法"><a class="anchor" href="#_mock静态方法"></a>5.2. mock静态方法</h3>
  303. <div class="listingblock">
  304. <div class="content">
  305. <pre class="highlight"><code class="language-java" data-lang="java">MockedStatic&lt;ExternalTaskClient&gt; mockedStatic = mockStatic(ExternalTaskClient.class);
  306. ExternalTaskClientBuilder clientBuilder = mock(ExternalTaskClientBuilder.class, RETURNS_SELF);
  307. when(ExternalTaskClient.create()).thenReturn(clientBuilder);
  308. </code></pre>
  309. </div>
  310. </div>
  311. </div>
  312. </div>
  313. </div>
  314. <div class="sect1">
  315. <h2 id="_测试controller"><a class="anchor" href="#_测试controller"></a>6. 测试Controller</h2>
  316. <div class="sectionbody">
  317. </div>
  318. </div>
  319. <div class="sect1">
  320. <h2 id="_数据库"><a class="anchor" href="#_数据库"></a>7. 数据库</h2>
  321. <div class="sectionbody">
  322. <div class="paragraph">
  323. <p>在测试涉及数据库操作的方法时选择合适的数据库很重要,目前比较主流的方法是在单元测试中使用内存数据库保证执行速度,
  324. 在集成测试中使用与生产环境相同的数据库容器。</p>
  325. </div>
  326. <div class="paragraph">
  327. <p>Spring Boot提供了
  328. <a href="https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.testing.spring-boot-applications.autoconfigured-spring-data-jpa"><code>@AutoConfigureTestDatabase</code></a>
  329. 注解自动配置一个测试的 <code>DataSource</code> 替换应用中声明的 <code>DataSource</code>,
  330. 这个 <code>DataSource</code> 会根据classpath中的数据库驱动启动一个内存数据库。如果想使用真实数据库而不是内存数据库,可以设置注解的 <code>replace</code> 属性为 <code>Replace.NONE</code>,</p>
  331. </div>
  332. <div class="exampleblock">
  333. <div class="content">
  334. <div class="listingblock">
  335. <div class="content">
  336. <pre class="highlight"><code class="language-java" data-lang="java"><span class="fold-block hide-when-folded">import org.junit.jupiter.api.Test;
  337. import org.springframework.beans.factory.annotation.Autowired;
  338. import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
  339. import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
  340. import static org.assertj.core.api.Assertions.assertThat;
  341. </span><span class="fold-block">@DataJpaTest <i class="conum" data-value="1"></i><b>(1)</b>
  342. @AutoConfigureTestDatabase(replace = Replace.NONE) <i class="conum" data-value="2"></i><b>(2)</b>
  343. class MyRepositoryTests {
  344. @Autowired
  345. private UserRepository repository;
  346. @Test
  347. void testExample() throws Exception {
  348. User user = this.repository.findByUsername("sboot");
  349. assertThat(user.getUsername()).isEqualTo("sboot");
  350. assertThat(user.getEmployeeNumber()).isEqualTo("1234");
  351. }
  352. }
  353. </span></code></pre>
  354. </div>
  355. </div>
  356. <div class="colist arabic">
  357. <table>
  358. <tr>
  359. <td><i class="conum" data-value="1"></i><b>1</b></td>
  360. <td>@DataJpaTest已经继承了@AutoConfigureTestDatabase,所以如果不需要替换为真实数据源就不需要下面的 @AutoConfigureTestDatabase(replace = Replace.NONE)</td>
  361. </tr>
  362. <tr>
  363. <td><i class="conum" data-value="2"></i><b>2</b></td>
  364. <td>替换为 application.yml 中声明的真实数据源</td>
  365. </tr>
  366. </table>
  367. </div>
  368. </div>
  369. </div>
  370. <div class="admonitionblock warning">
  371. <table>
  372. <tr>
  373. <td class="icon">
  374. <i class="fa icon-warning" title="Warning"></i>
  375. </td>
  376. <td class="content">
  377. 尽管内存数据库与真实数据库的非常相似,但仍然有些真实数据库才支持的特性是内存数据库不具备的,所以在
  378. 测试一些使用了比较生僻的关键字的sql语句时要特别注意,当然如果是使用JPA或者Hibernate这样的框架则不必担心,它们
  379. 会在底层处理不同数据库的差异。
  380. </td>
  381. </tr>
  382. </table>
  383. </div>
  384. <div class="paragraph">
  385. <p><code>@TestContainer</code> 提供了一种使用代码管理容器的方法,它整合了Junit框架,也在测试执行前启动一个容器,这一特性在集成测试
  386. 中非常有用,因为集成测试既要保证环境与真实环境一致但又不能影响真实环境。除了支持各类型数据库外,
  387. TestContainer也支持启动Kafka、 ELasticsearch、 Nginx、 RabbitMQ等主流中间件,详情参考其 <a href="https://www.testcontainers.org/">官方文档</a>。</p>
  388. </div>
  389. <div class="paragraph">
  390. <p>@TestContainer与@SpringBootTest <a href="https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto.testing.testcontainers">整合</a>示例如下</p>
  391. </div>
  392. <div class="exampleblock">
  393. <div class="content">
  394. <div class="listingblock">
  395. <div class="content">
  396. <pre class="highlight"><code class="language-java" data-lang="java">@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
  397. @Testcontainers
  398. public class UserAndRoleIntegrationTest {
  399. private final String adminPassword = "123456";
  400. @Container
  401. static MySQLContainer&lt;?&gt; mySQLContainer = new MySQLContainer&lt;&gt;("mysql:8.0.28"); <i class="conum" data-value="1"></i><b>(1)</b>
  402. @DynamicPropertySource
  403. static void containerProperties(DynamicPropertyRegistry registry) { <i class="conum" data-value="2"></i><b>(2)</b>
  404. registry.add("spring.datasource.url", mySQLContainer::getJdbcUrl);
  405. registry.add("spring.datasource.driver-class-name", mySQLContainer::getDriverClassName);
  406. registry.add("spring.datasource.username", mySQLContainer::getUsername);
  407. registry.add("spring.datasource.password", mySQLContainer::getPassword);
  408. }
  409. @Test
  410. void adminLogin(@Autowired TestRestTemplate testRestTemplate) {
  411. MultiValueMap&lt;String, String&gt; parameters = new LinkedMultiValueMap&lt;&gt;();
  412. parameters.add("username", "admin");
  413. parameters.add("password", RsaBasedPasswordEncoder.encryptPassword(adminPassword));
  414. CommonResult loginResult = testRestTemplate.postForObject("/api/login", parameters, CommonResult.class);
  415. assertEquals(CommonResult.SUCCESS, loginResult.getState());
  416. assertEquals("登录成功", loginResult.getMessage());
  417. }
  418. }
  419. </code></pre>
  420. </div>
  421. </div>
  422. <div class="olist arabic">
  423. <ol class="arabic">
  424. <li>
  425. <p>启动一个容器</p>
  426. </li>
  427. <li>
  428. <p>使用容器的信息作为数据源的属性</p>
  429. </li>
  430. </ol>
  431. </div>
  432. </div>
  433. </div>
  434. </div>
  435. </div>
  436. <div class="sect1">
  437. <h2 id="_mybatis"><a class="anchor" href="#_mybatis"></a>8. MyBatis</h2>
  438. <div class="sectionbody">
  439. <div class="paragraph">
  440. <p>测试是整个应用构建过程中最耗时的阶段,为了提升应用构建效率,必须关注测试的执行时间。提升测试执行速度的一个重要方法是减少不必要的类加载或初始化,对使用Spring框架构建的
  441. 应用来说就是只加载需要的Bean。 <code>spring-boot-test-autoconfigure</code> 模块提供了大量 <code>@&#8230;&#8203;Test</code> 注解用于初始化Spring上下文并加载某一类Bean,常用的如 <code>@WebMvcTest</code>
  442. 指定仅加载一个或多个 <code>Controller</code> 的Bean, <code>@DataJpaTest</code> 则只加载 Spring Data JPA 的 <code>Repository</code> 的Bean,完整的注解列表参考
  443. <a href="https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.testing.spring-boot-applications">官方文档</a>。</p>
  444. </div>
  445. <div class="paragraph">
  446. <p>MyBatis仿造此设计在 <code>mybatis-spring-boot-starter-test</code> 模块中提供了 <code>@MybatisTest</code> 注解用于测试 <code>Mapper</code> 接口。</p>
  447. </div>
  448. <div class="listingblock">
  449. <div class="content">
  450. <pre class="highlight"><code class="language-java" data-lang="java">@Mapper
  451. public interface CityMapper {
  452. @Select("SELECT * FROM CITY WHERE state = #{state}")
  453. City findByState(@Param("state") String state);
  454. }
  455. @MybatisTest
  456. public class CityMapperTest {
  457. @Autowired
  458. private CityMapper cityMapper;
  459. @Test
  460. public void findByStateTest() {
  461. City city = cityMapper.findByState("CA");
  462. assertThat(city.getName()).isEqualTo("San Francisco");
  463. assertThat(city.getState()).isEqualTo("CA");
  464. assertThat(city.getCountry()).isEqualTo("US");
  465. }
  466. }
  467. </code></pre>
  468. </div>
  469. </div>
  470. <div class="admonitionblock tip">
  471. <table>
  472. <tr>
  473. <td class="icon">
  474. <i class="fa icon-tip" title="Tip"></i>
  475. </td>
  476. <td class="content">
  477. @MybatisTest也继承了上文的 @AutoConfigureTestDatabase ,默认会启动一个内存数据库作为数据源
  478. </td>
  479. </tr>
  480. </table>
  481. </div>
  482. <div class="paragraph">
  483. <p>关于 <code>@MybatisTest</code> 的其他高级用法参考 <a href="http://mybatis.org/spring-boot-starter/mybatis-spring-boot-test-autoconfigure/">官方文档</a></p>
  484. </div>
  485. </div>
  486. </div>
  487. <div class="sect1">
  488. <h2 id="_mock外部服务"><a class="anchor" href="#_mock外部服务"></a>9. Mock外部服务</h2>
  489. <div class="sectionbody">
  490. <div class="paragraph">
  491. <p>测试中经常需要解决的问题是对外部环境或外部服务的依赖,<code>spring-test</code> 提供了一种 <a href="https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.testing.spring-boot-applications.autoconfigured-rest-client">Mock外部服务</a>的方法,可以在调用外部接口时返回Mock的数据,
  492. 保证代码能顺利执行。</p>
  493. </div>
  494. <div class="listingblock">
  495. <div class="content">
  496. <pre class="highlight"><code class="language-java" data-lang="java">@Service
  497. public class MyService {
  498. private final RestTemplate restTemplate;
  499. public MyService(RestTemplateBuilder restTemplateBuilder) {
  500. this.restTemplate = restTemplateBuilder.build();
  501. }
  502. public String invokeOutsideService() {
  503. return restTemplate.getForObject("http://outside.com/hello", String.class);
  504. }
  505. }
  506. @RestClientTest(MyService.class)
  507. public class MyServiceTest {
  508. @Autowired
  509. private MyService myService;
  510. @Autowired
  511. private MockRestServiceServer server;
  512. @Test
  513. void testInvokeOutsideService() {
  514. server.expect(requestTo("http://outside.com/hello")).andRespond(withSuccess("world", MediaType.TEXT_PLAIN));
  515. String hello = myService.invokeOutsideService();
  516. assertEquals("world", hello);
  517. }
  518. }
  519. </code></pre>
  520. </div>
  521. </div>
  522. </div>
  523. </div>
  524. <div class="sect1">
  525. <h2 id="_集成测试"><a class="anchor" href="#_集成测试"></a>10. 集成测试</h2>
  526. <div class="sectionbody">
  527. </div>
  528. </div>
  529. <div class="sect1">
  530. <h2 id="_spring_rest_docs"><a class="anchor" href="#_spring_rest_docs"></a>11. Spring Rest Docs</h2>
  531. <div class="sectionbody">
  532. <div class="paragraph">
  533. <p>接口开发完成后就要编写接口文档,目前主要的编写方法包括下面几种</p>
  534. </div>
  535. <div class="ulist">
  536. <ul>
  537. <li>
  538. <p>纯人工编写</p>
  539. </li>
  540. <li>
  541. <p>使用Swagger工具生成</p>
  542. </li>
  543. </ul>
  544. </div>
  545. <div class="paragraph">
  546. <p>上面几种方案都存在一些问题,纯人工编写工作量较大,Swagger工具对代码侵入性太强</p>
  547. </div>
  548. <div class="paragraph">
  549. <p>Spring Rest Docs是Spring团队开发的用于生成接口文档的框架,它利用 <code>spring-test</code> 提供的 <code>Spring MVC Test</code> 生成
  550. asciidoc格式的接口信息文档,再与手工编写的其他内容结合生成最终的接口文档。它的主要特点是对业务代码无侵入而且开发者能够自由
  551. 地编辑文档内容,同时由于接口信息是使用测试生成的,可以保证接口信息与实际代码始终是一致的。</p>
  552. </div>
  553. </div>
  554. </div>
  555. <div class="sect1">
  556. <h2 id="_覆盖率"><a class="anchor" href="#_覆盖率"></a>12. 覆盖率</h2>
  557. <div class="sectionbody">
  558. <div class="paragraph">
  559. <p>测试覆盖率用来衡量测试的完整性, <a href="https://www.jacoco.org/jacoco/">Jacoco</a>是目前最流行的测试覆盖率计算工具,它提供了非常美观的报告
  560. 展示已经被测试执行过的代码和未执行过的代码。
  561. Jacoco支持多种运行方式如Java Agent、Ant、Maven,Maven是最常用的方法,Maven构建后在 target/site/jacoco 目录下
  562. 会生成html格式的测试报告,可以直接在浏览器中打开查看。</p>
  563. </div>
  564. </div>
  565. </div>
  566. <div class="sect1">
  567. <h2 id="_最佳实践"><a class="anchor" href="#_最佳实践"></a>13. 最佳实践</h2>
  568. <div class="sectionbody">
  569. <div class="ulist">
  570. <ul>
  571. <li>
  572. <p>测试方法名做到见名知意</p>
  573. </li>
  574. <li>
  575. <p>测试执行速度越快越好,减少不必要的初始化</p>
  576. </li>
  577. <li>
  578. <p>时间紧迫情况下只写集成测试</p>
  579. </li>
  580. <li>
  581. <p>经常修改的代码要写单元测试</p>
  582. </li>
  583. <li>
  584. <p>测试代码量比业务代码多是很正常的,因为一个业务方法可能需要三到五个测试方法保证其运行正常</p>
  585. </li>
  586. <li>
  587. <p>使用固定数据作为输入,例如 <code>new Date()</code> 是变化的输入</p>
  588. </li>
  589. <li>
  590. <p>避免 <code>assertTrue</code> 和 <code>assertFalse</code></p>
  591. </li>
  592. <li>
  593. <p>使用参数测试减少重复代码</p>
  594. </li>
  595. <li>
  596. <p>不使用Spring依赖注入</p>
  597. </li>
  598. <li>
  599. <p>使用构造器注入</p>
  600. </li>
  601. </ul>
  602. </div>
  603. </div>
  604. </div>
  605. </div>
  606. <div id="footer">
  607. <div id="footer-text">
  608. Last updated 2024-03-18 05:44:42 UTC
  609. </div>
  610. </div>
  611. <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.3/highlight.min.js"></script>
  612. <script>
  613. if (!hljs.initHighlighting.called) {
  614. hljs.initHighlighting.called = true
  615. ;[].slice.call(document.querySelectorAll('pre.highlight > code')).forEach(function (el) { hljs.highlightBlock(el) })
  616. }
  617. </script>
  618. <script src="https://utteranc.es/client.js"
  619. repo="pxzxj/articles"
  620. issue-term="title"
  621. label="utteranc"
  622. theme="github-light"
  623. crossorigin="anonymous"
  624. async>
  625. </script>
  626. </div>
  627. </div>
  628. </div>
  629. </body>
  630. </html>